Skip to content

Course 5 · Module 3 — Migrating from EF Core migrations

EF Core migrations kept your schema in C#. Handy — your model and your tables move together, dotnet ef migrations add writes the diff for you. But every change spawns another timestamped migration class with an Up and a Down, a ModelSnapshot you can’t hand-edit quietly tracks the “current” model, and __EFMigrationsHistory on the server remembers which migrations ran. Three moving parts to keep in lockstep, and the database’s shape is scattered across a folder of Up() methods.

Let’s move that database to SchemaSmith — and let the table files be the model.

EF Core migrationsSchemaSmith
Migrations/*.cs (Up/Down steps)Declarative table files — the end state, not the steps to reach it
ShopContextModelSnapshot.cs (generated model cache)The table files are the model — nothing generated to diff against
__EFMigrationsHistoryNo history table — your definitions are the state
dotnet ef migrations add (diff model → new migration)No diff-then-record step — you declare the shape and commit it
dotnet ef database update (apply pending migrations)quench — converge the live database to the declared state
HasData seeding (seed rows baked into the model)DataTongs — seed and reference data as content files, kept apart from structure
Reverse-engineer / scaffold (DB-first)SchemaTongs extract — cast a live database straight to files

The shift is the first three rows. EF’s model is three parts in lockstep — the migration classes, the snapshot that caches “where we are,” and the history table that records “what ran.” SchemaSmith collapses all three into one: the table files. There’s no snapshot to drift, no history to consult, no Down() to get right. The files are the shape, and quench makes the database match.

EF gives you two modes: code-first (model drives the database) or database-first (scaffold the model from one). SchemaSmith is neither. It’s state-first — you declare the shape you want, and how you got there doesn’t enter into it.

The lab’s before/ folder shows a real EF Core project — ShopContext.cs, two migrations (CreateShop, AddOrderItem), and the ModelSnapshot — written for the SQL Server provider. That’s the only provider shown, on purpose: EF emits provider-specific SQL, but the model is the same everywhere. The identical ShopContext on Npgsql (PostgreSQL) or Pomelo (MySQL) produces the same four tables — only the generated column types differ (nvarchar vs text, datetime2 vs timestamp). You’re not migrating C# anyway. You’re migrating the database the C# produced, and that lives on the server in every dialect.

You don’t run EF. The setup already applied the model’s end state to shop_from_efcore on each engine — the four tables and __EFMigrationsHistory, exactly as dotnet ef database update would have.

Point SchemaTongs at that database. The settings file names the four tables you’re keeping:

"ShouldCast": { "ObjectList": "dbo.Customer,dbo.Product,dbo.SalesOrder,dbo.OrderItem" }
Terminal window
cd sqlserver
schematongs --ConfigFile:SchemaTongs.settings.json
=== Casting Summary ===
Tables: 4 extracted, 0 errors

Four tables. No __EFMigrationsHistory.json — you named the tables you wanted, and the history table wasn’t on the list. You don’t delete it; you just don’t invite it.

Now quench the package back to prove it captured the state:

Terminal window
schemaquench --ConfigFile:quench.settings.json

The first run adopts your tables — SchemaSmith stamps them as managed and stands up its own bookkeeping in a separate SchemaSmith schema, well clear of your dbo tables. Run it again and nothing happens. No adds, no alters, no drops. A clean no-op is the proof: the package is a faithful cast of the live database, and you’re declarative now.

__EFMigrationsHistory is still sitting there, untouched — an inert table you can drop whenever you like. You traded a migrations folder, a snapshot, and a history table for one set of files.

What about the snapshot and HasData seeding?

Section titled “What about the snapshot and HasData seeding?”
  • The model snapshotModelSnapshot.cs is EF’s generated picture of the current model, the thing it diffs against to write the next migration. SchemaSmith doesn’t need one, because it diffs the declared files against the live database directly. The files play the role the snapshot played, except you read and edit them.

  • HasData seeding — seed rows you baked into the model so migrations would insert them. That’s data, not structure, and it ships separately in SchemaSmith. You add a DataDelivery block to the table file pointing at a content file, and quench merges those rows in:

    // in the table file:
    "DataDelivery": {
    "ContentFile": "data/dbo.OrderStatus.tabledata",
    "MergeType": "Insert/Update",
    "MatchColumns": "Code"
    }

    MergeType: Insert/Update upserts on the MatchColumns key — so re-running converges instead of duplicating, the same idempotent behavior you get for structure. No INSERT baked into a migration, no foreign-key ordering to babysit (DataTongs delivers in dependency order). Course 2 covers data delivery in depth.

The mechanism is identical on all three engines — whitelist the four tables, extract, quench to a no-op. Only the dialect’s names differ.

SQL ServerPostgreSQLMySQL
ObjectList formdbo.Customer,…public.customer,… (folded lowercase)Customer,… (schema = database)
EF providerSQL ServerNpgsqlPomelo
Tracker left behind__EFMigrationsHistory__EFMigrationsHistory__EFMigrationsHistory
Extract result4 tables, no history4 tables, no history4 tables, no history
Check yourself: EF Core keeps the schema in three places — migrations, the model snapshot, and __EFMigrationsHistory. Where does SchemaSmith keep it?

In one place: the declarative table files. They’re the end state (replacing the migration steps), they’re what quench diffs the live database against (replacing the snapshot), and there’s no record of “what ran” because SchemaSmith doesn’t replay steps (replacing the history table). One source instead of three kept in lockstep.


EF forged your shop in code, then kept three sets of notes to remember it by — the migration steps, the snapshot, the history. But the finished metal holds its own shape. SchemaTongs reads it, casts it to files, and the notes go cold in the corner where EF left them.

Got a project leaning on HasData seeding or a tangled snapshot you’re not sure how to map? Email me at forgebarrett@schemasmith.com — I read every one.

Next up: Course 5 · Module 4 — Migrating from SSDT/DACPAC, where you’re already declarative — so the move is about deploy-time and one-toolset-many-engines, not giving up migration scripts.

Until then, may the shape you forged in code need no snapshot to remember it, and no history to vouch for it.

— Forge