👋Hi, I'm Waqas — a Software Architect and Technical Consultant specializing in .NET, Azure, microservices, and API-first system design..
I help companies build reliable, maintainable, and high-performance backend platforms that scale.
Repository and Unit of Work in .NET: when to use and how they fit with EF Core.
July 27, 2025 · Waqas Ahmad
Read the article
Introduction
This guidance is relevant when the topic of this article applies to your system or design choices; it breaks down when constraints or context differ. I’ve applied it in real projects and refined the takeaways over time (as of 2026).
Abstracting data access and coordinating transactions is common in .NET, but EF Core’s DbContext already provides both—so teams often wonder when to add an explicit Repository or Unit of Work. This article covers both patterns: what they are, when to use them with EF Core, implementation options, and common pitfalls. For architects and tech leads, adding a repository layer only when you need testability, swappable persistence, or domain-focused query APIs keeps the codebase clear and avoids unnecessary abstraction.
For a deeper overview of this topic, explore the full .NET Architecture guide.
System scale: Applications that need to query and persist data; from simple CRUD to domain-heavy with multiple aggregates. Applies when you’re deciding whether to add a repository layer on top of EF Core.
Team size: Backend developers; someone must own repository interfaces and implementations. Works when the team can agree on abstraction boundaries (or accept DbContext as the repository).
Time / budget pressure: Fits when you need testability (mock repository) or domain-focused query methods; breaks down when the app is simple CRUD and DbContext is enough—then skip the extra layer.
Technical constraints: .NET with EF Core; DbContext already provides DbSet (repository-like) and SaveChanges (unit of work). Assumes you can add interfaces and implementations if you go beyond EF Core.
Non-goals: This article does not optimise for “repository everywhere”; it focuses on when to add repository and unit of work and when EF Core is sufficient.
What is the Repository pattern?
Repository abstracts data access behind an interface. Clients use the repository instead of database APIs directly.
// DbContext as repository + unit of workpublicclassAppDbContext : DbContext
{
public DbSet<Order> Orders => Set<Order>();
public DbSet<Customer> Customers => Set<Customer>();
}
// Usagevar order = await _context.Orders.FindAsync(id);
order.Ship();
await _context.SaveChangesAsync(); // Unit of work commits all changes
So do you need your own Repository? Often no. EF Core provides the abstraction.
When to add your own Repository
Add your own Repository when:
You want to hide EF Core from business logic (Clean Architecture)
You need to swap persistence (EF to Dapper, or to another DB)
You want domain-focused query methods (not LINQ everywhere)
You need to test without EF Core (pure unit tests with mocks)
Exposes all operations (including ones you don’t want)
Hard to optimize queries per entity
Specific Repository is better:
publicinterfaceIOrderRepository
{
// Only methods this aggregate needs
Task<Order?> GetByIdAsync(OrderId id);
Task<IReadOnlyList<Order>> GetPendingAsync();
Task AddAsync(Order order);
// No Update/Delete if aggregate handles it internally
}
Recommendation: Use specific repositories with domain-focused methods.
Enterprise best practices
1. Use specific repositories, not generic. Domain-focused methods are more useful.
2. Repository per aggregate, not per table. One repository for Order (including OrderLines), not separate.
3. Keep repositories thin. Query and persist; no business logic.
4. Use DbContext directly for simple apps. Do not add abstraction you do not need.
5. Unit of Work should match request scope. One UoW per HTTP request (scoped).
6. Do not return IQueryable. Return materialized results. Keeps queries in repository.
7. Handle concurrency. Use EF Core’s concurrency tokens; handle DbUpdateConcurrencyException.
8. Test with in-memory or real DB. Integration tests with real DB are valuable.
Common issues
Issue
Cause
Fix
Repository becomes service
Business logic in repository
Keep logic in services/aggregates
IQueryable leaking
Repository returns IQueryable
Return IReadOnlyList or IEnumerable
Generic repository bloat
Everything uses same interface
Use specific repositories
Saving in repository
SaveChanges in each method
Save in Unit of Work at end
Over-abstraction
Repository for simple CRUD
Use DbContext directly
Testing difficulty
Hard to mock complex queries
Use integration tests; simpler interfaces
You can also explore more patterns in the .NET Architecture resource page.
Summary
Repository abstracts data access; Unit of Work coordinates transactions—EF Core’s DbContext already provides both, so add your own only when you need testability, swappable persistence, or domain-focused query APIs; use specific repositories per aggregate, not generic ones. Wrapping DbSet everywhere “by default” adds indirection without benefit; adding a repository when tests or a clear domain API justify it keeps persistence boundaries explicit. Next, decide whether your app needs to mock data access or expose domain-style methods (e.g. GetPendingForCustomer); if yes, add one repository for one aggregate and let the caller or a thin UoW own SaveChanges.
Position & Rationale
I use EF Core’s DbContext as the repository and unit of work when the app is straightforward CRUD and we don’t need to mock data access in unit tests—DbContext is already testable with an in-memory provider or integration tests. I add a repository layer when we need to swap persistence (e.g. for testing with fakes) or when we want domain-focused query methods (e.g. IOrderRepository.GetPendingForCustomer(customerId)) instead of exposing DbSet and LINQ everywhere. I prefer specific repositories (IOrderRepository, IProductRepository) over a generic IRepository<T> so each aggregate has a clear contract. I avoid putting SaveChanges inside repository methods; the unit of work (or the caller) should commit at the end of a logical operation. I don’t add a repository “by default” for every project—only when testability or domain API justifies it.
Trade-Offs & Failure Modes
Repository adds an abstraction and more types; you gain testability and a domain-oriented API. No repository keeps DbContext in the open; you gain simplicity but couple callers to EF. Generic repository (IRepository<T>) often grows into a fat interface that doesn’t match real queries; specific repositories stay focused. Failure modes: SaveChanges in every repository method (no single transaction boundary); over-abstraction for simple CRUD; generic repository that leaks EF or becomes a pass-through with no value.
What Most Guides Miss
Most guides show “create IRepository and implementation” but don’t stress that EF Core already is a repository (DbSet) and unit of work (SaveChanges)—you add your own only when you need a different abstraction. Another gap: specific vs generic—generic repositories tend to either expose too much (every LINQ method) or too little (only GetById, Add); specific repositories let you define GetPendingOrders, GetByCustomer, etc. Testing: if you only ever run integration tests with a real DB, you may not need a mockable repository; if you unit-test domain logic in isolation, a repository interface helps.
Decision Framework
If simple CRUD and no need to mock data access → Use DbContext directly; use integration tests for persistence.
If you need to mock or swap persistence, or want domain-focused query methods → Add specific repositories (IOrderRepository, etc.); keep Unit of Work at the scope (e.g. one SaveChanges per request).
Avoid generic IRepository<T> unless it’s a thin wrapper with a few methods; prefer specific repositories per aggregate.
Don’t call SaveChanges inside repository methods; let the unit of work or application service commit.
For testing → Integration tests with real or in-memory DB, or unit tests with repository mocks if you have the abstraction.
Key Takeaways
EF Core = DbSet (repository) + SaveChanges (unit of work); you may not need your own.
Add repository when you need testability (mocks) or domain-focused query API; use specific repositories per aggregate.
Unit of Work = one transaction boundary; don’t save inside each repository method.
Avoid generic repository bloat; prefer specific interfaces that match real use cases.
Need architectural guidance for real-world .NET platforms? I offer consulting for .NET architecture, API platforms, and enterprise system design.
When I Would Use This Again — and When I Wouldn’t
I’d use DbContext as repository again for simple CRUD and when we’re happy with integration tests for data access. I’d add specific repositories again when we need to mock persistence in unit tests or when we want a clear domain API (e.g. IOrderRepository.GetPendingForCustomer). I wouldn’t add a generic IRepository<T> that just wraps DbSet with no extra value. I also wouldn’t put SaveChanges inside repository methods—keep the transaction boundary at the application or unit-of-work level.
Frequently Asked Questions
Frequently Asked Questions
What is the Repository pattern?
Repository abstracts data access behind an interface. Clients use IOrderRepository instead of SQL or DbContext directly.
What is Unit of Work?
Unit of Work tracks changes and commits them in one transaction. In EF Core, SaveChangesAsync() is the unit of work.
Does EF Core already provide Repository?
Yes. DbSet<T> acts as a repository; SaveChangesAsync() is unit of work. You may not need your own.
When should I add my own Repository?
When you want to hide EF Core from business logic, swap persistence, add domain-focused queries, or mock in tests.
Should I use generic Repository?
Generally no. Specific repositories with domain methods are more useful and avoid exposing unwanted operations.
How do I test with Repository?
Unit tests: Mock the repository interface. Integration tests: Use EF Core with in-memory or real database.
Should Repository call SaveChanges?
No. Repository should not save. Unit of Work (or the caller) saves after all changes are made.
One repository per table or per aggregate?
Per aggregate. Order repository includes OrderLines. One repository per aggregate root.
Can I return IQueryable from Repository?
Avoid it. Return materialized results (IReadOnlyList). Keeps queries inside the repository.
How does Unit of Work fit with DI?
Register as scoped (one per request). Repositories share the same DbContext/UoW within the request.
What about Dapper with Repository?
Works well. Repository hides whether you use EF or Dapper. Switch implementations without changing callers.
Should every project use Repository?
No. For simple CRUD, DbContext directly is fine. Add abstraction when it provides value.
How do I handle transactions across repositories?
Unit of Work. Multiple repositories share one DbContext; one SaveChangesAsync() commits all changes.
What is the difference between Repository and DAO?
Similar. DAO (Data Access Object) is a more generic term. Repository is domain-focused and part of DDD vocabulary.
Can I use Repository without Unit of Work?
Yes. Repository can save internally. But Unit of Work is better for coordinating multiple changes.