Skip to main content

Finished reading? Get articles like this every Tuesday

Running Migrations in EF Core 10 - 5 Ways Compared

Learn 5 ways to apply EF Core 10 migrations: CLI, Migrate(), SQL scripts, migration bundles, and EnsureCreated. Includes a decision matrix and production checklist.

dotnet webapi-course

efcore migrations database-migrations ef-core-10 dotnet-10 dotnet-ef migration-bundles sql-scripts ensurecreated postgresql ci-cd production-deployment web-api aspnetcore dotnet-webapi-zero-to-hero-course devops docker

19 min read
2.2K views

Running dotnet ef database update on your dev machine works perfectly. You add a migration, update the database, and everything just works. Then you try to deploy to staging, and suddenly you’re staring at a failed deployment, a half-migrated database, and a Slack channel full of panicking teammates.

The problem isn’t the migration itself - it’s how you’re applying it. EF Core (Entity Framework Core) gives you at least five different ways to run migrations, and picking the wrong one for your environment is one of the most common mistakes I see .NET developers make. I’ve personally dealt with a production incident where a team used EnsureCreated() instead of Migrate() and lost their entire migration history - they couldn’t apply new schema changes without manually reconstructing everything.

In this article, I’ll walk you through every approach for applying EF Core 10 migrations - from the CLI tool you already know to migration bundles designed for CI/CD pipelines. I’ll give you a clear decision matrix so you know exactly which strategy fits your team, and a production checklist you can use before every deployment. Let’s get into it.

TL;DR

For production CI/CD pipelines, use migration bundles. For local development, use the dotnet ef CLI. For regulated environments, generate idempotent SQL scripts for DBA review. Use Database.Migrate() only for single-instance apps. Never use EnsureCreated() outside of throwaway test databases - it bypasses migration history and traps you the moment your schema needs to evolve.

What Are EF Core Migrations?

EF Core migrations are a version control system for your database schema. When you change your C# entity classes or Fluent API configuration, EF Core generates migration files that describe the exact SQL changes needed to bring the database in sync with your code model. Each migration is a point-in-time snapshot of your schema changes - and they’re applied in order, just like Git commits.

EF Core 10 (Entity Framework Core 10) tracks which migrations have been applied in a special __EFMigrationsHistory table in your database. When you run migrations, EF Core checks this table, determines which migrations are pending, and applies only the ones that haven’t been run yet. This makes migrations idempotent by design - you can safely run the migration command multiple times without breaking anything.

The critical question isn’t what migrations are - it’s how you apply them across different environments. Development, staging, and production each have different constraints, and using the wrong approach can lead to downtime, data loss, or deployment failures.

Prerequisites

This article builds on the Movie API I created in the CRUD with EF Core tutorial. If you’ve been following the course, you already have the Movie entity, MovieDbContext, and a working migration setup. If not, grab the source code from the repo linked above. You’ll need:

  • .NET 10 SDK - Install from here
  • Docker Desktop - Install from here (for PostgreSQL)
  • EF Core CLI tools - install with dotnet tool install --global dotnet-ef
  • The existing Movie API project with EF Core 10 and PostgreSQL configured

Five Ways to Apply EF Core Migrations

Before diving into implementation, here’s the landscape. EF Core gives you five distinct approaches, and each one suits a different scenario:

ApproachWhen It RunsRequires CLI Tools?Best ForRisk Level
dotnet ef database updateDeveloper runs manuallyYesLocal developmentLow
Database.Migrate()App startupNoSimple apps, single-instanceMedium
SQL scriptsDBA/DevOps runs manuallyGenerated with CLIRegulated environmentsLow
Migration bundlesCI/CD pipelineGenerated with CLIAutomated deploymentsLow
EnsureCreated()App startupNoPrototyping onlyHigh

Let’s break each one down with real code.

Approach 1: CLI with dotnet ef database update

This is the approach you already know from the CRUD tutorial. The EF Core CLI tool is the most straightforward way to manage migrations during development.

Core Commands

Here are the essential migration commands you’ll use daily:

Terminal window
# Add a new migration
dotnet ef migrations add AddMovieRating --project MovieApi.Api
# Apply all pending migrations to the database
dotnet ef database update --project MovieApi.Api
# Remove the last migration (only if NOT yet applied)
dotnet ef migrations remove --project MovieApi.Api
# List all migrations and their applied status
dotnet ef migrations list --project MovieApi.Api

How It Works Under the Hood

When you run dotnet ef database update, here’s what happens:

  1. EF Core builds your DbContext model from your C# code
  2. It connects to the database and reads the __EFMigrationsHistory table
  3. It compares applied migrations against available migration files
  4. It executes the Up() method of each pending migration in order
  5. After each migration succeeds, it inserts a row into __EFMigrationsHistory

If any migration fails, EF Core stops immediately - it won’t skip the failed migration and continue with the next one. This is important because migrations often depend on each other.

Applying to a Specific Migration

You can target a specific migration instead of applying all pending ones:

Terminal window
# Apply up to (and including) a specific migration
dotnet ef database update AddMovieRating --project MovieApi.Api
# Rollback to a specific migration (reverts everything after it)
dotnet ef database update AddMovieGenre --project MovieApi.Api
# Rollback ALL migrations (empty database, schema intact)
dotnet ef database update 0 --project MovieApi.Api

Important: Rolling back a migration runs the Down() method, which reverses the schema changes. If you dropped a column in the Up() method, the Down() method recreates it - but the data in that column is gone forever. Always back up before rolling back in production.

Specifying a Connection String

By default, the CLI reads the connection string from your app’s configuration. But you can override it for different environments:

Terminal window
dotnet ef database update --project MovieApi.Api --connection "Host=staging-db;Database=movies;Username=admin;Password=secret"

When to Use This

The CLI approach is perfect for local development. You’re running it manually, you can see the output, and you can fix issues immediately. But it’s not suitable for production - you don’t want to SSH into a production server and run CLI commands manually. That’s where the next four approaches come in.

Approach 2: Database.Migrate() at Application Startup

This approach applies pending migrations automatically when your application starts. It’s the most convenient option - deploy your app, and the database updates itself.

Implementation

Add Database.Migrate() (or its async counterpart) to your Program.cs:

var app = builder.Build();
// Apply pending migrations on startup
using (var scope = app.Services.CreateScope())
{
var dbContext = scope.ServiceProvider.GetRequiredService<MovieDbContext>();
dbContext.Database.Migrate();
}
app.MapOpenApi();
app.MapScalarApiReference();
// ... rest of the pipeline

Or with the async version (preferred for web applications):

var app = builder.Build();
// Apply pending migrations on startup (async)
await using (var scope = app.Services.CreateAsyncScope())
{
var dbContext = scope.ServiceProvider.GetRequiredService<MovieDbContext>();
await dbContext.Database.MigrateAsync();
}
app.MapOpenApi();
app.MapScalarApiReference();

How It Works

Database.Migrate() does the same thing as dotnet ef database update - it checks __EFMigrationsHistory, finds pending migrations, and applies them. The difference is it runs inside your application process instead of the CLI.

The Problem: Multiple Instances

Here’s where things get dangerous. If you’re running multiple instances of your app (load balancer, Kubernetes replicas, Azure App Service scaling), all instances call Migrate() simultaneously at startup. Two instances trying to apply the same migration at the same time can cause:

  • Deadlocks on the __EFMigrationsHistory table
  • Duplicate schema changes that fail with “table already exists” errors
  • Partial migrations that leave the database in an inconsistent state

Starting with EF Core 9, Migrate() and MigrateAsync() automatically acquire a database-wide lock to prevent concurrent migrations from racing each other (official docs on migration locking). The lock implementation is provider-specific - SQL Server and PostgreSQL handle it cleanly, while SQLite uses a lock table that can become abandoned if the process crashes mid-migration. If you’re on EF Core 8 or earlier, no built-in lock exists and concurrent calls can race.

Here’s the version-by-version behavior to keep straight:

EF Core VersionConcurrent Migrate() Behavior
EF Core 7 and earlierNo built-in lock. Multiple instances can race - first one wins, others fail with duplicate-schema or constraint errors.
EF Core 8No built-in lock. Same race conditions as EF 7. Mitigate with external coordination.
EF Core 9+Database-wide lock acquired automatically. Provider-specific implementation - SQL Server and PostgreSQL are clean, SQLite uses a lock table that can leak on crash.
EF Core 10Same as EF 9. The lock is on by default for all relational providers.

When to Use This

Database.Migrate() works well for:

  • Single-instance apps - no concurrency risk
  • Small internal tools - where deployment simplicity matters more than rigorous process
  • Development/staging environments - where occasional issues are acceptable

Avoid it for:

  • Multi-instance production deployments - use migration bundles instead
  • Regulated environments - where changes need audit trails and approval
  • Large databases - where long-running migrations would delay app startup

Approach 3: SQL Scripts

For teams that need full control and auditability over database changes, generating SQL scripts is the gold standard. Instead of letting EF Core execute migrations directly, you generate the SQL and hand it to a DBA or run it through your database change management tool.

Generating a Full Script

Generate a SQL script that covers all migrations from start to finish:

Terminal window
dotnet ef migrations script --project MovieApi.Api --output migrations.sql

This produces a single SQL file with every migration’s Up() method translated to raw SQL. But there’s a catch - this script assumes it’s running against an empty database. If your database already has some migrations applied, it will fail.

Generating an Idempotent Script

The safer option is an idempotent script - it checks __EFMigrationsHistory before each migration and skips ones that are already applied:

Terminal window
dotnet ef migrations script --idempotent --project MovieApi.Api --output migrations.sql

The generated SQL wraps each migration in a conditional check. For PostgreSQL (which our Movie API uses), it looks like this:

DO $EF$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM "__EFMigrationsHistory"
WHERE "MigrationId" = '20260305120000_AddMovieRating'
) THEN
ALTER TABLE app."Movies" ADD "Rating" double precision NOT NULL DEFAULT 0.0;
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
VALUES ('20260305120000_AddMovieRating', '10.0.0');
END IF;
END $EF$;

Pro tip: Always use --idempotent for production scripts. It’s safe to run multiple times, which means you can retry failed deployments without worrying about duplicate schema changes.

Generating a Script Between Two Migrations

You can also generate a script for a specific range:

Terminal window
# Script from AddMovieGenre to AddMovieRating (inclusive)
dotnet ef migrations script AddMovieGenre AddMovieRating --project MovieApi.Api --output patch.sql

This is useful when you need to cherry-pick specific migrations for a hotfix deployment.

When to Use This

SQL scripts are the right choice for:

  • Regulated industries (finance, healthcare) - where DBAs must review and approve every schema change
  • Teams using database change management tools - like Flyway, Liquibase, or Redgate
  • Large production databases - where you need to test the script on a copy first
  • Audit compliance - where you need to keep a record of exactly what SQL ran

The downside is manual effort - someone has to run the script, and it doesn’t integrate neatly into automated CI/CD pipelines. That’s what migration bundles solve.

Migration bundles were introduced in EF Core 6 and are the recommended approach for production deployments in CI/CD pipelines. A migration bundle is a self-contained executable that applies pending migrations - no .NET SDK, no CLI tools, no source code required on the deployment machine.

Creating a Bundle

Terminal window
dotnet ef migrations bundle --project MovieApi.Api --output efbundle --force

This produces a framework-dependent executable (efbundle on Linux/Mac, efbundle.exe on Windows) that contains:

  • All your migration files compiled in
  • The EF Core libraries needed to apply them
  • Connection string resolution logic

The bundle still requires the .NET runtime to be installed on the machine that runs it - it does not include the .NET SDK or dotnet-ef tool. If you want a fully self-contained executable that runs without a .NET runtime installed, add --self-contained and a runtime identifier:

Terminal window
dotnet ef migrations bundle --project MovieApi.Api --output efbundle --force --self-contained -r linux-x64

Use the runtime identifier that matches your deployment target (linux-x64, win-x64, linux-arm64, etc.).

Running the Bundle

Terminal window
# Use the connection string from the project's configuration
./efbundle
# Or specify a connection string explicitly
./efbundle --connection "Host=prod-db;Database=movies;Username=deploy;Password=secret"

The bundle does the same idempotent check - it reads __EFMigrationsHistory and only applies pending migrations. It’s safe to run multiple times.

Migration Bundles in a CI/CD Pipeline

Here’s a real-world GitHub Actions workflow that builds the bundle as a build artifact and applies it during deployment:

.github/workflows/deploy.yml
name: Deploy with Migrations
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-dotnet@v6
with:
dotnet-version: '10.0.x'
- run: dotnet tool install --global dotnet-ef
- run: dotnet ef migrations bundle --project MovieApi.Api --output efbundle --force
- uses: actions/upload-artifact@v7
with:
name: migration-bundle
path: efbundle
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v8
with:
name: migration-bundle
- run: chmod +x efbundle
- run: ./efbundle --connection "${{ secrets.PROD_CONNECTION_STRING }}"

Migration Bundles in Docker

If you’re deploying with Docker, you can build the bundle in a multi-stage Dockerfile:

# Build stage
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY . .
RUN dotnet tool install --global dotnet-ef
ENV PATH="$PATH:/root/.dotnet/tools"
RUN dotnet ef migrations bundle --project MovieApi.Api --output /app/efbundle --force
# Runtime stage
FROM mcr.microsoft.com/dotnet/aspnet:10.0
WORKDIR /app
COPY --from=build /app/efbundle .
COPY --from=build /src/MovieApi.Api/bin/Release/net10.0/publish .
ENTRYPOINT ["dotnet", "MovieApi.Api.dll"]

Then run the migration bundle as a separate step before starting your app:

Terminal window
docker run --rm myapp ./efbundle --connection "$PROD_CONNECTION_STRING"
docker run -d myapp

Why Bundles Are the Best Production Option

Migration bundles solve every pain point of the other approaches. Isn’t that cool?

ProblemBundle Solution
Need SDK on production serverBundle is self-contained - no SDK needed
Multiple instances running Migrate()Bundle runs once, separately from app startup
DBA wants to review SQLGenerate --idempotent script from the same migrations
CI/CD automationBundle is a build artifact - deploy it like any other binary
Connection string managementPass it as an argument or environment variable

Approach 5: EnsureCreated() - The Dangerous Shortcut

EnsureCreated() creates the database and all tables based on your current model - but it completely bypasses the migration system. No migration files, no __EFMigrationsHistory, no incremental changes.

// DON'T use this in production
using (var scope = app.Services.CreateScope())
{
var dbContext = scope.ServiceProvider.GetRequiredService<MovieDbContext>();
dbContext.Database.EnsureCreated(); // Creates tables, ignores migrations
}

Why It’s Dangerous

Here’s what happens with EnsureCreated():

  1. First run: Creates all tables based on your current model. Works great.
  2. You add a new property to Movie: EnsureCreated() sees the database already exists, does nothing. Your new column doesn’t get added.
  3. You try switching to Migrate(): Fails because there’s no __EFMigrationsHistory table - EF Core doesn’t know what state the database is in.
  4. You’re stuck: The only way forward is to either drop the database and start over, or manually create the migration history table and fake the entries.

I’ve seen this exact scenario play out in production. A team prototyped with EnsureCreated(), went to production without switching to migrations, and couldn’t apply schema changes when they needed to add a column three months later. They ended up writing raw SQL to alter the table and manually inserting migration history rows. It was not fun.

The Only Valid Use Case

EnsureCreated() has exactly one legitimate use case: integration tests with an in-memory or throwaway database that gets destroyed after each test run.

// This is fine - test database is thrown away after the test
var options = new DbContextOptionsBuilder<MovieDbContext>()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.Options;
using var context = new MovieDbContext(options);
context.Database.EnsureCreated(); // OK for tests

For everything else - development, staging, production - use Migrate() or one of the other approaches.

My Take: Which Strategy for Which Scenario?

Here’s the decision matrix I use when advising teams on migration strategy. The right answer depends on three factors: team size, CI/CD maturity, and compliance requirements.

ScenarioRecommended ApproachWhy
Solo developer, local devCLI (dotnet ef database update)Simple, immediate feedback, full control
Small team, no CI/CDDatabase.Migrate() in Program.csZero manual steps, just deploy the app
Team with CI/CD pipelineMigration bundlesAutomated, no SDK on prod, runs separately from app
Regulated industry (finance, health)SQL scripts + DBA reviewFull audit trail, human approval gate
Kubernetes / multi-instanceMigration bundles as init containerRuns once before any app instance starts
Integration testingEnsureCreated()Throwaway databases, no migration history needed

My default recommendation: If you have any kind of CI/CD pipeline, use migration bundles. They’re the most robust option - self-contained, idempotent, and designed for exactly this purpose. Trust me, I switched to bundles two years ago and haven’t looked back. The upfront setup in your pipeline takes 15 minutes, and you never have to worry about migration deployment again.

For small projects or early-stage development, Database.Migrate() in Program.cs is perfectly fine. Don’t over-engineer your deployment process for a two-developer side project. Just know its limitations and switch to bundles when you scale.

The one approach I never recommend for production: EnsureCreated(). It seems like the easiest option, but it’s a trap that creates more problems than it solves the moment you need to evolve your schema.

Production Migration Checklist

Before you apply migrations to a production database, walk through this checklist. I use a version of this for every deployment:

Before Deploying

  1. Back up the database - Non-negotiable. Even with idempotent scripts, things can go wrong.
  2. Review the migration SQL - Generate a script with dotnet ef migrations script --idempotent and read through it. Look for DROP statements, data type changes, and anything that could cause data loss.
  3. Test on a staging copy - Apply the migration to a copy of production data, not an empty test database. Schema changes that work on empty databases can fail on databases with millions of rows (lock timeouts, constraint violations).
  4. Check for long-running queries - Adding an index on a 10-million-row table can lock the table for minutes. Schedule migrations during low-traffic windows.
  5. Verify the Down() method - If you need to rollback, the Down() method must cleanly reverse the Up() changes. Auto-generated Down() methods handle most cases, but complex migrations might need manual fixes.

During Deployment

  1. Run migrations before deploying new app code - The new code expects the new schema. If the app starts before migrations finish, you’ll get runtime exceptions.
  2. Use a single migration runner - Whether it’s a CI/CD step, an init container, or a DBA running a script - only one process should apply migrations. Never rely on app startup migration in a multi-instance deployment.
  3. Monitor the migration - Watch for lock wait timeouts, deadlocks, and constraint errors. If a migration hangs, investigate before killing the process - you might end up with a partial migration.

After Deployment

  1. Verify __EFMigrationsHistory - Check that all expected migrations appear in the history table.
  2. Smoke test the API - Hit your critical endpoints. Schema changes can cause subtle issues that unit tests don’t catch (like missing columns that are only used in specific queries).

Common Mistakes and How to Avoid Them

Mistake 1: Using EnsureCreated() in Production

What happens: Database gets created without migration tracking. You can never use Migrate() afterward without manual intervention.

Fix: Replace EnsureCreated() with Database.Migrate(). If you’re already stuck, create an initial migration, then manually insert a row into __EFMigrationsHistory to mark it as applied.

Mistake 2: Forgetting --idempotent on SQL Scripts

What happens: You generate a script, run it on a database that already has some migrations, and it fails because it tries to re-create existing tables.

Fix: Always use dotnet ef migrations script --idempotent. The extra conditional checks add zero risk and make the script safe to re-run.

Mistake 3: Not Backing Up Before Production Migrations

What happens: A migration drops a column, and you realize too late that you needed that data. There’s no undo button for DROP COLUMN.

Fix: Automate database backups as the first step in your deployment pipeline. Make it impossible to run migrations without a backup.

Mistake 4: Running Migrate() in Multi-Instance Deployments

What happens: Three Kubernetes pods start simultaneously, all calling Migrate(). Two of them fail with concurrency errors, and your health checks mark those pods as unhealthy.

Fix: Use a migration bundle in a Kubernetes init container or a separate CI/CD step that runs before the deployment.

Mistake 5: Editing Migration Files After They’re Applied

What happens: You modify a migration file that’s already been applied to staging. EF Core detects the mismatch (the model snapshot doesn’t match) and throws errors on subsequent migrations.

Fix: Never edit applied migrations. If you need to change something, create a new migration. If you need to change a migration that hasn’t been applied to any shared environment yet, remove it with dotnet ef migrations remove and create a new one.

Mistake 6: Deleting Migration Files

What happens: You delete old migration files to “clean up” the folder. The model snapshot gets out of sync, and EF Core generates incorrect migrations going forward.

Fix: Don’t delete migration files unless you’re doing a proper migration squash. Each migration file contributes to the cumulative model snapshot. Deleting them breaks the chain.

Key Takeaways

  • EF Core gives you five approaches for applying migrations - CLI, Migrate(), SQL scripts, migration bundles, and EnsureCreated(). Each suits a different scenario.
  • Migration bundles are the production default - they’re self-contained, CI/CD-friendly, and don’t require the .NET SDK on deployment machines.
  • Never use EnsureCreated() outside of testing - it bypasses migration history and makes future schema changes impossible.
  • Always use --idempotent when generating SQL scripts - it makes scripts safe to re-run on partially migrated databases.
  • Back up before every production migration - migrations can drop columns, and there’s no undo. Make backups the first step in your deployment pipeline.
What is the difference between Migrate() and EnsureCreated() in EF Core?

Database.Migrate() applies pending migrations from your migration files and tracks them in the __EFMigrationsHistory table. EnsureCreated() creates the database schema from scratch based on your current model but completely bypasses the migration system. Migrate() supports incremental schema changes over time, while EnsureCreated() is a one-shot operation that cannot evolve the schema. Use Migrate() for any environment beyond throwaway test databases.

How do I apply EF Core migrations in production?

The recommended approach for production is migration bundles. Run dotnet ef migrations bundle to create a framework-dependent executable, then run it as a separate step in your CI/CD pipeline before deploying the application. Migration bundles don't require the .NET SDK on the production server and are designed to run exactly once. For regulated environments, generate idempotent SQL scripts with dotnet ef migrations script --idempotent and have a DBA review and execute them.

What are migration bundles in EF Core?

Migration bundles are self-contained executables that apply pending EF Core migrations to a database. They were introduced in EF Core 6.0 and are created with the dotnet ef migrations bundle command. The bundle contains all compiled migration files and the EF Core runtime, so it can run on any machine without needing the .NET SDK or source code. You pass a connection string as an argument, and it applies only pending migrations.

How do I generate SQL scripts from EF Core migrations?

Use the command dotnet ef migrations script --idempotent --output migrations.sql to generate a SQL script that checks which migrations are already applied before running each one. You can also generate scripts for a specific range by specifying from and to migrations: dotnet ef migrations script FromMigration ToMigration. The --idempotent flag is recommended because it makes the script safe to run multiple times.

Can I rollback a migration in EF Core?

Yes, you can rollback by targeting an earlier migration with dotnet ef database update MigrationName, which runs the Down() method of every migration after the target. However, rolling back a migration that dropped a column or table will not restore the data that was in those structures. Always back up your database before applying migrations in production so you can restore if a rollback is needed.

How do I apply EF Core migrations in a Docker container?

Build a migration bundle in your Dockerfile using a multi-stage build, then run the bundle as a separate step before starting your application container. In Kubernetes, use an init container that runs the bundle before the main app container starts. This ensures migrations run exactly once regardless of how many app replicas are deployed.

Should I run migrations at application startup?

Using Database.Migrate() at startup is convenient for single-instance applications and development environments. However, it is not recommended for multi-instance production deployments because multiple instances starting simultaneously can cause concurrency issues. For production, use migration bundles or SQL scripts that run as a separate step before the application starts.

What happens if I delete a migration file in EF Core?

Deleting a migration file breaks the migration chain. Each migration builds on the previous one's model snapshot, so removing one creates a gap that causes EF Core to generate incorrect future migrations. If you want to clean up old migrations, use a proper migration squash approach where you reset the migration history and create a fresh initial migration from the current model. Never delete individual migration files from the middle of the chain.

Troubleshooting

”The migration has already been applied to the database”

You’re trying to remove a migration with dotnet ef migrations remove, but it’s already been applied. Roll back the migration first with dotnet ef database update PreviousMigrationName, then remove it.

”More than one DbContext was found”

You have multiple DbContext classes and didn’t specify which one. Add the --context flag: dotnet ef database update --context MovieDbContext. See the Multiple DbContext article for detailed setup.

”A migration with that name already exists”

Migration names must be unique. Either use a more specific name, or if you’re re-creating a migration you just removed, make sure the removal completed successfully with dotnet ef migrations list.

Migration hangs on a large table

Adding a column or index to a large table can lock it for the duration of the operation. For PostgreSQL, use CREATE INDEX CONCURRENTLY by editing the migration file to use raw SQL: migrationBuilder.Sql("CREATE INDEX CONCURRENTLY ..."). Note that concurrent index creation cannot run inside a transaction, so you’ll also need to suppress the transaction for that migration.

Bundle fails with “connection string not found”

The bundle tries to resolve the connection string from the project’s configuration by default. If running outside the project directory, pass the connection string explicitly: ./efbundle --connection "your-connection-string".

”Pending model changes” warning when no changes were made

This happens when the model snapshot is out of sync. Run dotnet ef migrations add CheckSnapshot --project MovieApi.Api, check if the generated migration is empty (it should be if no real changes exist), then remove it. If it’s not empty, you have uncommitted model changes that need a proper migration.

Summary

Migrations in EF Core are straightforward in development - dotnet ef database update and you’re done. But production demands more thought. The approach you choose should match your team’s maturity: CLI for local dev, Database.Migrate() for simple single-instance apps, migration bundles for CI/CD pipelines, and SQL scripts for regulated environments.

The most important takeaway: treat migrations as deployment artifacts, not afterthoughts. Build the bundle in CI, test it on staging, and deploy it as a separate step before your application. This pattern has saved me from more production incidents than I can count.

If you found this article helpful, share it with your team - especially anyone who’s still using EnsureCreated() in production. And if you’re looking for the next step in mastering EF Core migrations, the Cleaning Migrations article covers squashing old migrations and keeping your migration folder manageable.

Subscribe to my newsletter for exclusive .NET content, decision matrices, and the production war stories I don’t put in articles. Happy Coding :)

Grab the Source Code

Get the full implementation. Enter your email for instant access, or skip to GitHub.

Skip, go to GitHub directly

Want to reach 7,100+ .NET developers? See sponsorship options.

What's your Feedback?

Do let me know your thoughts around this article.

Weekly .NET tips, free

Free weekly newsletter

Stay ahead in .NET

Tutorials Architecture DevOps AI

Once-weekly email. Best insights. No fluff.

Join 7,100+ developers · Delivered every Tuesday

We value your privacy

We use cookies to improve your browsing experience, analyze site traffic, and personalize content. By clicking "Accept All", you consent to our use of cookies. Read our Privacy Policy