Chapter 15 · Migrations, Model Snapshots, Seeding, Scripts, Bundles, and Deployment
Migration Bundles, Runtime Migration Risks, Permissions, Locking, and Multi-Instance Deployments
Build and operate migration bundles, understand EF migration locking and SQLite lock recovery, and enforce a single coordinated migration owner in multi-instance deployments.
Learning outcomes
Build framework-dependent and self-contained EF migration bundles for explicit target runtimes.
Explain how bundles locate configuration/connection strings and why secrets should be injected at deployment rather than committed.
Explain EF Core 9+ migration locking and distinguish it from application/schema compatibility during rolling deployment.
Recognize SQLite __EFMigrationsLock behavior and recover only a verified abandoned lock.
Compare bundles, scripts, CLI tools, and runtime migration as deployment strategies.
Design a single migration-owner workflow for multi-instance ServiceHub deployments.
1. The practical problem: production should not need source code and an SDK just to change schema
A deployment host may deliberately contain only the application
artifacts and narrowly scoped operational tools. Installing the
full .NET SDK, dotnet-ef, and source code merely to
run migrations increases moving parts. A migration bundle
packages EF's migration runner into a single executable
generated during CI. A framework-dependent bundle needs a
compatible runtime on the target; a self-contained bundle
includes the runtime for a specific runtime identifier (RID).
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. Build bundles deliberately for the target topology
mkdir -p artifactsdotnet ef migrations bundle --project src/ServiceHub.EfLab --output artifacts/efbundle
dotnet ef migrations bundle --project src/ServiceHub.EfLab --self-contained --target-runtime linux-x64 --output artifacts/efbundle-linux-x64
Do not build one platform binary and assume it is portable. The RID is an explicit deployment assumption. Pin the EF tool/package line used to create the bundle and archive the build metadata with the artifact.
3. Configuration and connection selection are deployment inputs
By default a bundle uses the application's connection
configuration; Microsoft documentation warns to copy
appsettings.json alongside the bundle when that is
how the application supplies configuration. For real
credentials, prefer secret injection or a protected
--connection argument from the deployment
environment rather than committing a production password to
source or an artifact.
./artifacts/efbundle --connection "Data Source=servicehub-lab.db" --verbose
Command-line secrets can be visible to process inspection or logs depending on platform/runner. In production, use the deployment system’s supported secret mechanism and avoid echoing credentials. The lab connection string contains no secret.
4. Migration locking prevents simultaneous migration runners—not incompatible apps
EF Core 9 introduced migration locking for EF-driven migration
application: CLI database update, bundles, and
Migrate/MigrateAsync. The lock prevents multiple
migration executions from modifying the schema concurrently. It
does not make old and new application binaries
automatically compatible with every intermediate schema state.
You still need one migration owner and expand/contract
compatibility where multiple versions overlap.
| Mechanism | What it protects | What it does not protect |
|---|---|---|
| EF migration lock | Two EF migration runners applying concurrently | Old app querying a renamed/dropped column |
| DB transaction around migration work | Provider-supported atomicity of migration operations | All provider DDL/data operations in every scenario |
| Rolling deployment orchestration | Ordering of app/schema rollout | Incorrect migration code/data-loss intent |
| Backup/snapshot | Recovery point | Forward compatibility of old binaries |
5. SQLite lock implementation has a real failure mode
SQLite has no native application lock like SQL Server
sp_getapplock. EF creates
__EFMigrationsLock and inserts a row. If a
migration process is killed unexpectedly, the row/table can
remain and later migration attempts can wait indefinitely.
Recovery is an operational act: first prove no migration process
is still active and inspect the database, then clear the
abandoned lock according to provider guidance.
-- Only after verifying no migration owner is active:DROP TABLE "__EFMigrationsLock";-- Provider documentation also permits deleting rows instead:-- DELETE FROM "__EFMigrationsLock";
A lock that looks “old” may still protect an active deployment. Lock cleanup must be tied to process/deployment evidence and a runbook, not a timer that deletes coordination state.
6. Deliberately wrong: every replica is allowed to be migration owner
Replica A startup -> MigrateAsync()Replica B startup -> MigrateAsync()Replica C startup -> MigrateAsync()All three runtime identities have ALTER/DROP permissions.
The EF lock serializes migration execution, but the topology is still poor: all runtime identities are over-privileged, rollouts can wait on schema work, and application/schema compatibility is uncontrolled. Repair it by making migration a distinct deployment stage/job with exactly one owner. Only after migration verification should application replicas progress according to the compatibility plan.
7. Compare deployment strategies instead of choosing by convenience
| Strategy | Strength | Important boundary |
|---|---|---|
| Reviewed SQL script | SQL approval/archive/change-control friendly | External runner; no EF migration lock; provider-specific idempotency. |
| Migration bundle | Portable deployment artifact; EF locking/seeding behavior retained | Must target correct runtime/provider/config; generated SQL not pre-reviewed unless separately scripted. |
| EF CLI database update | Excellent local/test ergonomics | Requires SDK/tool/source on execution host; not ideal production control plane. |
| Runtime MigrateAsync | Simple application code path | DDL permissions/startup coupling/compatibility risks; not the default production recommendation. |
8. Hands-on lab: build, hash, run, and verify a bundle
-
From the migration-complete teaching branch, run
has-pending-model-changes. - Build a framework-dependent bundle and record SHA-256 plus EF/dotnet-ef version.
-
Copy
appsettings.jsonif the lab factory/bundle expects it, or pass the disposable connection explicitly. -
Run the bundle against a copy of
servicehub-lab.db; record applied migration output. - Run the same bundle again; expect “already up to date”/no missing migrations.
- Inspect
__EFMigrationsHistory. - Review the SQLite lock-table runbook without manufacturing an abandoned lock on valuable data.
9. Production judgment and bridge to seeding/zero downtime
Bundles are strong automated deployment artifacts when you want EF to own migration execution without installing the SDK/source on the target. They are not a substitute for schema compatibility design, least privilege, backups, or operational ownership. Generate a separate reviewed SQL script if exact DDL review is required. The final chapter lesson combines migration ownership with deterministic data initialization and expand/contract release planning.
Check your understanding
- What does --self-contained change for a migration bundle?
- Does a bundle need source code or dotnet-ef installed on the deployment host?
- What does EF migration locking protect?
- How does SQLite implement EF migration locking?
- When is deleting __EFMigrationsLock appropriate?
- Why still prefer one migration owner?
Review the answers
1. It includes the .NET runtime for the selected target runtime/RID.
2. No; that is one of its main deployment advantages.
3. Concurrent EF-driven migration executions, not application/schema compatibility.
4. With the __EFMigrationsLock table/row.
5. Only after proving the lock is abandoned and no migration owner remains active.
6. It centralizes DDL permissions, rollout order, review, observability, and recovery decisions.
Authoritative references
Use primary documentation when regenerating or adapting these deployment steps; migration behavior is provider- and version-sensitive.