Data Persistence & Migrations
PearDrop separates write and read data models using Entity Framework Core (EF Core) to optimize each path independently. This section covers how persistence works in PearDrop and how to manage database schema evolution.
Architecture Overview
┌─────────────────────────────────────────────────────┐
│ Domain Aggregates (Write Side) │
│ User, Role, Task, Equipment, etc. │
├─────────────────────────────────────────────────────┤
│ Write DbContext (PearDropDbContext<T>) │
│ - Entity configurations │
│ - Domain validation │
│ - Change tracking & persistence modifiers │
├─────────────────────────────────────────────────────┤
│ Migrations (Schema versioning) │
│ - Data/WriteModel/Migrations/Generated/ │
├─────────────────────────────────────────────────────┤
│ SQL Server Database │
│ - Tables with TenantId for multi-tenancy │
│ - Stored procedures, views │
└─────────────────────────────────────────────────────┘
↡ (after SaveChanges)
┌─────────────────────────────────────────────────────┐
│ Read Model (Denormalized Projections) │
│ UserProjection, RoleProjection, TaskProjection │
├─────────────────────────────────────────────────────┤
│ Read DbContext (PearDropReadDbContextBase<T>) │
│ - Maps to views or denormalized tables │
│ - No business logic, query-optimized │
├─────────────────────────────────────────────────────┤
│ SQL Server Database │
│ - Views (vw_User, vw_Role, etc.) │
│ - Flattened, pre-calculated data │
└─────────────────────────────────────────────────────┘
Folder Structure
Each module follows a standardized data structure:
MyModule/
├── Data/
│ ├── WriteModel/
│ │ ├── MyModuleWriteDbContext.cs ← Write model DbContext
│ │ ├── MyModuleWriteDbContextFactory.cs ← Factory for DI
│ │ ├── EntityConfigs/
│ │ │ ├── UserMutableTypeConfiguration.cs ← Entity mappings
│ │ │ └── RoleMutableTypeConfiguration.cs
│ │ └── Migrations/
│ │ └── Generated/
│ │ ├── 20240101_InitialCreate.cs
│ │ ├── 20240115_AddColumns.cs
│ │ └── MyModuleWriteDbContextModelSnapshot.cs
│ │
│ ├── ReadModel/
│ │ ├── MyModuleReadDbContext.cs ← Read model DbContext
│ │ ├── MyModuleReadModels.cs ← Read models interface impl
│ │ └── Projections/
│ │ ├── UserProjection.cs
│ │ └── RoleProjection.cs
│ │
│ └── Constants.cs ← Table name prefixes
│
├── Domain/
│ └── {AggregateRoot}/
│ └── AggregateRoot/User.cs ← Domain aggregates
│
└── Module.cs ← Service registration
Key PearDrop Classes
Write Side
-
PearDropDbContext<T>: Base class for write models- Inherits from EF Core
DbContext - Supports multi-tenancy via
IMultiTenantContextAccessor - Integrates change processors and persistence modifiers
- Module identifier for audit trails
- Inherits from EF Core
-
IEntityTypeConfiguration<T>: Standard EF Core interface- Implemented by entity configuration classes
- Located in
Data/WriteModel/EntityConfigs/
-
IMutableTypeConfiguration: PearDrop marker interface- Indicates configuration handles domain aggregate persistence
- Used for filtering configuration application
Read Side
-
PearDropReadDbContextBase<T>: Base class for read models- Maps projection entities (read-optimized)
- Maps to database views (not domain aggregates)
- Separate from write DbContext (isolation)
-
IModuleReadModels: Interface exposing queryable read models- Implemented per module (e.g.,
IAuthReadModels) - Provides
IReadModelQueryable<T>properties for queries - Prevents direct DbContext exposure
- Implemented per module (e.g.,
Connection String Configuration
PearDrop uses standard ASP.NET Core configuration:
{
"ConnectionStrings": {
"PearDrop": "Server=localhost,1440;Database=MyApp;User Id=sa;Password=...;",
"PearDrop-Auth": "Server=localhost,1440;Database=MyApp;User Id=sa;Password=...;",
"PearDrop-Files": "Server=localhost,1440;Database=MyApp;User Id=sa;Password=...;"
}
}
Resolution order in AddPearDropSqlServerDbContextFactory<T>():
ConnectionStrings:{connectionStringName}(e.g., "PearDrop-Auth")ConnectionStrings:PearDrop(fallback for application modules)- Exception if neither found
Multi-Tenancy Integration
Write DbContext automatically integrates tenant isolation:
// Constructor receives IMultiTenantContextAccessor
public class AuthDbContext : PearDropDbContext<AuthDbContext>
{
public AuthDbContext(
DbContextOptions<AuthDbContext> options,
IMultiTenantContextAccessor<PearDropTenantInfo> multiTenantContextAccessor,
// ... other dependencies
) : base("auth", options, multiTenantContextAccessor, ...)
{
// Tenant context automatically available
}
}
// Entity configuration adds TenantId column
users.Property(user => user.TenantId).HasColumnName("TenantId");
// Query filters apply automatically in command handlers
Workflow: Adding a New Aggregate
-
Define the aggregate in
Domain/{Context}/AggregateRoot/public class Equipment : Entity, IAggregateRoot
{
public string Name { get; private set; }
public Guid TenantId { get; set; } // Required for multi-tenancy
} -
Create entity configuration in
Data/WriteModel/EntityConfigs/public class EquipmentMutableTypeConfiguration
: EntityTypeConfigurationBase<Equipment>, IMutableTypeConfiguration
{
public override void Configure(EntityTypeBuilder<Equipment> builder)
{
builder.ToTable($"{Constants.TablePrefix}equipment");
builder.HasKey(e => e.Id);
builder.Property(e => e.Name).IsRequired();
builder.Property(e => e.TenantId).HasColumnName("TenantId");
}
} -
Apply configuration in DbContext
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfiguration(new EquipmentMutableTypeConfiguration());
} -
Create migration
dotnet ef migrations add CreateEquipmentTable `
--project MyModule `
--startup-project MyApp `
--context MyModuleWriteDbContext -
Apply migration to database
dotnet ef database update --project MyModule --startup-project MyApp
Next Steps
- Write Model & DbContext - Set up write-side persistence
- Entity Configuration - Map domain aggregates to tables
- Read Models - Optimize queries with denormalized projections
- Creating Migrations - Schema version control
- Running Migrations - Apply schema changes via CLI or code