Skip to main content

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
  • 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

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>():

  1. ConnectionStrings:{connectionStringName} (e.g., "PearDrop-Auth")
  2. ConnectionStrings:PearDrop (fallback for application modules)
  3. 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

  1. 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
    }
  2. 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");
    }
    }
  3. Apply configuration in DbContext

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
    modelBuilder.ApplyConfiguration(new EquipmentMutableTypeConfiguration());
    }
  4. Create migration

    dotnet ef migrations add CreateEquipmentTable `
    --project MyModule `
    --startup-project MyApp `
    --context MyModuleWriteDbContext
  5. Apply migration to database

    dotnet ef database update --project MyModule --startup-project MyApp

Next Steps