The Repository pattern is one of those patterns that generates debate. Some say it’s essential, others call it unnecessary abstraction over EF Core. Here’s my pragmatic take on when and how to use it.
What is the Repository Pattern?
A repository abstracts data access, providing a collection-like interface to your domain objects. Instead of calling DbContext directly, you work with a repository:
// Without repository
var user = await _context.Users.FindAsync(id);
// With repository
var user = await _userRepository.GetByIdAsync(id);
Basic Implementation
public interface IRepository<T> where T : class
{
Task<T> GetByIdAsync(int id);
Task<IEnumerable<T>> GetAllAsync();
Task AddAsync(T entity);
void Update(T entity);
void Delete(T entity);
}
public class Repository<T> : IRepository<T> where T : class
{
protected readonly DbContext _context;
protected readonly DbSet<T> _dbSet;
public Repository(DbContext context)
{
_context = context;
_dbSet = context.Set<T>();
}
public async Task<T> GetByIdAsync(int id) => await _dbSet.FindAsync(id);
public async Task<IEnumerable<T>> GetAllAsync() => await _dbSet.ToListAsync();
public async Task AddAsync(T entity) => await _dbSet.AddAsync(entity);
public void Update(T entity) => _dbSet.Update(entity);
public void Delete(T entity) => _dbSet.Remove(entity);
}
Specific Repositories
Generic repositories are a starting point. Real value comes from domain-specific methods:
public interface IOrderRepository : IRepository<Order>
{
Task<IEnumerable<Order>> GetOrdersByCustomerAsync(int customerId);
Task<Order> GetOrderWithItemsAsync(int orderId);
Task<decimal> GetTotalRevenueAsync(DateTime from, DateTime to);
}
public class OrderRepository : Repository<Order>, IOrderRepository
{
public OrderRepository(AppDbContext context) : base(context) { }
public async Task<Order> GetOrderWithItemsAsync(int orderId)
{
return await _dbSet
.Include(o => o.Items)
.FirstOrDefaultAsync(o => o.Id == orderId);
}
}
When to Use It
- Use it when you need to abstract data access for testing
- Use it when you might change ORMs (rare but happens)
- Skip it for simple CRUD apps where EF is tightly coupled anyway
- Skip it if you’re just wrapping DbContext methods 1:1
Unit of Work
Pair Repository with Unit of Work to manage transactions:
public interface IUnitOfWork : IDisposable
{
IOrderRepository Orders { get; }
ICustomerRepository Customers { get; }
Task<int> SaveChangesAsync();
}
References
Discover more from C4: Container, Code, Cloud & Context
Subscribe to get the latest posts sent to your email.