Tired of waiting forever for your .NET builds? Our project had 400+ EF Core migrations and builds took 15 minutes — until we tried these two fixes.
When you start a new .NET project, everything feels smooth — builds are fast, migrations are few, and iteration is quick. But as projects evolve into long-term, business-critical systems, reality starts to look very different.
In one of the projects I’m working on, we’ve accumulated more than 400 EF Core migrations over several years of development. On paper, this doesn’t sound too bad — migrations are just C# files, right? But in practice, the build process became painfully slow:
- Even with solid hardware, a full build took 10–15 minutes.
- Small surface-level changes still needed 3–4 minutes to build.
- Changes in the application or domain layer (where entities live) triggered full rebuilds.
At that point, the development process looked like this: you could realistically build and test 3–4 times per hour. And as every .NET developer knows, waiting on builds is one of the fastest ways to lose focus and momentum.
So I started digging into ways to optimize this. What I discovered turned out to be surprisingly simple at first — and then led me to a more sustainable long-term solution.
In this article, I’ll share:
- A quick win you can apply in minutes to cut build time drastically.
- A more elegant solution with a dedicated migrations project.
- Some thoughts on the bigger picture (modular monoliths, legacy realities, microservices).
The Problem
EF Core migrations are useful and necessary — they keep your database schema in sync with your evolving domain. But in very large projects, they can also become a hidden bottleneck.
Here’s what we found when analyzing why our builds had become painfully slow:
-
Every migration is just C# code: EF migrations are stored as generated
.cs
files. When you have a handful, it’s fine — but with hundreds of migrations, every build must recompile all of them. - Deep changes trigger everything: Modifying code in the domain layer means the compiler re-checks all dependencies — migrations included. Even a small change can cause a full rebuild.
- Builds become painfully long: With 400+ migrations, a clean build can take 10–15 minutes, even on powerful hardware.
- Developer experience suffers: Productivity drops fast. Developers start avoiding builds “just to save time,” which delays feedback loops and slows the whole team down.
Of course, in theory, you wouldn’t end up here. Bounded contexts, modular monoliths, or even microservices could have prevented such a massive DbContext. But in real life, many long-term projects grow this way — often under business pressure where refactoring isn’t a priority.
So instead of a rewrite, we needed practical fixes that worked immediately.
First Solution: Exclude Migrations from Build
The first discovery was almost embarrassingly simple.
If you’re not actively working on migrations, you don’t need them compiled every time. In .NET, you can exclude files or folders from the build inside your .csproj
:
<ItemGroup>
<Compile Remove="Migrations\**\*.cs" />
</ItemGroup>
That’s it. Just one line.
The Results
As soon as we added this, build times dropped dramatically:
- From 10–15 minutes → to 30–60 seconds.
- Incremental builds felt instant again.
- Developer flow was restored.
The first build after excluding migrations felt almost magical. Of course, the trade-off is obvious: if you need to create or apply migrations, you’ll have to temporarily remove the exclusion. But migrations are usually touched less frequently, so this trade-off is worth it.
More Elegant Solution: A Dedicated Migrations Project
Excluding migrations is great for a quick fix, but it’s not the cleanest long-term strategy. Constantly toggling your .csproj
can be annoying.
A better approach is to move migrations into a separate project.
Why a Separate Migrations Project?
- Keeps the main project lean — no extra compilation overhead.
- Improves separation of concerns — domain code stays clean.
- Optional compilation — build migrations only when you actually need them.
How to Do It
- Create a new Class Library (e.g.,
MyApp.Migrations
). - Move your
Migrations
folder into this project. -
Add a reference to the domain project that contains your
DbContext
:
<ProjectReference Include="..\MyApp.Domain\MyApp.Domain.csproj" />
-
Update EF Core commands to use the new project:
dotnet ef migrations add Init --project MyApp.Migrations --startup-project MyApp.Web dotnet ef database update --project MyApp.Migrations --startup-project MyApp.Web
Now, your migrations live in their own project, isolated from your main application builds. You only compile them when you actually need them.
The Bigger Picture
This is where the architecture conversation comes in.
Yes, ideally, you wouldn’t have hundreds of migrations in a single DbContext. Modern approaches like modular monoliths encourage splitting the system into modules — each with its own DbContext and migrations. That way, you get strong boundaries without the full complexity of microservices.
In hindsight, adopting a modular monolith could have kept our migrations more manageable over time. But for a legacy project, it’s not always realistic to refactor the entire architecture just to speed up builds.
That’s why small steps like excluding migrations or moving them into a separate project can be game-changers. They don’t erase architectural debt, but they give developers back their time.
In our case, we went from:
- 3–4 builds per hour → to as many as needed without frustration.
- Developers stopped avoiding builds “just to save time.”
- The feedback loop became fast again, which improved both focus and morale.
Conclusion
Long build times silently kill productivity. In our case, EF Core migrations were the culprit — and the fixes were surprisingly simple.
- Exclude migrations from build when you don’t need them.
- Move migrations into their own project for a sustainable long-term solution.
- Keep modular monoliths or bounded contexts in mind to prevent this issue in the future.
These changes helped us cut build times from 15 minutes to under 1 minute — and restored developer flow in a project that had grown massive over the years.
💬 What about you?
Have you faced similar issues with EF Core migrations in large projects?
Did you solve them with modular monoliths, microservices, or something else entirely?
I’d love to hear your experience in the comments 👇
✍️ Author Note
Appreciate you reading this! I’m a software developer and computer science engineer exploring .NET, architecture, and productivity. I might be wrong sometimes — and that’s why I’d love to hear your feedback or alternative approaches in the comments.
Top comments (0)