Creating Migrations
Database migrations manage schema evolution as your domain model changes. EF Core migrations are generated from your DbContext configurations and stored as C# files. In PearDrop, migrations are generated separately for write and read contexts.
What is a Migration?
A migration is a timestamped C# file that describes schema changes:
Data/
├── WriteModel/
│ └── Migrations/Generated/
│ ├── 20251201185932_InitialCreate.cs ← Migration file
│ ├── 20251201185932_InitialCreate.Designer.cs ← Generated helper
│ ├── 20251215092000_AddEquipmentCategory.cs ← Second migration
│ └── ModelSnapshot.cs ← Current schema state
└── ReadModel/
└── Migrations/Generated/
├── 20251201185932_InitialCreate.cs
└── ModelSnapshot.cs
File Structure:
- Migration: Up/Down methods defining schema changes
- Designer.cs: Metadata file (don't edit manually)
- ModelSnapshot.cs: Records current schema state (don't edit manually)
Tool Setup
Install EF Core Tools
# Install globally (recommended)
dotnet tool install --global dotnet-ef
# Or install locally in project
dotnet tool install dotnet-ef
Verify installation:
dotnet ef --version
# Output: Entity Framework Core .NET Command-line Tools 8.0.0
Generating Migrations
Command Structure
dotnet ef migrations add <MigrationName> \
--project <ProjectPath> \
--startup-project <StartupProjectPath> \
--context <DbContextClass> \
--output-dir <OutputDirectory>
Write Model Migration
# Navigate to solution root
cd c:\path\to\solution
# Add migration for write model
dotnet ef migrations add InitialCreate \
--project ".\modules\mymodule\source\MyApp.Module\MyApp.Module.csproj" \
--startup-project ".\samples\MyApp\MyApp.csproj" \
--context MyAppWriteDbContext \
--output-dir "Data/WriteModel/Migrations/Generated"
# Add subsequent migration
dotnet ef migrations add AddEquipmentCategory \
--project ".\modules\mymodule\source\MyApp.Module\MyApp.Module.csproj" \
--startup-project ".\samples\MyApp\MyApp.csproj" \
--context MyAppWriteDbContext \
--output-dir "Data/WriteModel/Migrations/Generated"
Read Model Migration
# Add migration for read model
dotnet ef migrations add InitialCreate \
--project ".\modules\mymodule\source\MyApp.Module\MyApp.Module.csproj" \
--startup-project ".\samples\MyApp\MyApp.csproj" \
--context MyAppReadDbContext \
--output-dir "Data/ReadModel/Migrations/Generated"
Parameters:
--project: Path to module containing DbContext--startup-project: Entry point (program.cs)--context: Fully qualified DbContext class name--output-dir: Where to place migration files (relative to project)
Example:Generated Migration File
// 20251201185932_InitialCreate.cs
using System;
using Microsoft.EntityFrameworkCore.Migrations;
nullable disable;
namespace PearDrop.Auth.Data.WriteModel.Migrations.Generated
{
/// <remarks>
/// This migration was generated by the EF Core tools.
/// IMPORTANT: Review this migration carefully before applying to production.
/// </remarks>
public partial class InitialCreate : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
// Create tables
migrationBuilder.CreateTable(
name: "peardrop_authentication_user",
schema: "dbo",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
PasswordHash = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: false),
TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
WhenDisabled = table.Column<DateTime>(type: "datetime2", nullable: true),
WhenVerified = table.Column<DateTime>(type: "datetime2", nullable: true),
},
constraints: table =>
{
table.PrimaryKey("PK_peardrop_authentication_user", x => x.Id);
});
// Create related tables
migrationBuilder.CreateTable(
name: "peardrop_authentication_authenticationHistory",
schema: "dbo",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
UserId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
AttemptedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
Succeeded = table.Column<bool>(type: "bit", nullable: false),
},
constraints: table =>
{
table.PrimaryKey("PK_peardrop_authentication_authenticationHistory", x => x.Id);
table.ForeignKey(
name: "FK_peardrop_authentication_authenticationHistory_peardrop_authentication_user_UserId",
column: x => x.UserId,
principalTable: "peardrop_authentication_user",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
// Create indexes
migrationBuilder.CreateIndex(
name: "IX_peardrop_authentication_user_TenantId",
table: "peardrop_authentication_user",
column: "TenantId");
migrationBuilder.CreateIndex(
name: "IX_peardrop_authentication_authenticationHistory_UserId",
table: "peardrop_authentication_authenticationHistory",
column: "UserId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
// Reverse operations in opposite order
migrationBuilder.DropTable(
name: "peardrop_authentication_authenticationHistory",
schema: "dbo");
migrationBuilder.DropTable(
name: "peardrop_authentication_user",
schema: "dbo");
}
}
}
Key Patterns:
Up(): Describes schema changes to apply (CreateTable, AddColumn, CreateIndex, etc.)Down(): Reverses changes (DropTable, DropColumn, DropIndex, etc.)- Tables use module prefix (e.g.,
peardrop_authentication_user) - Foreign keys include module prefix in constraint naming
- Indexes created for foreign keys and queried columns
Migration Best Practices
✅ Do:
- Create focused migrations for each logical change
- Review generated migration SQL before applying
- Use meaningful migration names (
AddEquipmentStatusField, notMigration20251201) - Commit migrations to version control immediately
- Test migrations in development before applying to production
- Document complex migrations with comments in
Up()method
❌ Don't:
- Edit generated migration files manually (regenerate instead)
- Modify
Designer.csorModelSnapshot.csfiles - Skip reviewing EF-generated SQL
- Mix multiple unrelated changes in one migration
- Apply migrations directly to production (use CI/CD pipeline)
- Manually modify
.Designer.csfiles generated by EF Core tools
Reviewing Generated SQL
To preview the actual SQL that will be executed:
# Generate and preview SQL without applying
dotnet ef migrations script \
--project ".\modules\mymodule\source\MyApp.Module" \
--startup-project ".\samples\MyApp\MyApp.csproj" \
--context MyAppWriteDbContext \
--from 20251201185932 \
--to 20251215092000 \
--output "migration-script.sql"
# Review migration-script.sql before running
This is critical for production to review schema changes with your DBA.
Common Migration Scenarios
Adding a Column
// In DbContext: Add property to entity
public class Equipment : Entity, IAggregateRoot
{
public string? ManufacturerModel { get; set; } // New property
}
// Generate migration
dotnet ef migrations add AddManufacturerModelToEquipment ^
--context MyAppWriteDbContext ^
--output-dir "Data/WriteModel/Migrations/Generated"
// Generated migration will include:
migrationBuilder.AddColumn<string>(
name: "ManufacturerModel",
table: "myapp_equipment",
type: "nvarchar(256)",
maxLength: 256,
nullable: true);
Changing Column Properties
// Modify entity configuration
builder.Property(e => e.Category)
.HasMaxLength(256); // Previously 128
// Generate migration
dotnet ef migrations add IncreaseCategory LengthToEquipment
// Generated migration updates column definition
Adding Foreign Key Relationship
// In Equipment entity - add relationship
public Guid CategoryId { get; set; }
public Category Category { get; set; } = null!;
// In configuration
builder.HasOne(e => e.Category)
.WithMany()
.HasForeignKey(e => e.CategoryId)
.OnDelete(DeleteBehavior.Restrict);
// Generate migration
dotnet ef migrations add AddCategoryForeignKeyToEquipment
Adding Index
// In configuration
builder.HasIndex(e => new { e.TenantId, e.Category })
.HasName("IX_Equipment_TenantId_Category");
// Generate migration
dotnet ef migrations add AddEquipmentTenantCategoryIndex
// Generated migration:
migrationBuilder.CreateIndex(
name: "IX_Equipment_TenantId_Category",
table: "myapp_equipment",
columns: new[] { "TenantId", "Category" });
Handling Migration Conflicts
Branch Conflicts
If two branches create migrations with same timestamp:
# Remove conflicting migration from your branch
dotnet ef migrations remove --context MyAppWriteDbContext
# Pull main branch migrations
git pull origin main
# Regenerate your migration after main's migrations
dotnet ef migrations add YourFeatureMigration --context MyAppWriteDbContext
Existing Database
If database was manually modified:
# Regenerate migrations to match current state
dotnet ef migrations add CreateFromExistingDatabase --context MyAppWriteDbContext
# Or reset and start fresh (development only!)
dotnet ef database drop --context MyAppWriteDbContext
dotnet ef database update --context MyAppWriteDbContext
CI/CD Pipeline Integration
Migrations are typically applied during deployment:
# Azure Pipelines example
- task: DotNetCoreCLI@2
displayName: 'Apply Migrations (Write)'
inputs:
command: 'custom'
custom: 'ef'
arguments: 'database update --project ./MyApp.Module --startup-project ./MyApp --context MyAppWriteDbContext'
- task: DotNetCoreCLI@2
displayName: 'Apply Migrations (Read)'
inputs:
command: 'custom'
custom: 'ef'
arguments: 'database update --project ./MyApp.Module --startup-project ./MyApp --context MyAppReadDbContext'
Next Steps
- Running Migrations - Apply migrations to databases
- Write Model DbContext - Understand DbContext inheritance
- Entity Configuration - Configure entities that generate migrations