
EF Core Migrations: A Complete Guide for .NET Developers
Article Sponsors
EF Core too slow? Insert data up to 14x faster and cut save time by 94%. Boost performance with extension methods fully integrated into EF Core — Bulk Insert, Update, Delete, and Merge.
Join 5,000+ developers who’ve trusted our library since 2014.
Every growing application eventually hits the same wall: the database schema needs to change, but the app is already live, and someone’s data is sitting in it. Hand-writing ALTER TABLE scripts for every change works for a while — until a teammate forgets to run one, a script gets applied out of order, or production quietly drifts away from what’s in source control.
Entity Framework Core Migrations exist to solve exactly this problem. Instead of tracking schema changes in your head, EF Core lets you describe your data model in C#, and it generates the SQL needed to keep your database in sync — versioned, reviewable, and repeatable across every environment.
In this guide, we’ll walk through everything you need to actually use migrations with confidence: creating them, understanding what they generate, applying them through the CLI and Visual Studio, scripting them for production, rolling them back, and avoiding the mistakes that trip up most teams.
What Are EF Core Migrations?
A migration is a C# class that represents one incremental change to your database schema — adding a table, renaming a column, introducing an index, and so on. Each migration contains two methods:
Up()— the code that applies the changeDown()— the code that reverses it
EF Core keeps track of which migrations have already run using a table called __EFMigrationsHistory, which it creates automatically in your database. Every time you apply migrations, EF checks this table, figures out what hasn’t been applied yet, and runs only those changes. That’s what makes the process idempotent — safe to run repeatedly without side effects.
Alongside your migration files, EF Core also maintains a model snapshot — a single file that represents what your entire data model looked like after the last migration. When you add a new migration, EF compares your current model against this snapshot to figure out exactly what changed.
Prerequisites & Setup
Before creating your first migration, you’ll need the EF Core command-line tools installed.
Install (or update) the global tool:
dotnet tool install --global dotnet-efAnd add the design-time package to your project:
dotnet add package Microsoft.EntityFrameworkCore.DesignIf you’d rather work inside Visual Studio, the Package Manager Console (PMC) provides equivalent PowerShell commands (Add-Migration, Update-Database, and so on) — we’ll cover both throughout this guide.
Creating Your First Migration
Let’s start with a simple entity and a DbContext.
public sealed class Order
{
public int Id { get; set; }
public string CustomerName { get; set; } = string.Empty;
public decimal TotalAmount { get; set; }
public DateTime CreatedAt { get; set; }
}
public sealed class StoreDbContext : DbContext
{
public DbSet<Order> Orders => Set<Order>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Order>(builder =>
{
builder.Property(o => o.CustomerName).HasMaxLength(150);
builder.Property(o => o.TotalAmount).HasPrecision(18, 2);
builder.HasIndex(o => o.CreatedAt);
});
}
}With the model in place, generate your first migration:
dotnet ef migrations add InitialCreateVisual Studio (Package Manager Console):
Add-Migration InitialCreateBoth commands do the same thing — they create a Migrations folder in your project containing a timestamped migration file and an updated model snapshot.
Understanding the Generated Migration File
Opening the generated file, you’ll see something like this:
public partial class InitialCreate : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Orders",
columns: table => new
{
Id = table.Column<int>(nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
CustomerName = table.Column<string>(maxLength: 150, nullable: false),
TotalAmount = table.Column<decimal>(precision: 18, scale: 2, nullable: false),
CreatedAt = table.Column<DateTime>(nullable: false)
},
constraints: table => table.PrimaryKey("PK_Orders", x => x.Id));
migrationBuilder.CreateIndex(
name: "IX_Orders_CreatedAt",
table: "Orders",
column: "CreatedAt");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(name: "Orders");
}
}Two things worth internalizing here:
- Not every change is reversible. Dropping a column in
Up()means the data is gone —Down()can recreate the column, but not the values it once held. - Always read the generated file before applying it. EF Core is good at inferring intent, but it isn’t infallible — especially with renames, which it often interprets as “drop and recreate” unless you correct it (more on that below).
Applying Migrations to the Database
There are several ways to get a migration from your project into an actual database. Which one you use usually depends on your workflow and environment.
1. dotnet ef database update (CLI)
The most common way to apply pending migrations from the command line:
dotnet ef database updateTo target a specific migration (useful for rolling back, which we’ll cover shortly):
dotnet ef database update PreviousMigrationName2. Update-Database in Visual Studio
If you’re working inside Visual Studio, the Package Manager Console gives you the same capability without leaving the IDE:
Update-DatabaseYou can also target a specific migration the same way:
Update-Database -Migration PreviousMigrationNameUnder the hood, both dotnet ef database update and Update-Database do the same work: they check __EFMigrationsHistory, determine which migrations are outstanding, and run their Up() methods in order. Which one you reach for is largely a matter of whether you live in a terminal or in Visual Studio — functionally, they’re equivalent.
3. Applying Migrations Programmatically
You can also apply migrations from code by calling Migrate() on your DbContext:
using var scope = app.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<StoreDbContext>();
db.Database.Migrate();This is convenient for local development or spinning up databases for integration tests, but it’s not recommended for production. If the migration fails mid-way, you don’t get much control over recovery, and running it automatically on app startup can create race conditions when multiple instances start at once.
4. Migration Bundles
For CI/CD pipelines, EF Core can package your migrations into a self-contained executable:
dotnet ef migrations bundleThe resulting efbundle file can be shipped to a deployment environment and run there — no need for the .NET SDK or your full project to be present.
Customizing & Editing Migrations
EF Core gets things right most of the time, but there are cases where you need to step in.
Renaming a column is the classic example. If you rename a property in your model, EF Core might generate this by default:
migrationBuilder.DropColumn(name: "Description", table: "Orders");
migrationBuilder.AddColumn<string>(name: "Notes", table: "Orders", nullable: true);That’s a data-loss bug waiting to happen — it deletes the old column instead of renaming it. Fix it by hand:
migrationBuilder.RenameColumn(
name: "Description",
table: "Orders",
newName: "Notes");Custom SQL is also common when you need something the Fluent API can’t express — data backfills, complex constraints, or database-specific features:
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("UPDATE Orders SET Notes = '' WHERE Notes IS NULL");
}Just remember: if you add custom SQL to Up(), you’re responsible for writing the matching cleanup in Down().
Generating SQL Scripts with dotnet ef migrations script
In many real-world teams, migrations aren’t applied directly by developers — a DBA reviews a SQL script first, or a deployment pipeline runs it as a controlled step. That’s exactly what migrations script is for.
Generate a script covering every pending migration:
dotnet ef migrations scriptGenerate a script between two specific migrations:
dotnet ef migrations script FromMigration ToMigrationThe resulting .sql file can be handed to a DBA, checked into a deployment repo, or run manually — giving you a chance to review exactly what will happen before it happens.
Idempotent Scripts
If you’re not sure exactly which migration a given environment is currently on (common with multiple environments or long-lived deployments), generate an idempotent script:
dotnet ef migrations script --idempotentThis wraps each migration step in a check against __EFMigrationsHistory, so the script only applies changes that haven’t already run — safe to execute against a database at any prior state. This is the pattern most teams end up using for CI/CD pipelines, since it removes the guesswork of “what version is this environment on?”
Rolling Back Migrations
Sometimes a migration needs to be undone — either before it ships, or after something goes wrong.
If the migration hasn’t been applied yet, just remove it:
dotnet ef migrations removeIf it has already been applied to the database, roll the database back to an earlier migration first:
dotnet ef database update PreviousMigrationNameEF Core will run the Down() methods for every migration after your target, in reverse order. Once the database is back to the desired state, you can safely delete the unwanted migration file(s) and regenerate.
To revert everything, target a special built-in migration name:
dotnet ef database update 0Common Pitfalls
A few issues come up often enough to call out explicitly:
- Merge conflicts in the model snapshot. When multiple developers add migrations on different branches, the snapshot file often conflicts. Keeping your branch up to date with the latest migration before creating a new one minimizes this.
- Destructive changes slipping through. Dropping columns or tables is easy to do accidentally when EF Core misreads a rename. Always read the generated migration.
- Schema drift. If someone modifies the database directly (outside of a migration),
__EFMigrationsHistoryno longer reflects reality, and future migrations can fail unpredictably. - Manually editing
__EFMigrationsHistory. Deleting or altering rows in this table by hand can cause EF Core to attempt to re-run migrations that were already applied — usually surfacing as “table already exists” errors.
EF Core Migrations Best Practices
A short list of habits that consistently save teams pain down the line:
- Use descriptive migration names —
AddOrderNotesColumntells you something;Migration20260809doesn’t. - Keep each migration focused on one logical change. Small migrations are easier to review, test, and revert if needed.
- Review every generated migration before applying it, especially anything involving renames or drops.
- Test migrations in a staging environment that mirrors production before running them for real.
- Prefer SQL scripts for production deployments over calling
Migrate()at app startup — it gives you a review step and avoids startup race conditions. - Stay current with the latest migration on your branch before adding a new one, to avoid snapshot conflicts.
Alternatives to EF Core Migrations
EF Core Migrations aren’t the only option for schema versioning. If your team isn’t using EF Core as the ORM, or wants a database-first approach, a few well-established tools are worth knowing about:
- FluentMigrator — a .NET migration framework with a fluent API, independent of any ORM.
- DbUp — a lightweight library that applies plain SQL scripts in order.
- Flyway — a widely used, language-agnostic migration tool popular across polyglot teams.
Summary
- EF Core Migrations let you version your database schema in code, alongside the rest of your application.
- Every migration has an
Up()andDown()method — always review both before applying. - You can apply migrations via
dotnet ef database update(CLI) orUpdate-Database(Visual Studio’s Package Manager Console) — they’re functionally equivalent. dotnet ef migrations script(optionally with –idempotent) generates reviewable SQL, which is the safer path for production deployments.- Small, well-named, well-reviewed migrations are far easier to manage than large, unreviewed ones — especially in a team setting.
Migrations won’t eliminate every database headache, but used deliberately, they turn schema changes from a risky manual process into something versioned, testable, and boring — in the best possible way.
This article is sponsored by ZZZ Projects.
Thousands of developers fixed EF Core performance — with one library: Entity Framework Extensions.

