Skip to main content

Command Palette

Search for a command to run...

EF Core Migration Checklist for Production .NET Teams

Updated
โ€ข11 min read
EF Core Migration Checklist for Production .NET Teams

Applying EF Core migrations to a production database is one of the highest-risk operations your team performs. Unlike a code deployment, a schema change is hard to undo, can block active transactions, and โ€” if applied incorrectly โ€” can corrupt data or bring your API down mid-request. Most teams treat migrations as an afterthought: generate the file, run dotnet ef database update, and hope for the best. That approach works until it doesn't.

The complete implementation โ€” including the Outbox-aware migration workflow, idempotent script generation, and CI/CD integration โ€” is available on Patreon with annotated, production-ready source code that shows how enterprise teams handle this safely.

Chapter 3 of the Zero to Production course covers EF Core migrations inside a complete production codebase โ€” with soft deletes, audit fields, Fluent API configuration, and the startup migration strategy wired together so the context is always clear.

ASP.NET Core Web API: Zero to Production

This checklist distils what the best .NET teams run through before any schema change touches production. Work through it sequentially โ€” each item builds on the previous one, and skipping steps is how incidents happen.

Before You Write the Migration

1. Confirm Your Migration Is Scoped to One Change

A migration that adds a column, removes an index, and renames a table is three operations pretending to be one. If it fails halfway through, your rollback story gets complicated fast.

The check: Does your migration do exactly one logical thing? If not, split it.

Teams that rush this step under deadline pressure are the same teams writing post-mortems at 2 AM. One migration, one purpose.

2. Review the Generated SQL Before Committing Anything

EF Core generates the SQL for you, but it does not make decisions for you. Always inspect the generated script using:

dotnet ef migrations script --idempotent --output migration.sql

Look for DROP operations, column type changes with implicit data truncation, and index drops that could change query plans. EF Core 10 is good โ€” it is not perfect.

The check: Have you read every line of the SQL output? If the answer is "it looked fine at a glance", that is not a yes.

3. Verify Backward Compatibility with the Running Application

During a rolling deployment or blue/green cutover, both the old and new version of your application will run simultaneously against the same database. A migration that removes a column the old code still reads will break the in-flight old instances immediately.

The check: Does the schema after migration still support the previous application version? If not, you need a two-phase migration (expand first, then contract after full cutover).

This is the most commonly skipped check on this list, and the most commonly regretted one.

On the Database Side

4. Take a Full Database Backup Immediately Before Applying

This is non-negotiable. Backups taken by your cloud provider on a schedule are not a substitute for a point-in-time backup taken immediately before the migration window.

The check: Is there a backup from the last 15 minutes? Do you know the exact restore procedure and how long it takes? If you cannot answer the second question, the backup is incomplete protection.

5. Test the Migration on a Production-Clone Staging Environment

Not a dev database. Not a shared integration database. A production-clone: same data volume, same indexes, same SQL Server or PostgreSQL version.

The reason is simple โ€” a migration that adds a non-nullable column with no default will fail on a table with existing rows. On an empty dev database, it succeeds. On a staging database with 50 million rows, it fails with a constraint violation after a 10-minute lock.

The check: Has this exact migration script been applied successfully to a staging environment that mirrors production in data volume and structure?

6. Estimate Lock Duration and Plan a Maintenance Window If Needed

Some operations โ€” adding a non-nullable column without a default, rebuilding a clustered index, changing a column type โ€” require an exclusive table lock for the duration of the operation. On a large table under active traffic, that lock duration can exceed the acceptable window for your SLA.

The check: For each ALTER TABLE in the migration SQL, do you know the expected lock type and duration under production load? For anything that requires more than a few seconds, have you planned a maintenance window or explored online index rebuild options?

Microsoft's documentation on online index operations is the authoritative reference for SQL Server-specific lock behaviour.

7. Verify Idempotency of the Script

An idempotent migration script is one that can be run twice without causing errors. This matters because CI/CD pipelines sometimes re-run migrations on retry, and multi-instance environments can trigger simultaneous migration attempts.

The --idempotent flag in dotnet ef migrations script generates scripts with IF NOT EXISTS guards. Always use it for production.

The check: Does the SQL script use idempotent guards (IF NOT EXISTS, IF OBJECT_ID IS NULL, etc.) so re-running it does not fail or produce duplicate data?

During Deployment

8. Apply the Migration via CI/CD, Not a Developer Workstation

Running migrations from a developer's machine against a production database is a procedural risk: wrong environment variable, wrong connection string, wrong version. The migration must come from a reproducible, auditable pipeline step.

The check: Is the migration applied as a dedicated step in your GitHub Actions or Azure DevOps pipeline, using the same connection string that your application uses โ€” not a personal development connection?

A well-structured pipeline applies the migration before deploying new application code. If the migration fails, the deployment stops. The application continues running against the old schema with the old code โ€” no outage.

9. Never Use MigrateAsync() in Production Application Startup

context.Database.MigrateAsync() at application startup is a popular pattern in tutorials and completely wrong for production. Here is why:

  • In a multi-instance deployment, every instance races to apply pending migrations simultaneously

  • If migration fails, the application starts in a partially migrated state with no easy way to observe which instance applied what

  • Startup time increases by the duration of the migration, which can trigger health check failures and cascade restarts

The correct pattern is to apply migrations as a dedicated pre-deployment job โ€” isolated, sequential, observable, and separate from the application process.

The check: Is there any MigrateAsync() call in your Program.cs or startup code? If yes, remove it before this migration and before the next one.

10. Monitor Active Connections and Long-Running Transactions Before Applying

Applying a migration while a long-running transaction holds locks on the target tables will cause the migration to block โ€” or the migration will cause the long-running transaction to fail. Either outcome is bad.

The check: Before applying the migration, check for active connections, open transactions, and any long-running queries against the tables being modified. On SQL Server, sys.dm_exec_requests and sys.dm_exec_sessions are your tools. On PostgreSQL, pg_stat_activity shows you what to kill.

After the Migration

11. Validate the Schema and Spot-Check Production Data

After a migration applies, EF Core reports success โ€” but it is reporting on the mechanics of the SQL execution, not the correctness of your data. A column rename that migrates existing values is worth verifying independently.

The check: Run a targeted query against the modified tables immediately after migration. Verify row counts match expectations, nullable columns contain the expected nulls vs values, and foreign key constraints are intact.

One well-written smoke query run manually or as an automated post-migration step catches a silent data issue before your users do.

12. Document the Migration in Your Change Log and Update the Rollback Runbook

Every production migration should be documented: what changed, why, when it was applied, and how to roll it back. The rollback script is not the EF Core Down() method โ€” it is a tested SQL script that has been reviewed the same way the Up() script was.

The check: Is there a documented rollback procedure for this migration, and has that rollback script been tested on staging? If the answer is "we will figure it out if we need to", the migration is not production-ready.

What Does a Safe EF Core Migration Run Actually Look Like?

The pattern that enterprise .NET teams converge on looks like this: migration SQL is generated in CI, reviewed as part of the pull request, applied by a dedicated migration job as the first step of the deployment pipeline, and validated by a smoke-test query before the new application instances roll out. The application startup code has no migration calls. The developer who wrote the migration did not apply it โ€” the pipeline did.

It is boring. It is repeatable. It is how you avoid a 2 AM rollback call.

For internal tooling on this topic, the EF Core documentation on applying migrations covers the SQL script generation approach in detail, including idempotent script options.

For teams already using Clean Architecture with CQRS, the Infrastructure layer is the right place to manage migration tooling, keeping it isolated from domain logic and application startup. The Coding Droplets post on Clean Architecture and CQRS with MediatR walks through that layer structure in depth.

FAQ

What is the safest way to apply EF Core migrations in production?

Generate an idempotent SQL script using dotnet ef migrations script --idempotent, review it in a pull request, test it on a staging environment with production-scale data, and apply it as a dedicated pre-deployment CI/CD pipeline step โ€” never via MigrateAsync() at application startup.

Should I use MigrateAsync() at application startup in ASP.NET Core?

No. MigrateAsync() at startup creates race conditions in multi-instance deployments, increases startup latency, and makes it impossible to separate migration failures from application failures. Use a dedicated migration job in your deployment pipeline instead.

How do I write a zero-downtime EF Core migration?

Follow the expand-contract pattern: in the first deployment, add the new column (nullable, backward-compatible). After all instances have deployed, fill data and add constraints in a second migration. Remove the old column in a third migration, after the old code is fully retired. Never rename or drop a column in a single migration that runs during active traffic.

How do I generate an idempotent EF Core migration script?

Run dotnet ef migrations script --idempotent from the project directory containing your DbContext. This generates a SQL script with conditional guards (IF NOT EXISTS) so the script can be re-run without causing errors โ€” essential for CI/CD pipelines that may retry on failure.

What should I do if an EF Core migration fails in production?

Stop the deployment immediately. Do not retry automatically. Verify whether the migration applied partially or not at all by checking the __EFMigrationsHistory table. Restore from the pre-migration backup if data corruption is detected. Apply the tested rollback script. Investigate the root cause on staging before re-attempting.

How do I roll back an EF Core migration in production?

The Down() method in a migration class is for development use only โ€” not for production rollbacks. For production, maintain a tested rollback SQL script that reverses the schema change. Review and test this script alongside the migration itself. Never rely on EF Core's built-in rollback under production load.

Is it safe to run EF Core migrations in a multi-instance Kubernetes deployment?

Not without coordination. Multiple instances racing to apply the same migration simultaneously can cause conflicts or duplicate operations. The recommended approach is to run migrations as a Kubernetes Job (not an initContainer tied to each pod) that completes before the application Deployment rolls out. This ensures exactly-once execution before new pods start.

What is the difference between --idempotent and a regular migration script in EF Core?

A regular migration script fails if applied to a database that already has some of the migrations it contains. An idempotent script checks for each migration's presence in __EFMigrationsHistory before executing it, so it can be safely re-run without errors. Always use --idempotent for production deployments.

More from this blog

C

Coding Droplets

238 posts

Coding Droplets is your go-to resource for .NET and ASP.NET Core development. Whether you're just starting out or building production systems, you'll find practical guides, real-world patterns, and clear explanations that actually make sense.

From beginner-friendly tutorials to advanced architecture decisions. We publish fresh .NET content every day to help you grow at every stage of your career.