EF Core Migrations: Why Bundles Beat Startup MigrateAsync()
This guide explains why running MigrateAsync() at application startup couples deployments to schema changes and grants the app more database permissions than it needs. It walks through EF migration bundles as a controlled CI deployment step, with GitHub Actions and local examples. It also covers when runtime migrations are acceptable and the practical limits of bundles, such as the lack of a dry run.
Key points
- For production, run schema changes from a separate EF bundle step instead of calling MigrateAsync() at app startup.
- Build the bundle with dotnet ef migrations bundle and choose --self-contained and --target-runtime for the actual target.
- Bundles have no dry-run option; generate SQL with dotnet ef migrations script and test it on a sanitised copy of real data.
- Promote the same tested artifact through test, staging and production, and protect the deployment job with a GitHub environment.
- For local bundles, pass the connection explicitly with ./efbundle --connection.
- A bundle is not a rollback strategy; for released migrations, add a corrective migration instead.
Calling MigrateAsync() at startup ties every application boot to a schema change and gives the app DDL rights it rarely needs. The author admits his personal blog does exactly that, but argues that multiple replicas, sensitive data or real users make it a poor fit. The recommended alternative is an EF bundle, an executable that contains the compiled migrations, built in CI and run as a separate deployment step with its own credentials.
Bundles record applied migrations, surface failures in pipeline logs and let the same tested artifact move through test, staging and production. The article is clear that a bundle has no dry-run option, so SQL should be generated with migrations script and checked against a sanitised copy of real data. It also covers SQL script runners, DACPAC, local test workflows and build-time caching for anyone adopting this pattern.
Real-world use
- A multi-replica ASP.NET Core API with real user data that should move schema changes into a separate CI/CD deployment step.
- A regulated project where a DBA must review generated SQL before any change reaches production.
- A team that keeps MigrateAsync() for local development but ships a tested bundle to production.
Why it matters
Moving schema changes out of app startup reduces both outage risk and the database permissions your runtime app needs. Since bundles have no dry run, teams must test generated SQL against realistic data before a production rollout.