While you build a feature, the model keeps changing, and you run dotnet ef migrations add every time you want to try it against your local database. By the end of the branch you have AddInvoice, AddInvoice2, FixInvoiceFk, RemoveInvoiceNotes and AddInvoiceNotesBack.
Those are drafts. Once they are merged, every developer, every test run and every environment replays them forever, and nobody can safely remove them. Squash them into one migration that describes the feature while they are still only yours.
Once a migration has been applied to a shared or production database, it is permanent. Microsoft's guidance is to never remove a migration that has been applied to a shared database - you add a corrective migration instead. The only cheap moment to clean up is before the branch is merged.
Draft migrations also cost you on every deployment:
Migrations folder should read like a changelog, one meaningful name per feature.Designer.cs file carries a full copy of the modelMigrations/20260910021544_AddCustomers.cs <- from main20260915033012_AddInvoice.cs20260915041127_AddInvoice2.cs20260916012209_FixInvoiceFk.cs20260917053318_RemoveInvoiceNotes.cs20260918002741_AddInvoiceNotesBack.csAppDbContextModelSnapshot.cs
❌ Figure: Bad example - Five draft migrations for one feature are about to be merged
Migrations/20260910021544_AddCustomers.cs <- from main20260918034502_AddInvoicing.csAppDbContextModelSnapshot.cs
✅ Figure: Good example - One migration that describes the feature's real schema change
main, if main got new migrations in the meantime. Re-scaffolding on top of main's snapshot also fixes a diverged migration treeOnly squash migrations that exist only on your branch and have only been applied to your own local database.
main or has been pulled by other developerssuppressTransaction: true SQL that must not share a transaction with the othersThe goal is one migration per logical change, not one migration at any cost.
This rule is about draft migrations on a feature branch. Replacing an entire production migration history with a single baseline is a different, riskier process - see Resetting all migrations.
mainYour branch's migrations must sit on top of main's latest migration and snapshot.
Remove them newest first. Run this once for each migration your branch added:
dotnet ef migrations remove --force
--force first rolls your local database back to the previous migration, then deletes the migration's files. When you're done, your database and your Migrations folder both match main again.
Give it a name that describes the feature:
dotnet ef migrations add AddInvoicing
Update your local database, run your tests, and confirm the model has no pending changes:
dotnet ef database updatedotnet ef migrations has-pending-model-changes
Before you start, save the SQL your draft migrations produce. After squashing, generate it again and compare. Both scripts start from main's last migration, 20260910021544_AddCustomers in this example:
dotnet ef migrations script 20260910021544_AddCustomers -o before.sql # before squashingdotnet ef migrations script 20260910021544_AddCustomers -o after.sql # after squashing
✅ Figure: Good example - Both scripts end in the same schema, after.sql just skips the intermediate steps
EF Core can't tell a renamed property from a dropped one plus a new one, so it scaffolds DropColumn and AddColumn. If your branch renamed a column that already exists on main, the fresh migration will drop it and lose every value in it.
migrationBuilder.DropColumn(name: "Notes",table: "Invoices");migrationBuilder.AddColumn<string>(name: "Comments",table: "Invoices",nullable: true);
❌ Figure: Bad example - The re-scaffolded migration drops the existing column and its data
migrationBuilder.RenameColumn(name: "Notes",table: "Invoices",newName: "Comments");
✅ Figure: Good example - The rename is put back by hand, so the data is kept
migrations remove --force rolls the database back firstDbContexts - squash each context's migrations separately with --contextTreat migrations like commits - squash the drafts before anyone else sees them.