.NETMedium
Repository pattern vs Unit of Work — do you still need both with EF Core?
Short answer: with EF Core, DbContext already is Unit of Work, and DbSet<T> already is a repository. Wrapping them in your own Repo + UoW is almost always a net loss.
What the patterns mean (textbook)
- Repository — collection-like abstraction over data access.
IRepository<T>.GetById, Add, Delete. - Unit of Work — tracks multiple repository operations and commits them as one transaction.
What EF Core gives you for free
// DbContext = Unit of Work
public class AppDb : DbContext {
public DbSet<Order> Orders => Set<Order>(); // DbSet = Repository<T>
public DbSet<Customer> Customers => Set<Customer>();
}
// Single transaction across multiple entities
order.Status = OrderStatus.Paid;
customer.LoyaltyPoints += 10;
await db.SaveChangesAsync(); // both writes commit atomically
DbSet<T> exposes LINQ (richer than typical IRepository), tracking is automatic, and SaveChangesAsync is a real DB transaction.
Why the extra layer rarely pays
| Reason cited | Reality |
|---|---|
| "Decouple from EF" | You almost never swap ORMs. Cost of the abstraction outweighs the benefit. |
| "Easier testing" | EF Core 8 has solid in-memory provider; also Testcontainers + real Postgres is one Docker line. |
| "Repository is the unit of testing" | DbContext mocking is cumbersome; integration tests vs real DB are usually faster to write. |
When repositories are still worth it
- Aggregate roots in DDD. The repository's job is to enforce that you only load whole aggregates, not stray entities. The interface is shaped around domain operations:
IOrderRepository.LoadWithItemsAsync(id). - Multiple data stores. Reads from Elastic, writes to Postgres. Repository hides the dual-write coordination.
- Specification pattern integration —
IRepository.Find(ISpecification<T>)keeps query logic out of services. See separate question on Specification pattern.
Better than generic repository — Query / Command split
public class OrderQueries(AppDb db) {
public Task<Order?> GetWithItemsAsync(Guid id) =>
db.Orders.Include(o => o.Items).FirstOrDefaultAsync(o => o.Id == id);
}
public class OrderCommands(AppDb db) {
public async Task PlaceAsync(Order o) {
db.Orders.Add(o);
await db.SaveChangesAsync();
}
}
You get the testable boundary without the leaky IRepository<T> ceremony.
Bottom line
EF Core already implements both patterns. Re-implementing them is a 2010-era habit that costs more than it saves in 2026.