# Repository Pattern in C# With Entity Framework Core

> The repository pattern is one of the most debated patterns in .NET. This guide shows a clean EF Core implementation, explains why generic repositories backfire, and tells you exactly when to skip the pattern entirely.

Published: 2026-08-30. Author: Milan Jovanović.

Canonical: https://milanjovanovic.tech/blog/repository-pattern-csharp

EF Core already abstracts the database, tracks identity, and coordinates writes.
Adding a repository can still protect an aggregate boundary, but a generic CRUD wrapper usually hides useful query capabilities without creating a meaningful seam.
The decision should follow the domain boundary, not a rule that every `DbSet` needs an interface.

## What Is the Repository Pattern?

The **Repository pattern** mediates between the domain and data mapping layers.
It provides a collection-like interface for accessing domain objects, hiding the details of how data is persisted or retrieved.

In simpler terms: instead of your business logic talking directly to the database, it talks to a repository.
The repository handles the data access behind the scenes.

```csharp
public interface IOrderRepository
{
    Task<Order?> GetByIdAsync(int id, CancellationToken cancellationToken = default);
    Task<List<Order>> GetByCustomerAsync(int customerId, CancellationToken cancellationToken = default);
    void Add(Order order);
    void Remove(Order order);
}
```

The caller doesn't know (or care) whether you're using EF Core, Dapper, or a flat file.
That's the whole point.

## Why Use the Repository Pattern?

There are a few practical reasons to add repositories on top of EF Core:

**1. Abstraction over data access** - Your domain and application layers don't depend on `DbContext`. If you're following [**Clean Architecture**](https://milanjovanovic.tech/blog/clean-architecture-and-the-benefits-of-structured-software-design), this keeps the [**Dependency Rule**](https://milanjovanovic.tech/blog/clean-architecture-folder-structure) intact.

**2. Testability** - You can mock `IOrderRepository` in unit tests without setting up a database. Testing [**use cases in Clean Architecture**](https://milanjovanovic.tech/blog/unit-testing-clean-architecture-use-cases) becomes straightforward. For the repository implementations themselves, use real databases in **integration tests**.

**3. Encapsulation of query logic** - Complex queries live inside the repository, not scattered across your application. This makes them easier to find, optimize, and reuse.

**4. Consistent data access patterns** - Repositories give your team a clear pattern to follow. Every developer knows where data access code lives.

## Implementing a Repository With EF Core

Here's a concrete implementation of the `IOrderRepository`:

```csharp
public class OrderRepository : IOrderRepository
{
    private readonly AppDbContext _dbContext;

    public OrderRepository(AppDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public async Task<Order?> GetByIdAsync(
        int id,
        CancellationToken cancellationToken = default)
    {
        return await _dbContext.Orders
            .Include(o => o.LineItems)
            .FirstOrDefaultAsync(o => o.Id == id, cancellationToken);
    }

    public async Task<List<Order>> GetByCustomerAsync(
        int customerId,
        CancellationToken cancellationToken = default)
    {
        return await _dbContext.Orders
            .Where(o => o.CustomerId == customerId)
            .OrderByDescending(o => o.CreatedAt)
            .ToListAsync(cancellationToken);
    }

    public void Add(Order order)
    {
        _dbContext.Orders.Add(order);
    }

    public void Remove(Order order)
    {
        _dbContext.Orders.Remove(order);
    }
}
```

Notice that `Add` and `Remove` don't call `SaveChanges`.
That's intentional - saving is the responsibility of the [**Unit of Work**](https://milanjovanovic.tech/blog/working-with-transactions-in-ef-core), not the repository.

## The Unit of Work Pattern

Repositories handle individual aggregate persistence.
The **Unit of Work** coordinates saving changes across multiple repositories in a single transaction.

EF Core's `DbContext` already implements the Unit of Work pattern internally.
But exposing a clean interface keeps your code decoupled (I cover the full pattern in [**Unit of Work with EF Core**](https://milanjovanovic.tech/blog/unit-of-work-pattern-ef-core)):

```csharp
public interface IUnitOfWork
{
    Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
}
```

```csharp
public class UnitOfWork : IUnitOfWork
{
    private readonly AppDbContext _dbContext;

    public UnitOfWork(AppDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
    {
        return await _dbContext.SaveChangesAsync(cancellationToken);
    }
}
```

Now your use case can orchestrate multiple repositories and save once:

```csharp {11}
public async Task Handle(PlaceOrderCommand command, CancellationToken cancellationToken)
{
    var customer = await _customerRepository.GetByIdAsync(command.CustomerId, cancellationToken);

    var order = Order.Create(customer, command.Items);

    _orderRepository.Add(order);

    customer.IncrementOrderCount();

    await _unitOfWork.SaveChangesAsync(cancellationToken);
}
```

## The Generic Repository Debate

You'll find many tutorials suggesting a generic repository:

```csharp
public interface IRepository<T> where T : class
{
    Task<T?> GetByIdAsync(int id);
    Task<List<T>> GetAllAsync();
    void Add(T entity);
    void Update(T entity);
    void Remove(T entity);
}
```

I'd advise against this approach for most projects. Here's why:

**It's a leaky abstraction.** You end up exposing methods that don't make sense for every entity. Should you really be able to call `GetAll()` on a table with millions of rows?

**It pushes query logic into the wrong place.** Callers end up writing LINQ against `IQueryable<T>`, which defeats the purpose of the repository.

**It mirrors DbSet.** If your generic repository just wraps `DbSet<T>`, you haven't gained anything meaningful - you've just added a layer of indirection.

**Better alternative:** Write specific repository interfaces per aggregate root. Each interface exposes only the operations that make sense for that aggregate.

```csharp
// ✅ Specific repository - clear intent
public interface IOrderRepository
{
    Task<Order?> GetByIdAsync(int id);
    Task<List<Order>> GetPendingOrdersAsync();
    void Add(Order order);
}

// ❌ Generic repository - unclear intent
public interface IRepository<T> where T : class
{
    Task<T?> GetByIdAsync(int id);
    Task<List<T>> GetAllAsync(); // Millions of rows?
    void Add(T entity);
    void Update(T entity); // EF Core tracks changes automatically
    void Remove(T entity);
}
```

## Should Repositories Return IQueryable?

A common shortcut is exposing `IQueryable<T>` from the repository:

```csharp
// ❌ Avoid this
public interface IOrderRepository
{
    IQueryable<Order> Orders { get; }
}
```

It looks flexible, but it defeats the purpose of the pattern:

**The abstraction leaks.** Callers can compose any query, including ones that don't translate to SQL. The exception surfaces far from the code that caused it, at enumeration time.

**Query logic scatters.** The whole point was to centralize data access. With `IQueryable`, every handler writes its own `Include`, filtering, and paging logic.

**You can't test the contract.** An in-memory `IQueryable` behaves differently from the EF Core provider (case sensitivity, null handling, unsupported translations). Your mocks pass while production fails.

Return materialized results (`List<T>`, `T?`) or accept a specification object instead.
If a handler needs a truly one-off query, that's a sign it should use `DbContext` directly - and that's fine.

## When to Skip the Repository Pattern

The repository pattern isn't always necessary. Skip it when:

**You're building a simple CRUD app.** If your app mostly does basic create-read-update-delete operations, the repository adds overhead without meaningful benefit. Just use `DbContext` directly.

**You're using Vertical Slice Architecture.** In [**VSA**](https://milanjovanovic.tech/blog/vertical-slice-architecture), each slice owns its data access. Adding a repository per slice is over-engineering - your handler _is_ the data access boundary.

**You'll never swap your ORM.** The "what if we switch from EF Core to Dapper" argument rarely materializes. If you're committed to EF Core, the abstraction may not justify its cost.

**Your team is small and co-located.** Conventions and code reviews can enforce consistency without a formal pattern.

## Repository Pattern in Clean Architecture

In Clean Architecture, repository interfaces live in the **Domain layer** (or Application layer) and implementations live in the **Infrastructure layer**.

![The application handler depends on the IOrderRepository interface in the Domain layer, while the OrderRepository EF Core implementation in the Infrastructure layer implements that interface](https://milanjovanovic.tech/blogs/articles/repository-pattern-csharp/clean-architecture-layers.png)

```text
Domain/
  Entities/
    Order.cs
  Repositories/
    IOrderRepository.cs      ← Interface here

Infrastructure/
  Repositories/
    OrderRepository.cs        ← Implementation here
```

This keeps your domain free of EF Core dependencies while still allowing rich data access behind the scenes.

Register the repository in your DI container:

```csharp
builder.Services.AddScoped<IOrderRepository, OrderRepository>();
builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();
```

Or use [**Scrutor**](https://milanjovanovic.tech/blog/improving-aspnetcore-dependency-injection-with-scrutor) for automatic registration by convention.

## Repository Pattern With the Specification Pattern

For complex query scenarios, combine repositories with the [**Specification pattern**](https://www.martinfowler.com/apsupp/spec.pdf).
Specifications encapsulate query criteria into reusable objects:

```csharp
public class PendingOrdersSpecification : Specification<Order>
{
    public override Expression<Func<Order, bool>> ToExpression()
    {
        return order => order.Status == OrderStatus.Pending
                     && order.CreatedAt >= DateTime.UtcNow.AddDays(-30);
    }
}
```

The abstract `Specification<T>` base class only needs to declare the `ToExpression` method.
The repository applies the specification to the query:

```csharp
public async Task<List<Order>> GetAsync(
    Specification<Order> specification,
    CancellationToken cancellationToken = default)
{
    return await _dbContext.Orders
        .Where(specification.ToExpression())
        .ToListAsync(cancellationToken);
}
```

This avoids query logic leaking out of the repository while keeping things flexible.

## Summary

The repository pattern is a useful abstraction when:

- You're following Clean Architecture and need to enforce the Dependency Rule
- You want to isolate query logic in a consistent, testable way
- Your domain has complex data access requirements beyond simple CRUD

Skip it when:

- You're building simple CRUD applications
- You're using Vertical Slice Architecture where each handler owns its data access
- The abstraction adds complexity without meaningful benefit

Avoid generic CRUD repositories.
When a repository protects a real aggregate boundary, expose only the operations and query intent that boundary needs.
EF Core already supplies a unit of work and identity map, so the repository should organize domain-facing access rather than recreate the ORM.

## Frequently asked questions

### Is the repository pattern still needed with EF Core?

It depends on your architecture. EF Core's DbContext already implements the Unit of Work and Repository patterns internally, so for simple CRUD apps a repository adds little. It earns its place when you follow Clean Architecture, need to keep EF Core out of your domain layer, or want to encapsulate complex query logic per aggregate.

### Should I use a generic repository with EF Core?

Generally no. A generic repository ends up mirroring DbSet, exposes methods that make no sense for some entities (like GetAll on a huge table), and pushes query logic to callers. Specific repository interfaces per aggregate root with intention-revealing methods work better.

### Should a repository call SaveChanges?

No. Repositories should only add, remove, and query entities. Committing is the Unit of Work's job, which lets a single use case modify several aggregates through different repositories and save them in one transaction.

### Should repositories return IQueryable?

Avoid it. Returning IQueryable leaks the query provider to callers, spreads data access logic across the codebase, and makes it impossible to test the repository contract in isolation. Return materialized results or accept a specification instead.

### Where do repository interfaces belong in Clean Architecture?

Interfaces live in the Domain layer (or Application layer), next to the aggregates they persist. Implementations live in the Infrastructure layer, where the EF Core dependency is allowed.
