Repository & UoW

Repository and Unit of Work

The Repository pattern provides a collection-like interface for accessing domain objects, hiding the persistence mechanism from the domain layer. The Unit of Work pattern tracks all changes made during a business operation and commits them as a single atomic transaction. Together they decouple domain logic from data access technology and make persistence testable.

In EF Core, DbContext already implements both patterns: it acts as a repository-like gateway (you query through DbSet<T>) and as a Unit of Work (change tracking + SaveChangesAsync()). Wrapping EF Core in additional Repository/UoW abstractions is optional — justified when you need to swap persistence technology or enforce strict domain boundaries.

Repository Pattern

A Repository exposes domain-oriented methods (FindById, FindByCustomer, Save) rather than raw SQL or LINQ. The domain layer depends on the interface; the infrastructure layer provides the implementation.

// Domain layer: depends on abstraction
public interface IOrderRepository
{
    Task<Order?> FindAsync(OrderId id, CancellationToken ct);
    Task<IReadOnlyList<Order>> FindByCustomerAsync(CustomerId customerId, CancellationToken ct);
    void Add(Order order);
    void Remove(Order order);
}

// Infrastructure layer: EF Core implementation
public sealed class EfOrderRepository(AppDbContext db) : IOrderRepository
{
    public Task<Order?> FindAsync(OrderId id, CancellationToken ct) =>
        db.Orders
          .Include(o => o.LineItems)
          .FirstOrDefaultAsync(o => o.Id == id, ct);

    public Task<IReadOnlyList<Order>> FindByCustomerAsync(CustomerId customerId, CancellationToken ct) =>
        db.Orders
          .Where(o => o.CustomerId == customerId)
          .ToListAsync(ct)
          .ContinueWith(t => (IReadOnlyList<Order>)t.Result, ct);

    public void Add(Order order)    => db.Orders.Add(order);
    public void Remove(Order order) => db.Orders.Remove(order);
}

Note: Add and Remove don't call SaveChanges — that's the Unit of Work's responsibility.

Unit of Work Pattern

The Unit of Work tracks all changes within a business operation and commits them atomically. In EF Core, DbContext is the Unit of Work:

public interface IUnitOfWork
{
    Task<int> SaveChangesAsync(CancellationToken ct);
}

// AppDbContext implements both IUnitOfWork and exposes repositories
public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
    : DbContext(options), IUnitOfWork
{
    public DbSet<Order> Orders => Set<Order>();
}

// Application service: uses repository + unit of work
public sealed class PlaceOrderHandler(IOrderRepository orders, IUnitOfWork uow)
{
    public async Task HandleAsync(PlaceOrderCommand cmd, CancellationToken ct)
    {
        var order = Order.Create(cmd.CustomerId, cmd.LineItems);
        orders.Add(order);
        await uow.SaveChangesAsync(ct);  // single transaction for all changes
    }
}

When to Add the Abstraction

EF Core's DbContext already gives you Repository + UoW behavior. Adding explicit interfaces is justified when:

When NOT to add the abstraction: if you're building a simple CRUD service and the only consumer is EF Core, the extra interfaces add indirection without benefit. Inject DbContext directly.

Pitfalls

Repository That Returns IQueryable<T>

What goes wrong: the repository leaks EF Core's IQueryable<T> to the application layer. Callers add .Where() and .Include() outside the repository, coupling the application layer to EF Core.

Why it happens: returning IQueryable<T> feels flexible — callers can filter however they want.

Mitigation: return IReadOnlyList<T> or IEnumerable<T>. Add specific query methods to the repository interface (FindByCustomer, FindPendingOlderThan) rather than exposing raw queryable.

Generic Repository Anti-Pattern

What goes wrong: a single IRepository<T> with GetById, GetAll, Add, Update, Delete is used for every entity. It forces every aggregate to expose the same interface, including operations that don't make sense for that aggregate.

Why it happens: it looks like a clean abstraction and reduces boilerplate.

Mitigation: use aggregate-specific repositories (IOrderRepository, ICustomerRepository) with methods that reflect the domain's actual access patterns. Generic repositories are fine as a base implementation, but the interface should be domain-specific.

Tradeoffs

Approach Strengths Weaknesses When to use
Direct DbContext Simple, no extra abstraction, full EF Core power Couples application layer to EF Core, harder to unit-test Simple CRUD, small teams, no domain isolation requirement
Repository + UoW interfaces Testable, domain-isolated, swappable persistence Extra boilerplate, risk of leaky abstractions DDD projects, strict layering, multiple persistence backends

Decision rule: start with direct DbContext injection. Add Repository/UoW interfaces when you need to unit-test application services without a real database, or when the domain layer must not reference EF Core. Don't add the abstraction speculatively.

Questions

[!QUESTION]- When is a generic IRepository an anti-pattern?

References


Whats next

Parent
05 Architecture

Topics

Pages

Connected Pages
Repository & UoW
Repository and Unit of Work
  • Repository Pattern
  • Unit of Work Pattern
  • When to Add the Abstraction
  • Pitfalls Repository That Returns IQueryable
  • Generic Repository Anti-Pattern
  • Tradeoffs
  • Questions
  • References