EF Core migrations are version-controlled snapshots of your database schema. Each migration represents a set of schema changes — creating tables, adding columns, changing indexes — with both an Up method (apply the change) and a Down method (reverse it). This history of migrations lets your team apply schema changes consistently across development, staging, and production environments without manual SQL scripts, and lets you roll back if something goes wrong.
Essential Migration Commands
// ── Install EF Core tools ──────────────────────────────────────────────────
// dotnet tool install --global dotnet-ef
// ── Create a migration ────────────────────────────────────────────────────
dotnet ef migrations add InitialCreate --project src/BlogApp.Infrastructure --startup-project src/BlogApp.Api
// ── Apply all pending migrations ─────────────────────────────────────────
dotnet ef database update --project src/BlogApp.Infrastructure --startup-project src/BlogApp.Api
// ── Roll back to a specific migration ─────────────────────────────────────
dotnet ef database update AddSlugIndex // apply up to (and including) this migration
// ── Roll back all migrations (empty database) ─────────────────────────────
dotnet ef database update 0
// ── List all migrations and their status ──────────────────────────────────
dotnet ef migrations list
// ── Remove the last migration (if not yet applied) ────────────────────────
dotnet ef migrations remove
// ── Generated migration file (snippet) ───────────────────────────────────
public partial class InitialCreate : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Posts",
columns: table => new
{
Id = table.Column<int>(nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
Title = table.Column<string>(maxLength: 200, nullable: false),
Slug = table.Column<string>(maxLength: 100, nullable: false),
Body = table.Column<string>(type: "nvarchar(max)", nullable: false),
IsPublished = table.Column<bool>(nullable: false),
CreatedAt = table.Column<DateTime>(nullable: false,
defaultValueSql: "GETUTCDATE()"),
},
constraints: table =>
{
table.PrimaryKey("PK_Posts", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_Posts_Slug",
table: "Posts",
column: "Slug",
unique: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(name: "Posts");
}
}
20250115143022_InitialCreate). EF Core records which migrations have been applied in the __EFMigrationsHistory table. On startup (MigrateAsync()) or on CLI command (dotnet ef database update), EF Core compares the migration history table against the code migration files and applies any not yet in the history table. This makes migration application deterministic and idempotent — running it twice is safe.await db.Database.MigrateAsync() inside a startup scope. This ensures every deployment automatically applies pending migrations without a separate deployment step. Add it in a using block that creates a scope: using var scope = app.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService<AppDbContext>(); await db.Database.MigrateAsync();. For large production databases, consider running migrations separately (not on startup) to avoid startup timeouts during long-running migrations.dotnet ef database update commands fail with an error or (worse) silently skip the edited migration. Always create a new migration for corrections.Auto-Apply Migrations at Startup
// ── Program.cs — apply pending migrations at startup ─────────────────────
var app = builder.Build();
// Apply migrations before handling any requests
using (var scope = app.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var logger = scope.ServiceProvider.GetRequiredService<ILogger<Program>>();
try
{
await db.Database.MigrateAsync();
logger.LogInformation("Database migrations applied successfully.");
}
catch (Exception ex)
{
logger.LogError(ex, "Database migration failed.");
throw; // fail fast — do not start the app with a broken schema
}
}
app.MapControllers();
app.Run();
Common Mistakes
Mistake 1 — Editing an applied migration (history mismatch)
❌ Wrong — changing a migration that is already in __EFMigrationsHistory; future migrations fail to apply.
✅ Correct — create a new migration for any schema corrections; never modify applied migrations.
Mistake 2 — Not including –project and –startup-project in multi-project solutions
❌ Wrong — running migration commands from the wrong directory; migrations added to wrong project.
✅ Correct — always specify both flags: --project src/BlogApp.Infrastructure --startup-project src/BlogApp.Api.