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
DbContextwithout 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
DbContextlifetime and configuration: https://learn.microsoft.com/ef/core/dbcontext-configuration/- Repository pattern: https://martinfowler.com/eaaCatalog/repository.html