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, concept for concept
Section titled “EF Core, concept for concept”| EF Core migrations | SchemaSmith |
|---|---|
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 |
__EFMigrationsHistory | No 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.
One source, three providers
Section titled “One source, three providers”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.
Extract, and leave the history behind
Section titled “Extract, and leave the history behind”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" }cd sqlserverschematongs --ConfigFile:SchemaTongs.settings.json=== Casting Summary === Tables: 4 extracted, 0 errorsFour 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:
schemaquench --ConfigFile:quench.settings.jsonThe 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 snapshot —
ModelSnapshot.csis 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. -
HasDataseeding — 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 aDataDeliveryblock 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/Updateupserts on theMatchColumnskey — so re-running converges instead of duplicating, the same idempotent behavior you get for structure. NoINSERTbaked into a migration, no foreign-key ordering to babysit (DataTongs delivers in dependency order). Course 2 covers data delivery in depth.
Per-engine notes
Section titled “Per-engine notes”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 Server | PostgreSQL | MySQL | |
|---|---|---|---|
ObjectList form | dbo.Customer,… | public.customer,… (folded lowercase) | Customer,… (schema = database) |
| EF provider | SQL Server | Npgsql | Pomelo |
| Tracker left behind | __EFMigrationsHistory | __EFMigrationsHistory | __EFMigrationsHistory |
| Extract result | 4 tables, no history | 4 tables, no history | 4 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