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.

👉 Try it now — and feel the difference

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 change
  • Down() — 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:

Bash
dotnet tool install --global dotnet-ef

And add the design-time package to your project:

Bash
dotnet add package Microsoft.EntityFrameworkCore.Design

If 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.

C#
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:

Bash
dotnet ef migrations add InitialCreate

Visual Studio (Package Manager Console):

Bash
Add-Migration InitialCreate

Both 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:

C#
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:

  1. 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.
  2. 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:

Bash
dotnet ef database update

To target a specific migration (useful for rolling back, which we’ll cover shortly):

Bash
dotnet ef database update PreviousMigrationName
2. 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:

Bash
Update-Database

You can also target a specific migration the same way:

Bash
Update-Database -Migration PreviousMigrationName

Under 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:

C#
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:

Bash
dotnet ef migrations bundle

The 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.

EF Core performance optimization sponsor banner showing bulk insert, update, delete, and merge features with 14x faster data operations.

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:

C#
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:

C#
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:

C#
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:

Bash
dotnet ef migrations script

Generate a script between two specific migrations:

Bash
dotnet ef migrations script FromMigration ToMigration

The 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:

Bash
dotnet ef migrations script --idempotent

This 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:

Bash
dotnet ef migrations remove

If it has already been applied to the database, roll the database back to an earlier migration first:

Bash
dotnet ef database update PreviousMigrationName

EF 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:

Bash
dotnet ef database update 0

Common 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), __EFMigrationsHistory no 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 namesAddOrderNotesColumn tells you something; Migration20260809 doesn’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() and Down() method — always review both before applying.
  • You can apply migrations via dotnet ef database update (CLI) or Update-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.

👉 Insert data 14x faster with Bulk Insert

Found this article useful? Share it with your network and spark a conversation.