Repository and Unit of Work over EF Core: per-aggregate repositories vs generic IRepository<T>

.NET Architecture & Design Patterns November 13, 2026 csharp

DbContext is already a Unit of Work and DbSet is already a repository.

csharp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// ======================================================================
// Q: Repository and Unit of Work on top of EF Core: yes or no?
//
// DbContext is already a Unit of Work and DbSet is already a repository. A
// generic IRepository<T> that exposes IQueryable adds nothing. A repository per
// aggregate root (DDD) is worthwhile, though: it always loads the whole
// aggregate and keeps the domain independent of EF.
// For reads (lists, reports) you don't need a repository: DbContext with
// AsNoTracking() and projection to a DTO (Q1). For reusable query criteria there
// is the Specification pattern (e.g. Ardalis.Specification).
// ======================================================================

public interface IOrderRepository                        // in Domain / Application
{
    Task<Order?> GetAsync(int id, CancellationToken ct);
    void Add(Order order);
}
public interface IUnitOfWork { Task<int> SaveChangesAsync(CancellationToken ct = default); }

public class EfOrderRepository(AppDbContext db) : IOrderRepository   // in Infrastructure
{
    public Task<Order?> GetAsync(int id, CancellationToken ct) =>
        db.Orders.Include(o => o.Lines).FirstOrDefaultAsync(o => o.Id == id, ct);

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

public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options), IUnitOfWork
{
    public DbSet<Order> Orders => Set<Order>();
}

builder.Services.AddScoped<IOrderRepository, EfOrderRepository>();
builder.Services.AddScoped<IUnitOfWork>(sp => sp.GetRequiredService<AppDbContext>());  // same scoped instance