Skip to main content
Article complete

Get one like this every Tuesday at 7 PM IST.

codewithmukesh
Back to blog
dotnet webapi-course 16 min read Lesson 27/127 Updated

Cleaning Migrations in EF Core 10 - Squash, Reset & Manage History

Learn when and how to clean EF Core 10 migrations. Squash, reset, remove, resolve team conflicts, plus a decision matrix for the right cleanup strategy.

Learn when and how to clean EF Core 10 migrations. Squash, reset, remove, resolve team conflicts, plus a decision matrix for the right cleanup strategy.

dotnet webapi-course

efcore migrations squash-migrations reset-migrations ef-core-10 dotnet-10 database-migrations migration-history efmigrationshistory migration-cleanup code-first merge-conflicts team-development migration-bundles postgresql aspnetcore web-api dotnet-webapi-zero-to-hero-course devops schema-management

Mukesh Murugan
Mukesh Murugan
Software Engineer
3.9K views
Chapter 27 of 127
View course

.NET Web API Zero to Hero Course

From dotnet new to docker push — REST, EF Core 10, auth, caching, Clean Architecture, observability. 127 hands-on lessons, source on GitHub.

EF Core (Entity Framework Core) migrations are like Git commits for your database schema - they track every change, every rename, every new column. But just like a Git repo with 500 tiny commits and no squashing, your Migrations folder can turn into an unmanageable mess. I have worked on a project with over 200 migration files that added up to nearly 1 GB of C# code. The build took over an hour. Adding a new migration froze Visual Studio for five minutes. It was brutal.

The thing is, most of those migrations were irrelevant. Columns added and then renamed. Tables created during a prototype phase and dropped weeks later. The migration history told the story of every wrong turn the team ever took, but nobody needed that story anymore. What the team needed was a clean starting point.

In this article, I will walk you through every strategy for cleaning up EF Core 10 (Entity Framework Core 10) migrations - from removing a single bad migration to squashing your entire history into one clean file. I will give you a decision matrix so you know exactly which approach fits your situation, share the mistakes I have seen teams make during cleanup, and cover how to handle migration conflicts in team environments. Let’s get into it.

TL;DR. EF Core 10 has no built-in migration squash command (GitHub issue #2174 has been open since 2015). To squash safely: back up the database, ensure all environments are on the latest migration, clear the __EFMigrationsHistory table, delete the Migrations/ folder, run dotnet ef migrations add InitialCreate to generate one fresh migration, then dotnet ef migrations script to get the INSERT statement for the history table and run it on every database. The whole process takes 15 minutes and zero data is lost. For removing a single migration use dotnet ef migrations remove; for rolling back several use dotnet ef database update <TargetMigration>. Add dotnet ef migrations has-pending-model-changes to CI to catch missing migrations before deploy.

Free course .NET
Vol. 01

.NET Web API Course

Master .NET Web API development from scratch. Learn to build production-ready APIs with Clean Architecture, best practices, and real-world patterns.

20+ chapters 5,000+ developers Highly rated

Start the course Free forever No signup wall
Read next Companion article

Running Migrations in EF Core 10

This article is the companion to the Running Migrations guide. If you are not familiar with how EF Core applies migrations, start there first - it covers the CLI, Migrate(), SQL scripts, migration bundles, and a production deployment checklist.

Why Do Migrations Pile Up?

Before jumping into cleanup strategies, it is worth understanding why migrations accumulate in the first place. If you are new to EF Core migrations, I covered the full lifecycle in my CRUD with EF Core tutorial - including creating your first migration and understanding code-first development.

Every time you run dotnet ef migrations add, EF Core generates three files:

  1. The migration file (XXXXXX_MigrationName.cs) - contains the Up() and Down() methods with the actual schema changes.
  2. The designer file (XXXXXX_MigrationName.Designer.cs) - contains metadata EF Core uses internally.
  3. The model snapshot (AppDbContextModelSnapshot.cs) - a snapshot of your entire model at that point in time. This file grows with every migration.

On a mature project, these files multiply fast. A team of five developers, each adding 2-3 migrations per sprint, generates 30+ migration files per month. Especially when working with complex entity configurations and relationships, every schema refinement adds another file. After a year, you are looking at 300+ files. And unlike source code, old migration files are rarely deleted because they represent the history of your database schema.

The real problems start when:

  • Build times explode - GitHub issue #30057 documents a project where 220 migration files caused build times to jump from 12 minutes to over an hour after upgrading to .NET 6.
  • dotnet ef migrations add freezes - GitHub issue #35976 reports EF Core 9 taking 5+ minutes to scaffold a new migration on projects with large migration histories, with CPU spikes that freeze the entire system.
  • The model snapshot becomes massive - Every migration updates the snapshot file with the complete model state. On large schemas, this file alone can be thousands of lines.
  • Merge conflicts become constant - The snapshot file is a single file that every migration touches. Two developers adding migrations on separate branches will always conflict on this file.

The solution is not to avoid migrations - they are essential. The solution is to periodically clean them up, the same way you would squash commits or clean up feature branches.

How to Remove the Last Migration

The simplest cleanup operation. You added a migration, realized the model change was wrong, and want to undo it before applying it to any database.

Terminal window
dotnet ef migrations remove

Or in the Visual Studio Package Manager Console:

Terminal window
Remove-Migration

This command does two things:

  1. Deletes the migration file and its designer file from the Migrations folder.
  2. Reverts the model snapshot to its previous state.

Important: This only works if the migration has NOT been applied to the database yet. If you already ran dotnet ef database update, you need to revert first:

Terminal window
dotnet ef database update PreviousMigrationName
dotnet ef migrations remove

The first command tells EF Core to run the Down() method of your latest migration, reverting the database schema. The second command then safely removes the migration files.

Warning: Never remove migrations that have been applied to production databases. The migration history in __EFMigrationsHistory would go out of sync with the files on disk, and future migrations would fail. For production, always create a new migration that reverses the changes instead.

How to Revert to a Previous Migration

Sometimes you need to roll back multiple migrations, not just the last one. Use dotnet ef database update with the name of the migration you want to revert TO:

Terminal window
dotnet ef migrations list

This shows all migrations in order with their applied status. Find the migration you want to keep, then:

Terminal window
dotnet ef database update AddBlogCreatedTimestamp

EF Core runs the Down() methods of every migration after AddBlogCreatedTimestamp, in reverse order. Your database schema rolls back to the state after that migration was applied.

Once the database is reverted, you can remove the unapplied migrations:

Terminal window
dotnet ef migrations remove

Run this command repeatedly until all unwanted migrations are gone. Each call removes the last unapplied migration.

My take: I only use this approach during development when I realize the last few migrations went in the wrong direction. It is a surgical tool for fixing recent mistakes. For cleaning up months or years of accumulated migrations, you need squashing.

How to Squash EF Core Migrations (The Safe Way)

Squashing migrations means replacing your entire migration history with a single “initial” migration that represents the current database schema. It is the EF Core equivalent of git rebase --squash - you collapse hundreds of incremental changes into one clean snapshot.

EF Core does not have a built-in squash command (GitHub issue #2174 has been open since 2015). But the process is straightforward if you follow these steps carefully.

Step 1: Back Up Everything

Before touching anything, create backups:

Terminal window
# Back up the Migrations folder
cp -r Migrations Migrations_backup
# Back up your database (PostgreSQL example)
pg_dump -U postgres -d your_database > backup_before_squash.sql

For SQL Server, use SQL Server Management Studio or sqlcmd to create a backup. The point is - if something goes wrong, you can restore everything.

Step 2: Verify All Environments Are in Sync

Every database that your application connects to (development, staging, production) must have all existing migrations applied. Run this check:

Terminal window
dotnet ef migrations list --connection "your_connection_string"

If any environment has pending migrations, apply them first. Squashing only works safely when all environments are at the same migration state.

Step 3: Clear the Migration History Table

Connect to your database and delete all rows from the __EFMigrationsHistory table:

-- PostgreSQL
DELETE FROM "__EFMigrationsHistory";
-- SQL Server
DELETE FROM [__EFMigrationsHistory];

This tells EF Core that no migrations have been applied, even though the actual schema is fully up to date.

Step 4: Delete the Migrations Folder

Remove all existing migration files from your project:

Terminal window
rm -rf Migrations/

Or just delete the folder in your IDE. The model snapshot file goes with it.

Step 5: Create a Fresh Initial Migration

Terminal window
dotnet ef migrations add InitialCreate

EF Core generates a brand new migration that represents your entire current model. The Up() method contains every CREATE TABLE, every index, every relationship - everything needed to build your database from scratch.

Step 6: Generate the History Insert Script

You need the exact SQL that EF Core would insert into __EFMigrationsHistory for this new migration. Generate a SQL script:

Terminal window
dotnet ef migrations script

The very last line of the output will look like this:

INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
VALUES ('20260310120000_InitialCreate', '10.0.0');

Copy that INSERT statement.

Step 7: Insert the History Record

Run the copied INSERT statement against every database (dev, staging, production):

INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
VALUES ('20260310120000_InitialCreate', '10.0.0');

This tells EF Core “the InitialCreate migration has already been applied” - which it effectively has, since the schema is already there. Future migrations will be applied on top of this clean baseline.

Step 8: Verify

Run the migration list to confirm everything is clean:

Terminal window
dotnet ef migrations list

You should see a single migration marked as applied. Test by making a small model change and adding a new migration - it should scaffold instantly and only contain the incremental change. Isn’t that cool? From hundreds of files to one clean migration.

Warning: Any custom SQL in your old migrations (raw SQL operations, stored procedures, triggers, data seeding logic) will be lost during squashing. Review your old migrations for any migrationBuilder.Sql() calls before deleting them. You will need to manually add those to the new initial migration or handle them separately.

How to Reset All Migrations (Nuclear Option)

If you are in development and do not care about preserving the database data, resetting is simpler than squashing:

  1. Delete the Migrations folder.
  2. Drop the database entirely.
  3. Create a fresh initial migration.
  4. Apply it.
Terminal window
rm -rf Migrations/
dotnet ef database drop --force
dotnet ef migrations add InitialCreate
dotnet ef database update

This is the “nuclear option” and I only recommend it in two scenarios:

  1. Early development - You are prototyping, the schema changes daily, and nobody depends on the existing data.
  2. Throw-away environments - CI/CD pipelines that create fresh databases for every test run.

Never use this approach when any environment has data you need to keep. Use the squashing approach above instead.

Resolving Migration Merge Conflicts in Teams

One of the biggest reasons teams want to clean up migrations is merge conflict fatigue. When two developers add migrations on separate branches, the model snapshot file (AppDbContextModelSnapshot.cs) will always conflict because both migrations update it.

Trivial Conflicts (Most Common)

If both developers added unrelated changes - say, one added a Deactivated column and another added a LoyaltyPoints column - the snapshot conflict is trivial. You will see something like this:

<<<<<<< yours
b.Property<bool>("Deactivated");
=======
b.Property<int>("LoyaltyPoints");
>>>>>>> theirs

Keep both lines. The migrations are independent and can coexist.

True Conflicts

If both developers modified the same property - say, both renamed the Name column but to different names - you have a real conflict that cannot be auto-merged.

The correct resolution, per Microsoft’s official guidance:

  1. Abort the merge and roll back to your branch before the merge.
  2. Remove your migration (but keep your model/code changes).
  3. Merge your teammate’s changes into your branch.
  4. Re-add your migration on top of the merged state.
Terminal window
git merge --abort
dotnet ef migrations remove
git merge teammate-branch
dotnet ef migrations add YourMigrationName

This ensures migrations are always ordered correctly and the snapshot reflects the true combined model state.

Prevention: Team Migration Rules

I have been on teams where migration conflicts were a weekly headache, and teams where they almost never happened. The difference was process, not tooling:

  1. One migration per PR - Never ship a PR with multiple migrations. If your feature needs three schema changes, combine them into one migration before opening the PR.
  2. Pull before migrating - Always pull the latest main branch before running dotnet ef migrations add. This ensures your migration builds on the latest snapshot.
  3. Coordinate on big schema changes - If two developers need to touch the same table, one goes first. A 5-minute Slack message saves an hour of conflict resolution.
  4. Use has-pending-model-changes - EF Core 8+ added dotnet ef migrations has-pending-model-changes to check if your model has changed since the last migration. Use this in CI to catch forgotten migrations.
Terminal window
dotnet ef migrations has-pending-model-changes

If this returns Changes, someone forgot to add a migration. I add this check to every CI pipeline - it catches the “works on my machine” problem where a developer forgets to commit their migration file.

Free resource Companion download

Interview Questions PDF

100 real interview questions across 9 categories, junior to senior

Customizing the __EFMigrationsHistory Table

By default, EF Core tracks applied migrations in a table called __EFMigrationsHistory with two columns: MigrationId and ProductVersion. You can customize this table if needed.

Changing the Table Name and Schema

Configure it in your DbContext:

protected override void OnConfiguring(DbContextOptionsBuilder options)
=> options.UseNpgsql(
connectionString,
x => x.MigrationsHistoryTable("__MyMigrationsHistory", "migrations"));

Or when using dependency injection in Program.cs:

builder.Services.AddDbContext<AppDbContext>(options =>
options.UseNpgsql(
builder.Configuration.GetConnectionString("DefaultConnection"),
x => x.MigrationsHistoryTable("__MyMigrationsHistory", "migrations")));

Customizing Column Names

For deeper customization, you need to override the IHistoryRepository service. Here is an example that renames the MigrationId column:

internal class CustomHistoryRepository : NpgsqlHistoryRepository
{
public CustomHistoryRepository(HistoryRepositoryDependencies dependencies)
: base(dependencies) { }
protected override void ConfigureTable(EntityTypeBuilder<HistoryRow> history)
{
base.ConfigureTable(history);
history.Property(h => h.MigrationId).HasColumnName("Id");
}
}

Register it:

builder.Services.AddDbContext<AppDbContext>(options =>
options.UseNpgsql(connectionString)
.ReplaceService<IHistoryRepository, CustomHistoryRepository>());

My take: I rarely customize the history table. The default works fine for most projects. The one exception is multi-tenant architectures where each tenant has its own schema - moving the history table to a shared schema keeps migration tracking centralized.

When to Clean: The Signals

Not every project needs migration cleanup. Here are the signals that tell you it is time:

SignalSeverityAction
dotnet ef migrations add takes > 30 secondsMediumConsider squashing
Build time increased noticeably after adding migrations projectHighSquash immediately
Migrations folder has 50+ filesLowPlan a squash at the next major release
Migrations folder has 100+ filesMediumSchedule a squash this sprint
Migrations folder has 200+ filesHighSquash now - you are likely experiencing build and tooling slowdowns
Model snapshot file is > 2000 linesMediumSquash - the snapshot is slowing down migration scaffolding
Every PR has snapshot merge conflictsHighSquash + enforce team migration rules
Old migrations reference entities/tables that no longer existLowCosmetic - squash at convenience

My rule of thumb: Clean your migrations at every major .NET upgrade. You are already making breaking changes, updating packages, and testing thoroughly. Squashing migrations at the same time is virtually free and gives you a clean baseline for the next development cycle. I have been doing this for every project since .NET 6, and it takes about 15 minutes per database context.

My Take: The Cleanup Decision Matrix

Every cleanup scenario maps to one of four strategies. Here is how I decide:

ScenarioStrategyRiskTime
Last migration was wrong, not yet applieddotnet ef migrations removeNone30 seconds
Last few dev migrations need a redoRevert + removeLow5 minutes
50+ accumulated migrations, all environments in syncSquashLow-Medium15-30 minutes
Early development, schema is unstable, no real dataFull reset (drop + recreate)None2 minutes
Production database with years of historySquash (never reset)Medium30 minutes + testing
Multiple database contexts in one projectSquash each context separatelyMedium15 min per context
Team with frequent merge conflictsSquash + enforce migration rulesLow15 min + process change

The approaches I would avoid:

  • Never delete migration files without clearing __EFMigrationsHistory - EF Core will think those migrations still need to be applied and will fail.
  • Never manually edit the snapshot file - Always let EF Core regenerate it by removing and re-adding the migration.
  • Never squash if environments are out of sync - If staging is 3 migrations behind production, squashing will make it impossible to catch up. Sync first, then squash.
Read next Companion article

Global Query Filters in EF Core

If your project uses global query filters for soft deletes or multi-tenancy, be extra careful when squashing - the filter configuration in the snapshot must match your current model exactly.

Checking for Pending Model Changes in CI

EF Core 8+ introduced a helpful command for CI pipelines:

Terminal window
dotnet ef migrations has-pending-model-changes

This returns a non-zero exit code if your entity model has changed since the last migration was created. Add it to your CI pipeline to catch forgotten migrations:

# GitHub Actions example
- name: Check for pending EF Core model changes
run: dotnet ef migrations has-pending-model-changes --project src/MyApp.Api

You can also check programmatically in a unit test:

[Fact]
public void No_Pending_Model_Changes()
{
using var context = new AppDbContext(options);
Assert.False(context.Database.HasPendingModelChanges(),
"Model has changed since the last migration. Run 'dotnet ef migrations add'.");
}

This test fails when someone changes an entity class but forgets to create a migration. I add this to every project - it has caught forgotten migrations at least a dozen times. Pair it with structured logging and you will know exactly when and why a migration check failed in your pipeline.

Key Takeaways

  1. Remove recent mistakes with dotnet ef migrations remove - but only if the migration has not been applied to any database you care about.
  2. Squash accumulated migrations by deleting the Migrations folder, clearing __EFMigrationsHistory, creating a fresh InitialCreate, and inserting the history record. This is safe for production if done correctly.
  3. Reset (drop + recreate) only in early development or throwaway environments. Never in production.
  4. Prevent merge conflicts with team rules: one migration per PR, pull before migrating, and coordinate on shared tables.
  5. Clean at every major .NET upgrade - it takes 15 minutes and gives you a clean baseline.
  6. Add has-pending-model-changes to your CI pipeline to catch forgotten migrations automatically.

Troubleshooting Common Issues

”The migration has already been applied to the database”

Cause: You tried to remove a migration that has already been applied with dotnet ef database update.

Fix: Revert the migration first, then remove it:

Terminal window
dotnet ef database update PreviousMigrationName
dotnet ef migrations remove

“No migration was found with the ID specified”

Cause: The migration name you passed to dotnet ef database update does not match any migration in your project. Migration names are case-sensitive.

Fix: Run dotnet ef migrations list to see the exact names, then use the correct one.

Snapshot File Conflicts After Squash

Cause: You squashed migrations on your branch, but a teammate added a new migration on main that references the old snapshot state.

Fix: Merge main into your branch first, apply any pending migrations, THEN squash. The squash must happen on a branch that has every migration ever created.

”The model backing the context has changed since the database was created”

Cause: After squashing, you forgot to insert the history record into __EFMigrationsHistory. EF Core thinks no migrations have been applied and wants to run InitialCreate, which would fail because the tables already exist.

Fix: Run the INSERT statement from Step 7 of the squashing process against the affected database.

Build Time Still Slow After Squash

Cause: You squashed migrations but the old migration files are still in your Git history, and your IDE is indexing them.

Fix: Verify the Migrations folder only contains the new InitialCreate files and the snapshot. Run dotnet build from the CLI to confirm the build time improved. If your IDE is still slow, invalidate its cache.

EF Core Freezes When Adding New Migration

Cause: EF Core 9 has a known issue where adding migrations on projects with large models causes CPU spikes and system freezes.

Fix: Squash your existing migrations to reduce the history EF Core needs to process. If the problem persists with a clean migration history, consider using compiled models to improve startup performance.

How do I squash EF Core migrations without losing data?

Back up your database, delete all rows from the __EFMigrationsHistory table, delete the Migrations folder, run dotnet ef migrations add InitialCreate, generate a SQL script with dotnet ef migrations script, then insert the history record from the last line of the script into every database. Your schema stays intact because you never touch the actual tables - only the migration tracking metadata.

Is it safe to delete old migration files in EF Core?

Only if those migrations have been applied to ALL environments AND you also clear the corresponding rows from __EFMigrationsHistory. If you delete files without updating the history table, EF Core will lose track of which migrations have been applied and will fail on future migrations. The safest approach is to squash all migrations into a single InitialCreate migration.

How many migrations is too many in EF Core?

There is no hard limit, but problems typically start around 50 to 100 migrations. Build time increases, migration scaffolding slows down, and merge conflicts become more frequent. Projects with 200 or more migrations have reported build times increasing from minutes to over an hour. Clean up when you notice tooling slowdowns or when upgrading to a new major .NET version.

How do I resolve EF Core migration merge conflicts?

If both changes are unrelated (different properties or tables), keep both lines in the snapshot file. If both developers modified the same property, abort the merge, remove your migration but keep your code changes, merge your teammates branch, then re-add your migration. This ensures the snapshot reflects the true combined model state.

What is the __EFMigrationsHistory table and can I clean it up?

The __EFMigrationsHistory table is where EF Core records which migrations have been applied to the database. It has two columns: MigrationId and ProductVersion. You can clean it up during a squash operation by deleting all rows and inserting a single row for your new InitialCreate migration. Never delete rows without also deleting the corresponding migration files.

Does EF Core have a built-in squash migrations command?

No. There is a long-standing feature request on GitHub (issue 2174, open since 2015) but EF Core does not include a built-in squash command. The process is manual: clear the history table, delete migration files, create a fresh InitialCreate, and insert the history record. Third-party tools like StewardEF can automate parts of this process.

How do I reset EF Core migrations to a clean slate?

If you can drop the database (development only), delete the Migrations folder, run dotnet ef database drop --force, then dotnet ef migrations add InitialCreate followed by dotnet ef database update. If you need to keep data (production), use the squash approach: clear __EFMigrationsHistory, delete migration files, create InitialCreate, and insert the history record.

Can too many migrations slow down my build?

Yes. Each migration file is compiled C# code. Projects with 200 or more migrations have reported build times exceeding one hour and migration scaffolding taking 5 or more minutes. The model snapshot file also grows with every migration, which slows down the dotnet ef migrations add command. Squashing migrations is the most effective fix.

Summary

Migration cleanup is not something you do once - it is a recurring maintenance task, like updating NuGet packages or rotating secrets. The key is knowing which tool to reach for: remove for quick fixes, squash for periodic cleanup, and reset only when you can afford to lose data.

The strategy I follow on every project: squash at every major .NET upgrade. It takes 15 minutes, eliminates months of accumulated noise, and gives the team a clean foundation for the next release cycle. Combined with team rules (one migration per PR, pull before migrating) and a CI check for pending model changes, migration management goes from a weekly headache to something you barely think about. Trust me, once you adopt this rhythm, you will wonder why you ever let migrations pile up in the first place.

If you are building APIs with .NET 10, check out my free course that covers EF Core CRUD, entity configuration, relationships, and 100+ more lessons from zero to production.

Happy Coding :)

Source code Open on GitHub

Grab the source code.

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

Skip — go straight to GitHub
Continue readingHand-picked from the archive
View all articles
The conversation Hosted on GitHub Discussions

What's your take?

Push back, share a war story, or ask the obvious question someone else is wondering. I read every comment.

View on GitHub
All posts codewithmukesh · Trivandrum

Weekly .NET + AI tips · free

Newsletter

stay ahead in .NET

One email every Tuesday at 7 PM IST. One topic, deep. The week's articles. No filler.

Tutorials Architecture DevOps AI
Join 8,429 developers · Delivered every Tuesday
Privacy notice 30s read

Cookies, but only the useful ones.

I use cookies to understand which articles get read and which CTAs actually work. No third-party advertising trackers, ever. Read the privacy policy →