EF Core Migrations: Add-Migration, Bundles, Idempotent Scripts and CI

Entity Framework Core generates a migration by comparing your model with a snapshot of the model as it was after the previous migration, and writes a C# class whose Up and Down methods describe the change. Applying it is a separate decision with four reasonable answers, and the model snapshot is where teams collide. This guide covers the files, the four ways to apply, the history table and a pipeline that does not depend on a developer's laptop.

What Add-Migration actually creates#

Running dotnet ef migrations add CreateOwnersAndPets (or Add-Migration in the Package Manager Console) writes three files into the Migrations folder:

Migrations/
  20240101120000_CreateOwnersAndPets.cs            the migration: Up() and Down()
  20240101120000_CreateOwnersAndPets.Designer.cs   attributes and the target model of this migration
  ClinicContextModelSnapshot.cs                    the whole model after the newest migration

The first file is the one you read and edit. Its shape is always the same:

[Migration("20240101120000_CreateOwnersAndPets")]
public partial class CreateOwnersAndPets : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.CreateTable(
            name: "owners",
            columns: table => new
            {
                id = table.Column<int>(nullable: false),
                first_name = table.Column<string>(maxLength: 50, nullable: false),
                last_name = table.Column<string>(maxLength: 50, nullable: false),
                telephone = table.Column<string>(maxLength: 20, nullable: true)
            },
            constraints: table =>
            {
                table.PrimaryKey("pk_owners", x => x.id);
            });
    }

    protected override void Down(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.DropTable(name: "owners");
    }
}

The Designer.cs file is the other half of the same partial class. It carries the [DbContext] and [Migration] attributes that tie the class to your context and give it its id, and a BuildTargetModel method describing the model at that point in history, which EF uses for operations such as Down. The ModelSnapshot.cs file is the current model in the same form; the next migrations add diffs your classes against it. You never edit the snapshot by hand.

Read the generated code before you commit it#

Generated migrations are correct with respect to the model and often wrong with respect to your intent. The two things to look for on every review:

  • Renames. A renamed property becomes DropColumn plus AddColumn. Replace the pair with migrationBuilder.RenameColumn, or the data is gone. The same applies to tables and indexes.
  • Store types. dotnet ef writes the provider's concrete types into the migration when it needs to, nvarchar(max) for SQL Server, text for PostgreSQL. A migration generated against one provider will not necessarily run on another. If the project targets several databases, keep the migrations per provider or omit explicit type: arguments where the CLR type is enough, as the example above does.

Anything the fluent API cannot say goes into migrationBuilder.Sql("..."): backfills, partial indexes, check constraints with expressions, view definitions. Put a matching statement in Down or document why there is none.

Four ways to apply a migration#

MethodCommand or codeGood forWatch out for
CLI against a databasedotnet ef database updateDevelopment, small teamsNeeds the source, the SDK and the connection string on the machine that runs it
At application startupcontext.Database.Migrate()Single-instance apps, demosSeveral replicas race each other; the app needs DDL rights; a failed migration takes the app down
SQL scriptdotnet ef migrations script --idempotent -o migrate.sqlDBA review, regulated environmentsThe script must be generated from the same commit that is deployed
Migration bundledotnet ef migrations bundle -o efbundle, then ./efbundle --connection "..."Pipelines, containers, no SDK on the targetBuild the bundle in CI and treat it as an artifact of the release

The idempotent script deserves a note. Without --idempotent, the script contains the DDL of every migration between two points and assumes the database is exactly at the starting point. With it, every migration is wrapped in a check against the history table, so the same script can be run against databases at different versions and only does what each one is missing. It is the format DBAs tend to accept, because they can read every statement before running it.

Bundles are the newest option and the most pipeline-friendly: a single executable that contains the migrations and the provider, run with a connection string, exit code zero on success. A self-contained bundle does not even need the .NET runtime on the target host.

The history table#

EF Core records applied migrations in __EFMigrationsHistory, one row per migration id plus the EF version that applied it. Every application method consults that table, and migrations script --idempotent generates IF NOT EXISTS checks against it. Recent EF Core versions also create a __EFMigrationsLock table and take a lock in it while migrating, so that two processes applying migrations at the same time no longer corrupt each other; it is a safety net, not a reason to migrate from several replicas on purpose.

Both tables are bookkeeping, not part of your domain. Whatever draws or documents your schema should leave them out, in the same way it leaves out Flyway's history table or Django's django_migrations. The guide to reading a schema diagram lists the usual suspects.

A pipeline that does not need a laptop#

  1. On every pull request, build the solution and run dotnet ef migrations has-pending-model-changes; a non-zero exit means someone changed the model and forgot to add a migration.
  2. Still on the pull request, apply the full migration set to a throw-away database in a container. This proves that the migrations run from scratch and catches provider-specific SQL that only fails on the real engine. The article on testing migrations with Docker shows the setup.
  3. On release, produce either an idempotent script or a bundle from the tagged commit and store it with the build artifacts.
  4. Deploy in two steps: run the bundle or script with a connection that has DDL rights, then roll out the application with a connection that does not.

Separating the migration step from the application start is the part that pays for itself first. It gives you a clear failure point, a log you can read, and an application identity that cannot alter tables even if a bug or an attacker asks it to.

Surviving model snapshot conflicts#

Two developers add a migration on two branches; both branches modify ClinicContextModelSnapshot.cs; the second merge produces a conflict in generated code that nobody wants to resolve by hand. The reliable procedure:

  1. On your branch, before rebasing, remove your migration with dotnet ef migrations remove. This deletes the migration files and rewinds the snapshot to the previous state.
  2. Rebase onto main, which now contains the other developer's migration and snapshot.
  3. Run dotnet ef migrations add again. Your migration is regenerated on top of the current snapshot, with a newer timestamp, and the snapshot is updated cleanly.

Do not resolve the snapshot by keeping both sides or by accepting one side; the next migrations add would generate a migration that undoes the half you dropped. If a migration has already been applied to a shared database, do not remove it; add a new migration instead and, if necessary, regenerate only the snapshot by adding and then deleting an empty migration.

Where SchemaDrawer fits#

An EF Core migrations folder can be compiled and applied without the project it came from, as long as the migration classes stand on their own. That is what SchemaDrawer does: upload the .cs files of your Migrations folder with migrationType=EF_CORE and the engine and version you deploy to, and the service compiles them into a small runner project, applies them to a throw-away database in Docker, reads back the tables, columns, primary keys and foreign keys over JDBC and returns an SVG diagram, with the history tables left out. The Designer.cs and ModelSnapshot.cs files reference your own DbContext and cannot compile there, so they are dropped; the migration ids are taken from the file names, which is where dotnet ef puts them anyway. EF Core migrations are drawn on PostgreSQL, MySQL, MariaDB and SQL Server; migrations that contain provider-specific store types should be drawn on the provider they were generated for. The EF Core page shows a complete upload and a GitHub Action step.

Key takeaways#

  • migrations add writes the migration, its Designer file and updates the model snapshot; review renames and store types in the first one.
  • Apply with the CLI in development, with an idempotent script or a bundle in pipelines, and avoid Database.Migrate() on multi-instance deployments.
  • __EFMigrationsHistory is the source of truth for what ran; hide it from schema documentation.
  • Resolve snapshot conflicts by removing, rebasing and regenerating, never by merging generated code.
Marek Hudyma
Builds SchemaDrawer, a service that draws database diagrams by running your migrations on a real engine. About
Keep reading

Related articles

Draw your schema in the next five minutes

One HTTP call with your migration files, one poll, one SVG. Free while we build it.