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:

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:

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:

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:

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:

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.