← Back to all posts

Repository Pattern with Entity Framework Core

Repository Pattern with Entity Framework Core

Reading Time: 5 minutes

The Problem

When you first start building an ASP.NET Core application with Entity Framework Core, it’s tempting to inject DbContext directly into your services and controllers. It works, and it’s simple:

// ❌ Problematic: DbContext directly in service
public class OrderService : IOrderService
{
    private readonly AppDbContext _db;

    public OrderService(AppDbContext db)
    {
        _db = db;
    }

    public async Task<List<Order>> GetOrdersForUserAsync(int userId)
    {
        return await _db.Orders
            .Where(o => o.UserId == userId && !o.IsDeleted)
            .Include(o => o.Items)
            .OrderByDescending(o => o.CreatedAt)
            .ToListAsync();
    }

    public async Task<Order> CreateOrderAsync(CreateOrderDto dto)
    {
        // Complex EF Core query logic mixed with business logic
        var order = new Order { UserId = dto.UserId, CreatedAt = DateTime.UtcNow };
        _db.Orders.Add(order);
        await _db.SaveChangesAsync();
        return order;
    }
}

This approach has several problems:

  • Hard to unit test - you can’t easily mock DbContext without an in-memory database
  • Duplicated queries - the same LINQ query (e.g., “active orders for user”) gets copy-pasted everywhere
  • Leaked data layer concerns - business logic is mixed with EF Core specifics
  • No abstraction - switching from EF Core to Dapper or another ORM requires touching every service

The Solution

The Repository Pattern abstracts data access behind interfaces. Your services depend on IOrderRepository, not on AppDbContext. This means:

  • Services can be unit tested with mock repositories
  • Data access logic lives in one place
  • Switching the data layer doesn’t affect business logic

Defining the IRepository Interface

Start with a generic repository interface that covers common CRUD operations:

public interface IRepository<T> where T : class
{
    Task<T?> GetByIdAsync(int id);
    Task<IEnumerable<T>> GetAllAsync();
    Task<T> AddAsync(T entity);
    Task UpdateAsync(T entity);
    Task DeleteAsync(T entity);
    Task<bool> ExistsAsync(int id);
}

Implementing the Generic Repository

Create a base implementation using EF Core:

public class Repository<T> : IRepository<T> where T : class
{
    protected readonly AppDbContext _context;
    protected readonly DbSet<T> _dbSet;

    public Repository(AppDbContext context)
    {
        _context = context;
        _dbSet = context.Set<T>();
    }

    public async Task<T?> GetByIdAsync(int id)
    {
        return await _dbSet.FindAsync(id);
    }

    public async Task<IEnumerable<T>> GetAllAsync()
    {
        return await _dbSet.ToListAsync();
    }

    public async Task<T> AddAsync(T entity)
    {
        await _dbSet.AddAsync(entity);
        return entity;
    }

    public async Task UpdateAsync(T entity)
    {
        _dbSet.Update(entity);
    }

    public async Task DeleteAsync(T entity)
    {
        _dbSet.Remove(entity);
    }

    public async Task<bool> ExistsAsync(int id)
    {
        return await _dbSet.FindAsync(id) is not null;
    }
}

Creating Specific Repositories

Extend the generic repository with domain-specific methods:

// Interface with domain-specific queries
public interface IOrderRepository : IRepository<Order>
{
    Task<IEnumerable<Order>> GetOrdersForUserAsync(int userId);
    Task<IEnumerable<Order>> GetPendingOrdersAsync();
    Task<Order?> GetOrderWithItemsAsync(int orderId);
}

// Implementation
public class OrderRepository : Repository<Order>, IOrderRepository
{
    public OrderRepository(AppDbContext context) : base(context) { }

    public async Task<IEnumerable<Order>> GetOrdersForUserAsync(int userId)
    {
        return await _dbSet
            .Where(o => o.UserId == userId && !o.IsDeleted)
            .Include(o => o.Items)
            .OrderByDescending(o => o.CreatedAt)
            .ToListAsync();
    }

    public async Task<IEnumerable<Order>> GetPendingOrdersAsync()
    {
        return await _dbSet
            .Where(o => o.Status == OrderStatus.Pending)
            .Include(o => o.Customer)
            .ToListAsync();
    }

    public async Task<Order?> GetOrderWithItemsAsync(int orderId)
    {
        return await _dbSet
            .Where(o => o.Id == orderId)
            .Include(o => o.Items)
            .ThenInclude(i => i.Product)
            .FirstOrDefaultAsync();
    }
}

The Unit of Work Pattern

Repositories alone don’t coordinate transactions. The Unit of Work pattern wraps multiple repositories and provides a single SaveChangesAsync() call:

// Unit of Work interface
public interface IUnitOfWork : IDisposable
{
    IOrderRepository Orders { get; }
    IProductRepository Products { get; }
    ICustomerRepository Customers { get; }
    Task<int> SaveChangesAsync();
}

// Implementation
public class UnitOfWork : IUnitOfWork
{
    private readonly AppDbContext _context;

    public IOrderRepository Orders { get; }
    public IProductRepository Products { get; }
    public ICustomerRepository Customers { get; }

    public UnitOfWork(AppDbContext context)
    {
        _context = context;
        Orders = new OrderRepository(context);
        Products = new ProductRepository(context);
        Customers = new CustomerRepository(context);
    }

    public async Task<int> SaveChangesAsync()
    {
        return await _context.SaveChangesAsync();
    }

    public void Dispose()
    {
        _context.Dispose();
    }
}

Registering in DI Container

Wire everything up in Program.cs:

builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(connectionString));

// Option 1: Register individual repositories
builder.Services.AddScoped<IOrderRepository, OrderRepository>();
builder.Services.AddScoped<IProductRepository, ProductRepository>();

// Option 2: Register Unit of Work (preferred)
builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();

Using Repositories in Services

Your service layer now depends only on abstractions:

public class OrderService : IOrderService
{
    private readonly IUnitOfWork _unitOfWork;
    private readonly ILogger<OrderService> _logger;

    public OrderService(IUnitOfWork unitOfWork, ILogger<OrderService> logger)
    {
        _unitOfWork = unitOfWork;
        _logger = logger;
    }

    public async Task<Order> CreateOrderAsync(int userId, List<OrderItemDto> items)
    {
        var customer = await _unitOfWork.Customers.GetByIdAsync(userId)
            ?? throw new NotFoundException($"Customer {userId} not found");

        var order = new Order
        {
            CustomerId = userId,
            CreatedAt = DateTime.UtcNow,
            Status = OrderStatus.Pending
        };

        await _unitOfWork.Orders.AddAsync(order);
        await _unitOfWork.SaveChangesAsync(); // Single save for the whole operation

        _logger.LogInformation("Created order {OrderId} for customer {CustomerId}", order.Id, userId);
        return order;
    }
}

Testing with Mock Repositories

The real payoff: clean unit tests without a database:

public class OrderServiceTests
{
    [Fact]
    public async Task CreateOrderAsync_WhenCustomerExists_ReturnsNewOrder()
    {
        // Arrange
        var mockUnitOfWork = new Mock<IUnitOfWork>();
        var mockOrders = new Mock<IOrderRepository>();
        var mockCustomers = new Mock<ICustomerRepository>();

        var customer = new Customer { Id = 1, Name = "Jane Doe" };

        mockCustomers.Setup(r => r.GetByIdAsync(1)).ReturnsAsync(customer);
        mockOrders.Setup(r => r.AddAsync(It.IsAny<Order>()))
                  .ReturnsAsync((Order o) => o);
        mockUnitOfWork.Setup(u => u.Customers).Returns(mockCustomers.Object);
        mockUnitOfWork.Setup(u => u.Orders).Returns(mockOrders.Object);
        mockUnitOfWork.Setup(u => u.SaveChangesAsync()).ReturnsAsync(1);

        var logger = new NullLogger<OrderService>();
        var service = new OrderService(mockUnitOfWork.Object, logger);

        // Act
        var result = await service.CreateOrderAsync(1, new List<OrderItemDto>());

        // Assert
        Assert.NotNull(result);
        Assert.Equal(OrderStatus.Pending, result.Status);
        mockUnitOfWork.Verify(u => u.SaveChangesAsync(), Times.Once);
    }
}

No database connection, no in-memory EF setup, no slow I/O - just fast, isolated unit tests.

Summary

The Repository Pattern with EF Core gives you:

  • Testability - mock your repositories in unit tests with no database required
  • Separation of concerns - data access logic stays in repositories, business logic stays in services
  • Single source of truth - complex queries are defined once and reused
  • Flexibility - swap out EF Core for Dapper or another ORM by changing only the repository implementations

The Unit of Work pattern complements repositories by coordinating transactions across multiple repositories in a single operation. Together, they form a clean and maintainable data access layer for any ASP.NET Core application.

References