The Repository Pattern is one of those architectural patterns that almost every .NET developer eventually encounters.
It appears frequently in Clean Architecture examples, Domain-Driven Design discussions, enterprise applications, and software architecture courses.
The usual idea sounds reasonable:
Put a repository between your application and your database so your business logic does not depend directly on persistence technology.
But once Entity Framework Core enters the picture, things become less obvious.
EF Core already provides many of the capabilities that developers traditionally expect from repositories and Unit of Work implementations.
That raises an important architectural question:
Do we still need repositories when using EF Core?
The answer, in my view, is not simply yes or no.
The more useful question is:
What problem is the repository actually solving?
The Traditional Repository Pattern
A common implementation looks something like this:
public interface IRepository<T>
where T : class
{
Task<T?> GetByIdAsync(Guid id);
Task<IReadOnlyList<T>> GetAllAsync();
void Add(T entity);
void Update(T entity);
void Delete(T entity);
}Then every entity gets the same abstraction:
IRepository<User>
IRepository<Order>
IRepository<Product>
IRepository<Customer>At first glance, this looks clean.
The application no longer talks directly to EF Core.
But there is a potential problem.
We may simply be recreating functionality that
DbSet<T> already provides.
EF Core already supports:
- entity tracking
- querying
- inserts
- updates
- deletes
- change detection
- transactions
- persistence coordination
If our repository merely forwards calls to
DbSet<T>, then the abstraction may not be adding much
architectural value.
Instead, we may simply be introducing another layer developers need to understand, maintain, and test.
EF Core Already Behaves Like a Repository
Consider a typical EF Core DbContext.
public sealed class ApplicationDbContext : DbContext
{
public DbSet<Order> Orders { get; set; }
public DbSet<Customer> Customers { get; set; }
public DbSet<Product> Products { get; set; }
}Each DbSet<T> already gives us repository-like
behavior.
For example:
Order? order = await dbContext.Orders
.FirstOrDefaultAsync(
x => x.Id == orderId,
cancellationToken);Adding a generic repository may result in something like this:
Order? order = await orderRepository
.GetByIdAsync(orderId);The second version may look cleaner, but we should ask whether the abstraction is protecting anything meaningful.
If the repository simply executes the same EF Core query internally, we have added another layer without necessarily improving the design.
This is where repository implementations can become architectural ceremony rather than architectural value.
A Repository Should Represent Domain Intent
Repositories become much more useful when they communicate business intent rather than CRUD operations.
Instead of this:
Task<Order?> GetByIdAsync(Guid id);we might have:
Task<Order?> GetPendingOrderAsync(
Guid orderId,
CancellationToken cancellationToken);or:
Task<Order?> GetOrderForCustomerAsync(
Guid orderId,
Guid customerId,
CancellationToken cancellationToken);Now the repository represents something meaningful in the application.
The abstraction is no longer simply hiding EF Core.
It is expressing a domain rule.
That distinction is important.
A repository should ideally answer questions such as:
- Which aggregate are we loading?
- Under what business conditions?
- Which security or ownership rules apply?
- What persistence behavior should remain consistent?
That is much more valuable than creating a generic wrapper around
DbSet<T>.
Repositories Work Well Around Aggregate Boundaries
This becomes even more important when working with Domain-Driven Design.
Suppose we have an Order aggregate:
Order
├── OrderItem
├── ShippingAddress
└── PaymentInformation
It usually does not make sense to create independent repositories for every object.
For example:
IOrderRepository
IOrderItemRepository
IShippingAddressRepository
IPaymentInformationRepository
If OrderItem belongs to the Order
aggregate, persistence should generally happen through the aggregate
root.
A more appropriate boundary may simply be:
public interface IOrderRepository
{
Task<Order?> GetByIdAsync(
Guid id,
CancellationToken cancellationToken);
void Add(Order order);
}This reinforces an important principle:
Repositories should follow domain boundaries, not database tables.
Creating one repository for every table often leads us back toward database-driven architecture rather than domain-driven design.
Be Careful with IQueryable
Another common repository design is:
IQueryable<Order> Query();This initially seems flexible.
A consumer can now do:
IQueryable<Order> query = repository.Query();
Order? order = await query
.Include(x => x.Items)
.Where(x => x.Status == OrderStatus.Pending)
.FirstOrDefaultAsync();But this introduces another problem.
The repository abstraction now exposes the query provider.
Application code effectively becomes coupled to EF Core-style querying again.
The repository is no longer controlling persistence behavior.
Different handlers may start constructing slightly different versions of the same business rule.
One handler might correctly filter pending orders.
Another might forget the condition.
Another might load a different set of navigation properties.
Over time, persistence rules become scattered across the application.
This defeats much of the reason we introduced the repository in the first place.
When repositories are used, I prefer intent-specific operations
rather than exposing unrestricted IQueryable.
CQRS Changes the Repository Discussion
The Repository Pattern becomes particularly interesting when CQRS is introduced.
CQRS separates application operations into two categories:
Commands → Change state
Queries → Read state
These two workloads have different requirements.
Commands usually work with domain behavior and consistency.
Queries usually care about efficient data retrieval.
Because of that, they do not necessarily need the same persistence strategy.
A practical architecture might look like this:
Command
↓
Command Handler
↓
Domain Aggregate
↓
Repository
↓
Unit of Work
↓
Database
While the query side might look like:
Query
↓
Query Handler
↓
EF Core / Dapper
↓
Projection
↓
DTO
This is an important architectural distinction.
We do not have to force every read through a repository simply because repositories are used on the write side.
Repositories Are Often More Valuable for Commands
Commands usually change domain state.
For example:
Create Order
Cancel Order
Approve Payment
Complete Shipment
A command handler may load an aggregate:
Order? order = await orderRepository
.GetByIdAsync(orderId, cancellationToken);Then execute domain behavior:
order.Cancel();And finally persist the transaction:
await unitOfWork.SaveChangesAsync(
cancellationToken);Here, the repository provides a meaningful persistence boundary around the domain aggregate.
That makes sense.
The application is not simply executing database queries.
It is loading domain state, executing business behavior, and persisting the resulting changes.
Queries Are Different
Query handlers usually do not need full aggregates.
Their goal is often to return information efficiently.
For example:
OrderDetailsResponse? order =
await dbContext.Orders
.AsNoTracking()
.Where(x => x.Id == request.OrderId)
.Select(x => new OrderDetailsResponse
{
Id = x.Id,
CustomerName = x.Customer.Name,
TotalAmount = x.TotalAmount,
Status = x.Status
})
.FirstOrDefaultAsync(cancellationToken);This query does not need to reconstruct the entire Order
aggregate.
It simply retrieves the exact data required by the API.
Adding a repository abstraction here might produce unnecessary complexity.
For read-heavy workloads, direct projections through EF Core or tools such as Dapper can often be simpler and more efficient.
This leads to a practical rule I find useful:
Use repositories for domain-oriented writes, and use optimized query models for reads.
This is not a universal rule.
But it is a useful default when combining CQRS, Clean Architecture, EF Core, and Domain-Driven Design.
Unit of Work Still Matters
Another concept frequently associated with repositories is the Unit of Work pattern.
The Unit of Work coordinates multiple persistence operations and commits them as one transaction.
EF Core’s DbContext already behaves similarly to a Unit
of Work.
For example:
orderRepository.Add(order);
paymentRepository.Add(payment);
await unitOfWork.SaveChangesAsync(
cancellationToken);The important idea is that repositories generally should not
independently call SaveChanges() every time an entity
changes.
For example, I would avoid designing repositories like this:
await orderRepository.AddAndSaveAsync(order);because it makes transaction boundaries harder to control.
Suppose one operation needs to:
Create Order
Create Payment Record
Update Inventory
Write Outbox Message
These changes may need to succeed or fail together.
One transaction boundary makes that behavior much easier to reason about.
Don’t Add Repositories Just to Replace EF Core Someday
Another argument often used for repositories is:
“If we use repositories, we can easily replace EF Core later.”
Technically, repositories can reduce some direct dependencies.
But replacing an ORM is rarely as simple as swapping one implementation.
Different persistence technologies have different:
- query capabilities
- transaction semantics
- concurrency models
- relationship handling
- performance characteristics
- mapping strategies
Changing from EF Core to another ORM or database usually requires more than replacing a repository implementation.
Because of that, I do not consider hypothetical ORM replacement alone a strong reason to introduce repositories.
Architectural abstractions should usually solve a problem we actually have.
The Cost of Abstractions
Every abstraction has a maintenance cost.
Adding repositories may introduce:
Repository interface
Repository implementation
Dependency injection registration
Mocks or test substitutes
Additional method definitions
Additional navigation between files
That cost may be completely justified.
But we should expect something valuable in return.
A useful repository might give us:
- domain boundaries
- consistent aggregate loading
- ownership rules
- security rules
- transaction consistency
- persistence encapsulation
- clearer command-side architecture
If we are not getting those benefits, the repository may simply be adding another layer.
Avoid the Generic Repository Trap
One pattern I now approach carefully is the universal generic repository.
For example:
IRepository<T>used by every domain object.
It certainly reduces duplicated CRUD code.
But reducing duplicated CRUD code is not necessarily the same as improving architecture.
Different aggregates often have very different retrieval requirements.
For example:
Order
Customer
Invoice
Subscription
Each may have different domain rules, relationships, ownership rules, and consistency requirements.
Trying to squeeze all of them into:
GetById()
GetAll()
Add()
Update()
Delete()can hide meaningful differences.
Sometimes a small amount of explicit code produces a much clearer architecture.
My Practical Rule
When deciding whether to introduce a repository, I now ask:
What architectural boundary is this repository protecting?
If the answer is only:
“I don’t want the application to see DbContext.”
that may not be enough.
But if the answer is:
“This repository controls how an aggregate is loaded, protects important business constraints, and defines a clear persistence boundary.”
then the abstraction probably has real value.
My preferred mental model looks something like this:
Commands
↓
Domain / Aggregates
↓
Repositories
↓
Unit of Work
↓
EF Core
while reads can remain:
Queries
↓
EF Core / Dapper
↓
Projection
↓
Response DTO
This keeps the architecture intentional without forcing every database operation through the same abstraction.
Patterns Are Tools, Not Rules
Perhaps the bigger lesson here is not really about repositories.
It is about architectural patterns in general.
Clean Architecture, CQRS, Domain-Driven Design, Repository Pattern, Unit of Work, Mediator, Outbox, Specification Pattern, and similar techniques are tools.
They solve particular problems.
The goal should not be to use as many patterns as possible.
The goal should be to create software that is:
- understandable
- maintainable
- testable
- secure
- evolvable
Sometimes that requires another abstraction.
Sometimes the cleaner architecture is the one with fewer abstractions.
The challenge is learning to recognize the difference.
Final Thoughts
The Repository Pattern is neither obsolete nor mandatory when using Entity Framework Core.
Used poorly, it can become another layer that merely duplicates EF Core.
Used intentionally, it can provide a valuable boundary around domain aggregates and application behavior.
The distinction comes down to purpose.
Instead of asking:
Should every EF Core application use repositories?
I think a better question is:
What problem would a repository solve in this part of my architecture?
If there is a clear answer, use it.
If there isn’t, EF Core may already provide everything you need.
Further Learning
A significant part of how I approach these architectural decisions comes from continuously studying experienced .NET developers and software architects, including Milan Jovanović’s material on Clean Architecture, Domain-Driven Design, CQRS, EF Core, and modern .NET application architecture.
If you want to explore these topics in more depth, you can check out Milan Jovanović’s courses here:
Milan Jovanović — Modular Monolith Architecture
Milan Jovanović — Pragmatic REST APIs
Milan Jovanović — Pragmatic Clean Architecture
Disclosure: Some links on this page may be affiliate links. If you purchase through one of these links, I may receive a commission at no additional cost to you. The opinions and architectural conclusions in this article are my own and are based on my learning and practical experience.
