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
__EFMigrationsHistorytable, delete theMigrations/folder, rundotnet ef migrations add InitialCreateto generate one fresh migration, thendotnet ef migrations scriptto 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 usedotnet ef migrations remove; for rolling back several usedotnet ef database update <TargetMigration>. Adddotnet ef migrations has-pending-model-changesto CI to catch missing migrations before deploy.
.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
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:
- The migration file (
XXXXXX_MigrationName.cs) - contains theUp()andDown()methods with the actual schema changes. - The designer file (
XXXXXX_MigrationName.Designer.cs) - contains metadata EF Core uses internally. - 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 addfreezes - 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.
dotnet ef migrations removeOr in the Visual Studio Package Manager Console:
Remove-MigrationThis command does two things:
- Deletes the migration file and its designer file from the Migrations folder.
- 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:
dotnet ef database update PreviousMigrationNamedotnet ef migrations removeThe 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
__EFMigrationsHistorywould 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:
dotnet ef migrations listThis shows all migrations in order with their applied status. Find the migration you want to keep, then:
dotnet ef database update AddBlogCreatedTimestampEF 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:
dotnet ef migrations removeRun 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:
# Back up the Migrations foldercp -r Migrations Migrations_backup
# Back up your database (PostgreSQL example)pg_dump -U postgres -d your_database > backup_before_squash.sqlFor 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:
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:
-- PostgreSQLDELETE FROM "__EFMigrationsHistory";
-- SQL ServerDELETE 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:
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
dotnet ef migrations add InitialCreateEF 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:
dotnet ef migrations scriptThe 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:
dotnet ef migrations listYou 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:
- Delete the
Migrationsfolder. - Drop the database entirely.
- Create a fresh initial migration.
- Apply it.
rm -rf Migrations/dotnet ef database drop --forcedotnet ef migrations add InitialCreatedotnet ef database updateThis is the “nuclear option” and I only recommend it in two scenarios:
- Early development - You are prototyping, the schema changes daily, and nobody depends on the existing data.
- 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:
<<<<<<< yoursb.Property<bool>("Deactivated");=======b.Property<int>("LoyaltyPoints");>>>>>>> theirsKeep 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:
- Abort the merge and roll back to your branch before the merge.
- Remove your migration (but keep your model/code changes).
- Merge your teammate’s changes into your branch.
- Re-add your migration on top of the merged state.
git merge --abortdotnet ef migrations removegit merge teammate-branchdotnet ef migrations add YourMigrationNameThis 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:
- 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.
- Pull before migrating - Always pull the latest
mainbranch before runningdotnet ef migrations add. This ensures your migration builds on the latest snapshot. - 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.
- Use
has-pending-model-changes- EF Core 8+ addeddotnet ef migrations has-pending-model-changesto check if your model has changed since the last migration. Use this in CI to catch forgotten migrations.
dotnet ef migrations has-pending-model-changesIf 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.
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:
| Signal | Severity | Action |
|---|---|---|
dotnet ef migrations add takes > 30 seconds | Medium | Consider squashing |
| Build time increased noticeably after adding migrations project | High | Squash immediately |
| Migrations folder has 50+ files | Low | Plan a squash at the next major release |
| Migrations folder has 100+ files | Medium | Schedule a squash this sprint |
| Migrations folder has 200+ files | High | Squash now - you are likely experiencing build and tooling slowdowns |
| Model snapshot file is > 2000 lines | Medium | Squash - the snapshot is slowing down migration scaffolding |
| Every PR has snapshot merge conflicts | High | Squash + enforce team migration rules |
| Old migrations reference entities/tables that no longer exist | Low | Cosmetic - 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:
| Scenario | Strategy | Risk | Time |
|---|---|---|---|
| Last migration was wrong, not yet applied | dotnet ef migrations remove | None | 30 seconds |
| Last few dev migrations need a redo | Revert + remove | Low | 5 minutes |
| 50+ accumulated migrations, all environments in sync | Squash | Low-Medium | 15-30 minutes |
| Early development, schema is unstable, no real data | Full reset (drop + recreate) | None | 2 minutes |
| Production database with years of history | Squash (never reset) | Medium | 30 minutes + testing |
| Multiple database contexts in one project | Squash each context separately | Medium | 15 min per context |
| Team with frequent merge conflicts | Squash + enforce migration rules | Low | 15 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.
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:
dotnet ef migrations has-pending-model-changesThis 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.ApiYou 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
- Remove recent mistakes with
dotnet ef migrations remove- but only if the migration has not been applied to any database you care about. - Squash accumulated migrations by deleting the Migrations folder, clearing
__EFMigrationsHistory, creating a freshInitialCreate, and inserting the history record. This is safe for production if done correctly. - Reset (drop + recreate) only in early development or throwaway environments. Never in production.
- Prevent merge conflicts with team rules: one migration per PR, pull before migrating, and coordinate on shared tables.
- Clean at every major .NET upgrade - it takes 15 minutes and gives you a clean baseline.
- Add
has-pending-model-changesto 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:
dotnet ef database update PreviousMigrationNamedotnet 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 :)




What's your take?
Push back, share a war story, or ask the obvious question someone else is wondering. I read every comment.