Database migrations
:::info Status Outline — expand with a worked add-a-migration example. :::
Your configuration holds one shared EF Core model with a DbContext subclass per provider you
selected at scaffold time (--IncludeSqlite/--IncludeSqlServer/--IncludePostgres) and
separate migrations per provider under Acme.Persistence.Databases/Migrations/{Sqlite,SqlServer,PostgreSql}/,
kept in lockstep. Regenerate all of them together whenever the model changes.
kandra-migrate — the tool for this
KandraEfMigrationTool (command
kandra-migrate) wraps dotnet ef and applies a given operation uniformly across every EF
Core provider you enabled — one command instead of one dotnet ef invocation per provider.
Your scaffolded repo already ships the migrations.manifest.json it reads (resolved provider
list, project/startup-project/context names per provider — see the tool's README for the
schema) so it works out of the box once installed:
dotnet tool restore --add-source <your-nuget-feed> # once the package is reachable from your feed setup
dotnet kandra-migrate doctor # sanity-check the manifest + every enabled provider's DbContext
dotnet kandra-migrate list
dotnet kandra-migrate status # pending-model-change gate, CI-friendly
dotnet kandra-migrate add <MigrationName>
dotnet kandra-migrate remove
dotnet kandra-migrate script --output-dir artifacts/sql
dotnet kandra-migrate update --provider Sqlite --environment Development
dotnet kandra-migrate drop --provider Sqlite
Notes on the less obvious commands:
updaterequires both--providerand--environment(there's no "update all providers" — a running instance targets one real database at a time). It resolves the real per-environment connection string itself rather than trustingDOTNET_ENVIRONMENTto boot your app's host — every Kandra configuration's design-time factory hardcodes a local/dev connection string that would otherwise win regardless of environment. Production always prompts for a typed confirmation, even with--yes.drophas no--environmentat all —dotnet ef database drophas no--connectionoverride, so it can only ever touch the design-time factory's hardcoded local database, making it structurally local/dev-only. It requires--providerand typing the resolved database identifier back to confirm, unless--yes.squashisn't built yet — don't try to collapse migration history by hand either; see the tool's README for why that's deliberately being built carefully rather than rushed.
Fallback: dotnet ef directly
If kandra-migrate isn't installed/reachable yet in your setup, fall back to dotnet ef
per provider, only for the provider(s) you actually included at scaffold time:
cd src/Acme.Persistence.Databases
dotnet ef migrations add <MigrationName> --project . --startup-project ../Acme.WebApi/Acme.WebApi.csproj --context AcmeDbContextSqlite --output-dir Migrations/Sqlite
dotnet ef migrations add <MigrationName> --project . --startup-project ../Acme.WebApi/Acme.WebApi.csproj --context AcmeDbContextSqlServer --output-dir Migrations/SqlServer
dotnet ef migrations add <MigrationName> --project . --startup-project ../Acme.WebApi/Acme.WebApi.csproj --context AcmeDbContextPostgreSql --output-dir Migrations/PostgreSql
The project's own Providers csproj property is computed from which Contexts/*.cs files
exist on disk, so it always reflects reality even if you add or remove a provider by hand
later — there's no separate config to keep in sync, whichever path (tool or direct) you use.
Gotcha: the abstract DocumentBase guard
A self-healing guard in AcmeDbContext.OnModelCreating (in Acme.Persistence) unmaps the
engine's abstract DocumentBase entity until you have at least one concrete Document type —
without it, EF Core throws at migrations add time on a fresh scaffold, which has zero
Document types by definition. Don't simplify this to an unconditional Ignore<DocumentBase>()
— it's written to stop applying on its own once you add a real Document, and an unconditional
version would silently break Document Links once you have one.
See also
- How to start for the scaffolding commands that produce this project shape in the first place.