Chapter 15 · Migrations, Model Snapshots, Seeding, Scripts, Bundles, and Deployment
Deterministic Data Seeding, Environment-Specific Data, Rollback Strategy, and Zero-Downtime Thinking
Seed deterministic reference data with EF Core 10 mechanisms, separate environment data, and design rollback/expand-contract deployments that keep old and new application versions compatible.
Learning outcomes
Choose between UseSeeding/UseAsyncSeeding, HasData/model-managed data, custom initialization, and manual migration data operations.
Use deterministic identifiers and idempotent checks for reference data without embedding environment secrets or operational tenant data in model snapshots.
Explain how EF migration locking interacts with UseSeeding/UseAsyncSeeding.
Design forward-fix/restore rollback plans instead of assuming Down migrations recover business data.
Apply expand/contract sequencing so old and new application versions can coexist during rolling deployment.
Assemble a Chapter 15 deployment checklist covering model, migration, artifact, permissions, locking, seed state, verification, and recovery.
1. The practical problem: schema is correct, but the application still cannot start safely
ServiceHub may need stable reference rows—work-order priorities, system-owned tag definitions, or migration-era feature metadata—before application traffic uses a new feature. That is different from production tenant/customer data, secrets, demo fixtures, or large operational imports. “Seeding” is not one mechanism; EF Core exposes several choices with different snapshot, deployment, locking, and environment behavior.
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. Four initialization mechanisms serve different ownership models
| Mechanism | Good fit | Important boundary |
|---|---|---|
UseSeeding / UseAsyncSeeding
|
Imperative initialization tied to EF database initialization/migration flow | EF9+; run under migration locking; implement both sync and async counterparts. |
HasData model-managed data |
Small deterministic reference rows whose values belong in model/migrations | Changes become migration operations; deterministic keys required; poor fit for secrets/large mutable data. |
Manual migration operations / Sql |
Schema-coupled one-time transforms/backfills | Privileged deployment code; provider/data-volume sensitive. |
| Application/import workflow | Environment/tenant/operational data with independent lifecycle | Do not make every app startup rewrite environment data. |
EF documentation recommends UseSeeding/UseAsyncSeeding
as the general seeding location for EF-managed initialization.
Tooling invokes the synchronous seeding path in some flows, so
implementing only the async delegate is an avoidable operational
surprise.
3. Deterministic reference data: stable keys, no secrets
Suppose ServiceHub needs system priority definitions. Use stable
keys that do not change between environments. Do not use
Guid.NewGuid() inside HasData or other
model-building paths; that makes every model build appear
different and can trigger pending-model/migration churn.
public sealed class WorkOrderPriority{ public int Id { get; init; } public required string Code { get; init; } public required string DisplayName { get; init; }}// In OnModelCreating / IEntityTypeConfiguration:modelBuilder.Entity<WorkOrderPriority>(builder =>{ builder.ToTable("work_order_priorities"); builder.HasKey(x => x.Id); builder.HasIndex(x => x.Code).IsUnique(); builder.Property(x => x.Code).HasMaxLength(24); builder.Property(x => x.DisplayName).HasMaxLength(80);});
builder.HasData( new WorkOrderPriority { Id = 1, Code = "normal", DisplayName = "Normal" }, new WorkOrderPriority { Id = 2, Code = "urgent", DisplayName = "Urgent" });
Production endpoints, tenant/customer rows, API keys, connection strings, rotating credentials, and large operational imports do not belong in model snapshots/migration source. Use environment-specific provisioning or application workflows with their own authorization/audit model.
4. UseSeeding/UseAsyncSeeding for imperative, idempotent initialization
optionsBuilder .UseSqlite(connectionString) .UseSeeding((context, _) => { if (!context.Set<WorkOrderPriority>().Any(x => x.Code == "normal")) { context.Set<WorkOrderPriority>().Add( new WorkOrderPriority { Id = 1, Code = "normal", DisplayName = "Normal" }); context.SaveChanges(); } }) .UseAsyncSeeding(async (context, _, ct) => { if (!await context.Set<WorkOrderPriority>() .AnyAsync(x => x.Code == "normal", ct)) { context.Set<WorkOrderPriority>().Add( new WorkOrderPriority { Id = 1, Code = "normal", DisplayName = "Normal" }); await context.SaveChangesAsync(ct); } });
The predicate is deliberately idempotent for the small lab. For multiple reference rows, use unique constraints and a robust upsert/provisioning strategy appropriate to the provider. Seeding is executed as part of EF initialization/migration operations and is protected by the migration locking mechanism; it should still be observable and bounded.
5. Deliberately wrong: random model data and production secrets in HasData
builder.HasData(new IntegrationEndpoint{ Id = Guid.NewGuid(), // model changes on every build Name = "production-broker", ApiKey = "real-secret-here" // secret committed into migration/snapshot});
This can create perpetual pending model changes and leak sensitive values into source control/migration history. Repair it by keeping the EF model deterministic and moving secrets/environment-specific resources to the deployment platform’s secret/configuration system.
6. Rollback is a business recovery plan, not “run Down and hope”
A deployment can fail because of code bugs, data incompatibility, long-running backfills, or a migration defect. Decide recovery before release:
| Failure type | Preferred recovery question | Why Down alone may fail |
|---|---|---|
| Pure additive schema + app bug | Can old app run against expanded schema while new app is rolled back? | No schema rollback may be needed. |
| Destructive migration | Is there a verified backup/snapshot and restore time objective? | Dropped/transformed data may not be reconstructable. |
| Bad backfill | Can a corrective forward migration identify/repair affected rows? | Down may not know original values. |
| Partially external side effect | What compensation/reconciliation runbook exists? | Database migration rollback cannot undo external systems. |
For many production incidents, forward-fix or application rollback against a backward-compatible expanded schema is safer than immediately reversing DDL.
7. Expand/contract: make zero-downtime compatibility a sequence
Release N: EXPAND 1. Add nullable routing_code; keep dispatch_code. 2. Backfill existing rows in a measured deployment step. 3. Deploy app version that can read old/new and writes the compatibility plan.Release N+1: SWITCH 4. Make routing_code authoritative after all old writers are gone. 5. Observe missing/invalid routing_code metrics and repair stragglers.Release N+2: CONTRACT 6. Remove legacy dispatch_code reads/writes. 7. Only then drop dispatch_code in a separately reviewed migration.
Zero downtime is not guaranteed by EF. It emerges from application/schema compatibility, provider lock characteristics, data-migration duration, deployment topology, and observability. Large backfills may need chunking or a separate operational job rather than one giant migration transaction.
8. Chapter capstone lab: one deployable migration package
-
Run
dotnet ef migrations has-pending-model-changes; it must be clean after intended migrations are scaffolded. - List migrations and record the intended source/target migration.
-
For SQLite, generate a bounded reviewed script; do not claim
--idempotentsupport. - Build a migration bundle and record its SHA-256, target RID if self-contained, EF/tool/runtime versions, and config assumptions.
- Apply to a copy of the disposable DB with the deployment identity/path.
-
Verify
__EFMigrationsHistory, expected columns/indexes, deterministic seed rows, and smoke queries. - Run the deployment a second time and verify no duplicate seed/reference rows appear.
- Document rollback/restore and abandoned SQLite migration-lock recovery procedures.
dotnet ef migrations has-pending-model-changes --project src/ServiceHub.EfLabdotnet ef migrations list --project src/ServiceHub.EfLabdotnet ef migrations script PreviousMigration LatestMigration --project src/ServiceHub.EfLab --output artifacts/servicehub-bounded.sqldotnet ef migrations bundle --project src/ServiceHub.EfLab --output artifacts/efbundle
9. Production judgment and bridge to reverse engineering
A mature migration workflow has an owner, source-controlled
migration code/snapshot, provider-specific review, CI
pending-model gate, deployment artifact, restricted DDL
identity, migration-state verification, locking/runbook
awareness, deterministic reference-data strategy,
backward-compatible rollout plan, and tested recovery. Never
equate a successful database update with a safe
release. Chapter 16 now reverses the direction: rather than
evolving a model-first schema, it will inspect existing
databases and scaffold EF models from them while preserving
sustainable ownership.
Check your understanding
- What EF Core seeding APIs should usually be implemented together?
- Why are random keys dangerous in model-managed HasData?
- Should production secrets be placed in HasData?
- Does Down guarantee recovery from a destructive migration?
- What is the purpose of expand/contract?
- What proves a migration package is ready?
Review the answers
1. UseSeeding and UseAsyncSeeding, because tooling/runtime paths can invoke different forms.
2. They make the model appear different between builds and can generate perpetual migration changes.
3. No. They become source/migration artifacts; use a secret/provisioning system.
4. No. Restore, forward-fix, or compensating data procedures may be required.
5. Keep old/new app versions compatible with an intermediate schema during staged/rolling deployment.
6. Clean pending-model check, reviewed artifact, known source/target, provider/version metadata, deployment permissions, seed verification, smoke tests, and a recovery/runbook plan.
Authoritative references
Use primary documentation when regenerating or adapting these deployment steps; migration behavior is provider- and version-sensitive.