Chapter 15 · Migrations, Model Snapshots, Seeding, Scripts, Bundles, and Deployment

How Migrations Diff Models: Snapshot Semantics, Operations, and Migration History

Trace EF Core migrations from the current IModel and ModelSnapshot to migration operations, provider SQL, and __EFMigrationsHistory without confusing migrations with live-database diffing.

Advanced145–185 minutessnapshot + migration-history labEF Core 10.0.11 · .NET 10.0.11SQLite provider 10.0.11 mandatory baselinedotnet-ef 10.0.11 · SDK 10.0.400Last reviewed: August 2026

Learning outcomes

01

Explain the difference between the runtime IModel, the generated ModelSnapshot, migration source files, and the database history table.

02

Trace design-time context creation and explain why EF migration scaffolding compares models rather than diffing the live database.

03

Read Up/Down operations and the provider SQL they generate before applying a schema change.

04

Use __EFMigrationsHistory, migration-list APIs, and pending-model checks as different kinds of evidence.

05

Diagnose drift caused by manual database changes or deleted migration artifacts without rewriting history blindly.

06

Run a disposable SQLite migration lab with explicit verification and rollback/reset steps.

1. The practical problem: “the database already has the column—why does EF still want a migration?”

A ServiceHub developer adds a nullable dispatch code to WorkOrder. A teammate manually adds a similarly named column to a local SQLite file and expects dotnet ef migrations add to notice that the database is already correct. It does not. This is the first migration mental model to make explicit: scaffolding normally compares the current EF model with the last model snapshot checked into source control. The live database is not the general source of truth for the scaffold diff.

Reproducible baseline

Mandatory labs use .NET SDK 10.0.400, .NET runtime 10.0.11, Microsoft.EntityFrameworkCore/SQLite 10.0.11, dotnet-ef 10.0.11, the disposable servicehub-lab.db, deterministic ServiceHub seed data, the existing Guid Revision concurrency token, shadow audit fields, and the Chapter 12 outbox teaching model when referenced. EF Core 11 previews are excluded. Production credentials and databases are never used in destructive labs.

The current model is the finalized IModel built from conventions, attributes, and Fluent configuration. The snapshot is generated C# representing what EF believed the model looked like after the last migration. The migration class records operations needed to move from that snapshot toward the new model. The target database enters the story later, when EF asks which migration identifiers appear in __EFMigrationsHistory and executes missing migrations.

2. Four artifacts answer four different questions

Artifact Question it answers Do not confuse it with
context.Model / IModel What does this application build consider the EF model now? The physical database schema
ServiceHubContextModelSnapshot What model did the latest scaffolded migration record? A database backup or runtime cache
<timestamp>_Name.cs + designer metadata What operations/code represent one incremental schema transition? An automatically reviewed deployment plan
__EFMigrationsHistory Which migration IDs has this database recorded as applied? Proof that the live schema has no manual drift

A migration can therefore be perfectly “applied” according to history while the database also contains manual objects EF never modeled. Conversely, a developer can change the EF model without adding a migration; the database history remains unchanged because no migration exists to apply.

3. Design-time context creation comes before the diff

The EF tools must first construct the correct DbContext at design time. They may use application services, a parameterless/configurable path, or an IDesignTimeDbContextFactory<ServiceHubContext>. The course already established a design-time factory for the disposable SQLite lab. That factory must resolve the same provider/model configuration you intend to scaffold; it must not embed production credentials merely because tooling needs a connection path.

shell · inspect the design-time target before changing schema
dotnet tool run dotnet-ef --versiondotnet ef dbcontext info --project src/ServiceHub.EfLabdotnet ef migrations list --project src/ServiceHub.EfLab

dbcontext info proves which context/provider the tool constructed. It does not prove that a production database is at the expected migration. Keep design-time construction, model-diff evidence, and deployment-state evidence separate.

4. Make one additive model change and scaffold it

For the disposable Chapter 15 branch, add an optional routing precursor named DispatchCode. Nullable-first is intentional: it lets old rows remain valid while the application and deployment evolve.

csharp · teaching-only WorkOrder addition
public string? DispatchCode { get; private set; }
csharp · preserve the course snake_case relational convention
builder.Property(x => x.DispatchCode)    .HasColumnName("dispatch_code")    .HasMaxLength(32);
shell · scaffold into the established migrations directory
dotnet ef migrations add AddDispatchCode   --project src/ServiceHub.EfLab   --output-dir Data/Migrations

At this point stop. A successful scaffold is not approval to deploy. Review the generated C# and the changed snapshot in version control.

5. Read Up, Down, and the snapshot as executable design intent

csharp · representative migration operations
protected override void Up(MigrationBuilder migrationBuilder){    migrationBuilder.AddColumn<string>(        name: "dispatch_code",        table: "work_orders",        type: "TEXT",        maxLength: 32,        nullable: true);}protected override void Down(MigrationBuilder migrationBuilder){    migrationBuilder.DropColumn(        name: "dispatch_code",        table: "work_orders");}

The provider later turns these operations into database-specific DDL. On SQLite, AddColumn is directly supported; many other operations may require a table rebuild. The snapshot should now contain the dispatch_code mapping. If the migration file says one thing and a hand-edited snapshot says another, future diffs become unreliable.

Down is code, not a backup

Dropping a column in Down() can discard data. A reversible schema operation is not the same thing as reversible business data. Before destructive rollback, require a tested restore/data-reconstruction plan.

6. Generate SQL, apply, and verify the history table

shell · inspect SQL before applying
dotnet ef migrations script   --project src/ServiceHub.EfLab   --output artifacts/ch15-add-dispatch-code.sql
shell · apply only to the disposable lab database
dotnet ef database update   --project src/ServiceHub.EfLab   --connection "Data Source=servicehub-lab.db"
sql · SQLite verification
PRAGMA table_info('work_orders');SELECT "MigrationId", "ProductVersion"FROM "__EFMigrationsHistory"ORDER BY "MigrationId";

The expected evidence is a dispatch_code column plus a new migration-history row. That proves the target file recorded and physically reflects this migration. It still does not prove there are no out-of-band schema changes elsewhere.

7. Deliberately wrong: use the database as the migration authoring surface

sql · wrong: manual DDL first, migration story later
ALTER TABLE work_orders ADD COLUMN dispatch_code TEXT;

If you manually alter a database and then scaffold from the unchanged model/snapshot pair, EF's migration authoring process does not magically infer that manual action. Applying the generated migration may then fail because the column already exists. The repair depends on ownership: for a disposable local DB, recreate/reset it from migrations; for a shared database, investigate the exact drift and create/review a deliberate corrective migration or operational repair. Never “fix” production by deleting history rows until the migration plan looks convenient.

shell · detect model changes that were never captured
dotnet ef migrations has-pending-model-changes   --project src/ServiceHub.EfLab

This command compares the current model with the latest snapshot. It is useful in CI, but it does not inspect arbitrary live-database drift.

8. Hands-on lab: prove each layer independently

  1. Copy or recreate the disposable servicehub-lab.db; do not use shared data.
  2. Add the optional DispatchCode property and mapping.
  3. Run migrations has-pending-model-changes; it should report a difference before scaffolding.
  4. Scaffold AddDispatchCode, then inspect the migration and snapshot diff.
  5. Generate the SQL script and identify the DDL before execution.
  6. Apply the migration and query PRAGMA table_info plus __EFMigrationsHistory.
  7. For cleanup on the disposable DB, update back to the previous migration, then remove the unapplied latest migration from source if you do not intend to carry it forward.
shell · safe local rollback/remove sequence
dotnet ef database update PreviousMigration --project src/ServiceHub.EfLab   --connection "Data Source=servicehub-lab.db"dotnet ef migrations remove --project src/ServiceHub.EfLab

9. Production judgment and bridge to safe migration editing

Treat migration files and snapshots as reviewed source artifacts. CI should build the context, fail on pending model changes, and generate deployable evidence without production credentials. Deployment should verify the target database’s known migration state and use a controlled schema-change identity. Do not rely on startup migration as a default, and do not assume the history table detects manual schema drift. The next lesson addresses the point where scaffolded code itself is dangerous: renames, data moves, ordering, and repairs.

Check your understanding

  1. What two models are normally compared when scaffolding a migration?
  2. Does __EFMigrationsHistory prove the physical schema has no manual drift?
  3. What does has-pending-model-changes detect?
  4. Why inspect Up/Down before applying?
  5. Is Down() a backup strategy?
  6. Where should destructive migration labs run?
Review the answers

1. The current EF model and the last generated ModelSnapshot.

2. No. It records applied migration IDs, not a complete live-schema checksum.

3. Model changes not represented by the latest snapshot; it is not a live database diff.

4. They are executable schema intent and can contain destructive or provider-sensitive operations.

5. No. Schema rollback can lose or fail to reconstruct business data.

6. Only against disposable databases with explicit reset/restore paths.

Authoritative references

Use primary documentation when regenerating or adapting these deployment steps; migration behavior is provider- and version-sensitive.

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.

\n