Chapter 15 · Migrations, Model Snapshots, Seeding, Scripts, Bundles, and Deployment
Create, Review, Rename, Reorder, and Repair Migrations Without Losing Data
Repair unsafe migration scaffolds deliberately: preserve data through renames and data moves, remove only unapplied work, and correct already-deployed schema changes with new migrations.
Learning outcomes
Recognize the destructive drop/add migration commonly scaffolded when EF cannot infer a rename.
Replace unsafe rename scaffolds with explicit RenameColumn/RenameTable operations where the provider supports them.
Use expand-copy-contract steps when an in-place rename cannot satisfy compatibility or data-transformation requirements.
Remove only unapplied migrations safely and handle already-applied migrations with new corrective history.
Reason about migration ordering and snapshot conflicts across branches.
Verify preserved data on the disposable SQLite lab before accepting a repaired migration.
1. The practical problem: a harmless C# rename becomes a data-loss migration
Continue the teaching branch from Lesson 1. ServiceHub now wants
DispatchCode renamed to RoutingCode. A
developer changes the property and its column mapping from
dispatch_code to routing_code,
scaffolds a migration, sees green “Build succeeded,” and assumes
the data will be preserved. EF cannot reliably infer intent from
every remove/add pair. A scaffold may represent the change as
dropping one column and adding another, which can erase existing
values.
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.
2. Diagnose the scaffold, not the name of the migration
migrationBuilder.DropColumn( name: "dispatch_code", table: "work_orders");migrationBuilder.AddColumn<string>( name: "routing_code", table: "work_orders", type: "TEXT", maxLength: 32, nullable: true);
The migration name RenameDispatchCode has no
semantic power. Review operations. If rows contain dispatch
values, drop/add loses them. EF documentation explicitly warns
that generated migrations must be reviewed and shows rename
scenarios as a classic case where manual correction is
necessary.
Treat DropColumn, DropTable,
narrowing type/length changes, non-nullable conversions, and
destructive custom SQL as review blockers until data impact is
proven.
3. Repair a true rename with RenameColumn
protected override void Up(MigrationBuilder migrationBuilder){ migrationBuilder.RenameColumn( name: "dispatch_code", table: "work_orders", newName: "routing_code");}protected override void Down(MigrationBuilder migrationBuilder){ migrationBuilder.RenameColumn( name: "routing_code", table: "work_orders", newName: "dispatch_code");}
SQLite’s EF provider supports RenameColumn. Other
schema operations may be rebuilt under the hood, so still
generate and inspect provider SQL. A rename operation preserves
values by expressing the semantic operation directly; it does
not make rolling deployment compatibility disappear. Old
application code that still queries
dispatch_code will fail immediately after an
in-place rename.
4. When compatibility matters, expand → copy/backfill → contract
For a rolling deployment, avoid forcing old and new binaries to
agree on the same instant rename. Add
routing_code first while keeping
dispatch_code; backfill data; deploy code that can
tolerate or dual-write both fields; later make the new path
authoritative; only then drop the old column in a separate
migration after the old application version is gone.
migrationBuilder.AddColumn<string>( name: "routing_code", table: "work_orders", type: "TEXT", maxLength: 32, nullable: true);migrationBuilder.Sql( "UPDATE work_orders SET routing_code = dispatch_code " + "WHERE routing_code IS NULL;");
That SQL is intentionally SQLite-compatible and idempotent for the shown one-time backfill predicate, but the overall migration itself is still applied once via migration history. For large tables, backfill size, locks, transaction log/WAL growth, and deployment timing require production-specific design; do not blindly copy the one-statement lab into a high-volume system.
5. Remove versus repair: was the migration already applied?
| State | Safe action | Why |
|---|---|---|
| Latest migration scaffolded but not applied anywhere shared |
dotnet ef migrations remove after review
|
Tool rewinds migration code and snapshot together. |
| Applied only to disposable local DB | Update DB to previous migration, then remove | Keeps local DB and source history aligned. |
| Applied to shared/test/production DB | Keep it; add a new corrective migration | Deleting/editing deployed history breaks reproducibility and downstream assumptions. |
| Older migration in middle of unpublished stack | Remove later migrations in reverse order and re-scaffold | Do not hand-delete middle files and hand-edit the snapshot. |
dotnet ef database update PreviousMigration --project src/ServiceHub.EfLab --connection "Data Source=servicehub-lab.db"dotnet ef migrations remove --project src/ServiceHub.EfLab
EF Core 10 also supports migrations remove --force,
but that couples database rollback and source removal. In a
teaching lab the explicit two-step form is easier to reason
about. Never use force as a substitute for checking where a
migration has already been deployed.
6. Branches and ordering: migration history is a sequence
Two feature branches can each scaffold from the same snapshot. When merged, both migration files may exist but the final snapshot can only represent one coherent resulting model. Review migration timestamps/order, merge model configuration first, resolve snapshot conflicts carefully, and re-scaffold unpublished migrations when necessary. Do not reorder already-deployed migrations merely to make Git history prettier.
dotnet ef migrations list --project src/ServiceHub.EfLabdotnet ef migrations has-pending-model-changes --project src/ServiceHub.EfLab
The ordered list tells you which code artifacts exist. The pending-model check tells you whether the latest snapshot matches the current model. Neither replaces deployment-state verification on each target database.
7. Custom SQL and data moves require reversibility judgment
migrationBuilder.Sql() is an escape hatch for data
transforms and provider-specific DDL. Parameterization is not
the normal runtime LINQ story here; migration SQL is authored
source code and must be reviewed like privileged deployment
code. A data transformation may not have a meaningful inverse.
If Down() cannot reconstruct discarded information,
make that operationally explicit instead of inventing a fake
reverse transform.
protected override void Down(MigrationBuilder migrationBuilder){ // Schema can be restored, but deleted/normalized business data cannot // be reconstructed from this migration alone. Restore from a verified // backup/snapshot or use a documented compensating data procedure. migrationBuilder.RenameColumn( name: "routing_code", table: "work_orders", newName: "dispatch_code");}
8. Hands-on lab: prove values survive the repair
-
Start from a disposable DB with the Lesson 1
dispatch_codemigration applied. -
Insert a recognizable value such as
NW-ROUTE-01. -
Rename the model mapping to
routing_codeand scaffoldRenameDispatchToRouting. -
If EF produced drop/add, replace those operations with
RenameColumn. - Generate SQL and verify no destructive drop/add pair remains for this rename.
-
Apply the migration, query the row, and confirm
NW-ROUTE-01survived. - For the expand/contract variant, use a fresh disposable DB and verify both columns coexist during the transition.
SELECT work_order_id, routing_codeFROM work_ordersWHERE routing_code = 'NW-ROUTE-01';SELECT "MigrationId"FROM "__EFMigrationsHistory"ORDER BY "MigrationId";
9. Production judgment and bridge to deployment artifacts
Migration scaffolding is a starting proposal. Schema renames, transforms, constraints, and large-table changes demand human review against real data volume, indexes, application-version compatibility, and recovery objectives. Never rewrite an applied migration casually. Preserve history and move forward with a corrective migration. The next lesson turns reviewed migration code into deployment artifacts with explicit source/target bounds and provider-aware script behavior.
Check your understanding
- Why can a property rename scaffold as drop/add?
- What operation preserves data for a true supported column rename?
- Why might expand/contract be safer than an in-place rename?
- Should an applied production migration be deleted after discovering a mistake?
- What is the snapshot risk when branches scaffold independently?
- Does a Down method guarantee business-data recovery?
Review the answers
1. EF cannot always infer developer intent from model differences; remove/add can look like a replacement.
2. RenameColumn, after provider/version verification.
3. It allows old and new application versions to coexist during rolling deployment.
4. Normally no; keep history and create a corrective migration.
5. Each branch may derive from the same old snapshot, so merge order/configuration can make the final sequence incoherent.
6. No. Some transforms are irreversible without a separate backup or compensating procedure.
Authoritative references
Use primary documentation when regenerating or adapting these deployment steps; migration behavior is provider- and version-sensitive.