src/Host/FSH.Starter.DbMigrator/README.md
One-shot console application that applies EF Core migrations across the tenant catalog and every tenant's per-module databases, then exits.
Database.MigrateAsync() at app startup is convenient but has well-known
production issues:
Industry practice in mid-to-large .NET shops is to run migrations as an explicit deployment step, with elevated DB credentials, before the runtime app starts. This project is that step.
# Default — apply pending migrations for the tenant catalog + every tenant.
dotnet run --project src/Host/FSH.Starter.DbMigrator -- apply
# Apply only to one tenant.
dotnet run --project src/Host/FSH.Starter.DbMigrator -- apply --tenant root
# Apply only the tenant catalog (no per-tenant pass).
dotnet run --project src/Host/FSH.Starter.DbMigrator -- apply --catalog-only
# Preview what would run without touching the database.
dotnet run --project src/Host/FSH.Starter.DbMigrator -- list-pending
# Apply migrations AND run idempotent seed data.
dotnet run --project src/Host/FSH.Starter.DbMigrator -- apply --seed
# Just the seed step (assumes schema is already current).
dotnet run --project src/Host/FSH.Starter.DbMigrator -- seed
# Dev only — provision the demo tenants (acme, globex) with users,
# custom roles, sample catalog, tickets, and chat. Hard-refuses outside
# Development. Idempotent: safe to re-run.
DOTNET_ENVIRONMENT=Development \
dotnet run --project src/Host/FSH.Starter.DbMigrator -- seed-demo
Exit codes: 0 on success, 1 on any failure (see logged exception).
Reads from the same appsettings.json / appsettings.{Environment}.json
as FSH.Starter.Api (both files are linked into the project so they
stay in lock-step). Override anything via environment variables:
| Variable | Notes |
|---|---|
DatabaseOptions__Provider | POSTGRESQL (only provider currently) |
DatabaseOptions__ConnectionString | Use elevated DDL credentials here |
DatabaseOptions__MigrationsAssembly | FSH.Starter.Migrations.PostgreSQL |
CachingOptions__Redis | Optional — only used by module DI graphs |
Logging__LogLevel__Default | Information is the default |
Use a Job (or pre-install/pre-upgrade Helm hook) that runs the
migrator container image, then deploy the API only after the Job
succeeds. The image is built via the PublishContainer target:
dotnet publish src/Host/FSH.Starter.DbMigrator -c Release \
/t:PublishContainer /p:ContainerRepository=fsh-db-migrator
# helm/templates/migrator-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: {{ .Release.Name }}-db-migrator
annotations:
"helm.sh/hook": pre-install,pre-upgrade
"helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
spec:
backoffLimit: 0
template:
spec:
restartPolicy: Never
containers:
- name: migrator
image: ghcr.io/your-org/fsh-db-migrator:{{ .Chart.AppVersion }}
args: ["apply"]
env:
- name: DatabaseOptions__ConnectionString
valueFrom: { secretKeyRef: { name: db-ddl, key: connection } }
- name: DatabaseOptions__Provider
value: POSTGRESQL
- name: DatabaseOptions__MigrationsAssembly
value: FSH.Starter.Migrations.PostgreSQL
Run as a step before the deploy step:
- name: Migrate database
run: |
dotnet run --project src/Host/FSH.Starter.DbMigrator -- apply
env:
DatabaseOptions__ConnectionString: ${{ secrets.DB_DDL_CONNECTION }}
DatabaseOptions__Provider: POSTGRESQL
DatabaseOptions__MigrationsAssembly: FSH.Starter.Migrations.PostgreSQL
There is no development-only auto-migration or auto-seed in the API. In every environment, the migrator is the only path that touches schema OR data. The two convenient ways to run it locally are:
Aspire: dotnet run --project src/Host/FSH.Starter.AppHost —
Aspire already chains the migrator as a WaitForCompletion
dependency of the API, so the API never starts against an
unmigrated database.
Raw: run the migrator once after pulling, before starting the
API. Add seed-demo for a populated dev environment:
# Schema only (every env)
dotnet run --project src/Host/FSH.Starter.DbMigrator -- apply
# Dev: also provision acme + globex with rich demo content
dotnet run --project src/Host/FSH.Starter.DbMigrator -- seed-demo
# Then start the API
dotnet run --project src/Host/FSH.Starter.Api
seed-demo is the only way to get the demo tenants and their
users / catalog / tickets / chat. Fresh tenants created via
POST /api/v1/tenants come up with just a tenant admin user — no
catalogue, no demo content. This matches production behaviour.
If the API boots against a database whose schema is behind the running
build, the db:tenants-migrations health check returns Unhealthy
and GET /health/ready returns 503 Service Unavailable with the
list of pending tenants + migration names in the response body.
GET /health/live continues to return 200 OK because the process
itself is alive — so Kubernetes will not crash-loop the pod, but the
readiness probe will keep it out of rotation until DbMigrator runs.
This means: a failed (or skipped) migrator step surfaces as a clear operator-visible health-check failure rather than as cryptic EF errors per request.
ConfigureServices), with web-only concerns (CORS, OpenAPI, jobs,
mailing, SSE, realtime, OpenTelemetry, quotas, idempotency)
disabled.IHostedService so background workers don't fight
with the migrator.TenantDbContext migrations and seeds the root tenant if
missing.AppTenantInfo from the catalog and, for each, calls
ITenantService.MigrateTenantAsync (and SeedTenantAsync if
--seed is set) which walks every registered IDbInitializer
inside a scoped multi-tenant context.The per-tenant pass reuses TenantService.MigrateTenantAsync — the
exact code path the runtime app uses today — so behavior is identical
between the migrator and the API's startup pass when both are enabled.