Migrations
Migrations turn a model change into a versioned, reviewable schema change you can apply to a database and roll back.
dotnet ef migrations add AddProducts → dotnet ef database updateYour entity classes describe a schema. The database has an actual schema. A migration is the recorded difference between the two, stored as C# code in your project.
The loop
Install the tool once with dotnet tool install --global dotnet-ef, and add the Microsoft.EntityFrameworkCore.Design package to the project. Then change your classes and run dotnet ef migrations add AddProducts. EF Core compares your model against the snapshot of the previous model and writes three files into a Migrations folder: the migration, a designer file, and an updated snapshot. All three belong in source control.
dotnet ef database update applies every migration the database has not seen yet. EF Core records applied migrations in a table it owns, __EFMigrationsHistory, which is how it knows what is left to do.
Going backwards
Each migration has an Up method and a Down method. Up moves the schema forward, Down undoes it. dotnet ef database update AddProducts runs the Down of everything newer; dotnet ef database update 0 reverts all of them. dotnet ef migrations remove deletes a migration you have not applied yet.
Applying in production
For deployment, generate SQL with dotnet ef migrations script --idempotent so a human can read it, or a single-file bundle with dotnet ef migrations bundle. Calling Database.MigrateAsync() at startup takes a database-wide lock as of EF Core 9, so replicas no longer race each other, but the application then needs rights to change the schema, nobody reviews the SQL, and during a rolling deployment old replicas keep serving traffic against the new schema. Prefer a one-shot job before the new version starts.
Example
// dotnet add package Microsoft.EntityFrameworkCore.Design
// dotnet ef migrations add AddProducts writes the file below
// dotnet ef database update runs Up against the database
using Microsoft.EntityFrameworkCore.Migrations;
Console.WriteLine("Apply this migration with: dotnet ef database update");
// The generated file is Migrations/20260214093000_AddProducts.cs and also
// carries [DbContext(typeof(ShopContext))] and [Migration("20260214093000_AddProducts")];
// the attributes are left off here so this file compiles on its own.
public partial class AddProducts : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "products",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Name = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_products", x => x.Id);
});
}
// Down is the exact reverse of Up, and it is what a rollback runs.
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(name: "products");
}
}
// Output:
// Apply this migration with: dotnet ef database updateWhen to use it
- Add a column to a live table without dropping the table or losing its rows.
- Hand a DBA a reviewable SQL script for a release instead of letting the app change the schema.
- Roll a bad schema change back to the previous migration while you fix the code.
More examples
The commands, in the order you run them
Keep this sequence next to you while learning. The idempotent script checks the history table itself, so it is safe to run against a database whose exact migration state you do not know.
// Once per machine:
// dotnet tool install --global dotnet-ef
// Once per project:
// dotnet add package Microsoft.EntityFrameworkCore.Design
// After changing the entity classes:
// dotnet ef migrations add AddSkuIndex
// dotnet ef migrations list shows which are applied
// dotnet ef database update applies them locally
// Made a mistake and have not applied it yet:
// dotnet ef migrations remove
// For a deployment someone has to approve:
// dotnet ef migrations script --idempotent --output artifacts/migrations.sql
// For an automated deployment, a single executable that needs no SDK:
// dotnet ef migrations bundle --self-contained -r linux-x64 --output artifacts/efbundle
// ./efbundle --connection "$DEPLOY_CONNECTION"
// In continuous integration, fail the build if someone changed the model
// but forgot the migration:
// dotnet ef migrations has-pending-model-changesEditing a generated migration to keep the data
EF Core compares two models and cannot see intent, so a rename looks like a deletion plus an addition. Read every generated migration before applying it, and fix this one by hand. Also fix Down, or the rollback loses the data instead.
using Microsoft.EntityFrameworkCore.Migrations;
public partial class RenameTitleToName : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
// What EF Core generated, which would lose every value:
// migrationBuilder.DropColumn(name: "Title", table: "products");
// migrationBuilder.AddColumn<string>(name: "Name", table: "products", ...);
// What to replace it with:
migrationBuilder.RenameColumn(
name: "Title",
table: "products",
newName: "Name");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.RenameColumn(
name: "Name",
table: "products",
newName: "Title");
}
}Migrating at startup, and why to avoid it
The lock removes the multi-replica race, not the other costs: the running application needs schema permissions, no human sees the SQL, there is no rollback step, and a slow migration delays startup past a readiness probe. Keep it to development.
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<ShopContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("Shop")));
var app = builder.Build();
// Acceptable for a single-replica internal tool. Since EF Core 9 this takes a
// database-wide lock, so two replicas cannot corrupt each other, and it throws
// if the model has changes with no migration behind them.
if (app.Environment.IsDevelopment())
{
using IServiceScope scope = app.Services.CreateScope();
ShopContext db = scope.ServiceProvider.GetRequiredService<ShopContext>();
await db.Database.MigrateAsync();
}
app.Run();
// In production, run the bundle as a job before the new version rolls out.
// Never call EnsureCreatedAsync before MigrateAsync: EnsureCreated builds the
// schema without recording any migration, and MigrateAsync then fails.
Discussion