Why I No Longer Wrap Entity Framework in Generic Repositories
The generic repository pattern sounds clean on paper, but it often adds indirection without buying a real architectural boundary. I have moved away from it as a default and prefer a more focused persistence design when the abstraction is actually earning its place.
Technical Insight
Common Advice
The received wisdom is that a generic repository is a clean way to keep application code from depending on Entity Framework. The idea is attractive because it sounds disciplined. If the application only talks to an IRepository<T> and not directly to DbContext or DbSet, then the persistence layer looks more modular and the domain model looks more isolated.
Why It Is Incomplete
In practice, the generic repository rarely delivers on that promise. I have seen teams add IRepository<T> and then, within a few sprints, extend it with IncludeRelated, GetPaged, GetWithSpec, or a dozen overloads that exist only to smuggle EF Core-specific behavior back through a generic interface. Eager loading, split queries, compiled queries, projection into DTOs, change tracking control, and raw SQL for a reporting query all resist that mapping. None of it fits cleanly onto Add, Update, Delete, and Find. So the interface either stays thin and mostly useless, or it grows until it is reimplementing DbSet<T> one method at a time, badly. The ORM-swap justification rarely holds up either. Most systems built this way stay on EF Core anyway. And the day a team actually migrates to Dapper or a document store, the repository interface has to be redesigned regardless, because EF's querying model and something like Dapper's do not share a shape.
Root Cause
The deeper issue is treating abstraction as inherently valuable, independent of what it is protecting. A repository earns its place when it encapsulates a boundary that actually needs protecting: a domain rule about how an aggregate is loaded and saved, a persistence detail the rest of the application genuinely should not know about, or a seam a specific test suite needs. A generic repository protects none of that. It is a pass-through. DbContext already is a unit of work. DbSet<T> already is a repository, with querying, tracking, and change detection built in. Wrapping it in another interface with the same shape does not add a boundary. It adds a translation layer between two APIs that already do the same thing. The structural mistake is reaching for a pattern because it is conventional, rather than asking what specific problem it is solving in this codebase.
Better Approach
What I favor now is using EF Core directly inside the application or use-case layer, and reserving a repository for cases where there is real domain-specific persistence behavior to encapsulate. Something like an OrderRepository that knows how to load an Order aggregate with its LineItems and enforce invariants on save is worth the interface. A generic CustomerRepository that just forwards to DbSet<Customer> is not.
public class PlaceOrderHandler
{
private readonly AppDbContext _db;
public PlaceOrderHandler(AppDbContext db) => _db = db;
public async Task Handle(PlaceOrderCommand cmd)
{
var order = new Order(cmd.CustomerId, cmd.Items);
_db.Orders.Add(order);
await _db.SaveChangesAsync();
}
}
The use case owns the query and the save. No IRepository in between, no interface pretending to be storage-agnostic when it is not. Testability does not disappear. It moves. Instead of mocking a repository, I test against EF Core's in-memory provider or a real SQL Server instance through Testcontainers, which also catches query-shape bugs that a mocked repository would hide entirely.
Trade-offs
This is not free. The application layer becomes more explicitly coupled to Entity Framework Core, and that is a real cost if there is ever serious pressure to change ORMs, not just a theoretical one. Testing strategy has to change too. Some teams are more comfortable mocking an interface than standing up a database in CI, and moving to integration-style tests against a real or in-memory provider is a bigger shift than it sounds like on a slide. Persistence concerns still need discipline. Removing the generic repository does not give a free pass to scatter DbContext calls through controllers or domain entities. That boundary still has to be enforced deliberately, without a repository interface doing it automatically. And a focused repository is still worth building when the aggregate or the query is complex enough to earn one. Getting rid of the generic version does not mean getting rid of the pattern entirely.
When Common Advice Is Correct
There are systems where the generic repository still makes sense. If a team genuinely expects to support multiple storage backends, or the application layer needs to stay strictly free of any ORM reference for organizational or licensing reasons, that abstraction is buying something real. Early in a project, before query patterns have settled, a thin repository can also buy time to figure out what the actual persistence boundary should look like. And for teams newer to EF Core, a constrained repository interface can prevent accidental N+1 queries or untracked-entity bugs until they are more familiar with the tool. In those cases, the extra layer is a reasonable trade, not a reflex.
Key Takeaways
- An abstraction is not automatically an improvement. It has to protect something specific.
- DbSet<T> and DbContext already provide repository and unit-of-work behavior.
- Generic repositories often hide EF Core's real capabilities for little return.
- Build focused repositories when there is a concrete domain reason for one.
- The real question is not repository or no repository. It is what boundary you are actually protecting.
Perspective from colleagues and peers
“The argument is sharp because it focuses on whether the abstraction is actually protecting a meaningful boundary.”
“This makes a strong case for being deliberate about persistence boundaries instead of reaching for abstractions by habit.”
“It is a practical reminder that a thin abstraction can be more harmful than no abstraction at all.”
Planning a complex platform decision?
I’m always interested in thoughtful conversations around architecture, cloud strategy, and practical AI-enabled systems.
Start a Conversation