Explain the Unit of Work and Repository patterns.
Quick Answer
The Repository pattern abstracts data access behind a collection-like interface, decoupling domain logic from EF Core. The Unit of Work pattern coordinates multiple repositories and commits them in a single transaction (one `SaveChanges`). Note that EF Core's `DbContext` already implements both — `DbSet` is a repository and `SaveChanges` is a unit of work — so extra layers add value mainly for testability or strict abstraction, not by default.
Detailed Answer
Repository Pattern Abstracts data access logic and provides a collection-like interface for accessing domain objects.
public interface IRepository where T : class
{
Task GetByIdAsync(int id);
Task<IEnumerable> GetAllAsync();
Task<IEnumerable> FindAsync(Expression<Func> predicate);
Task AddAsync(T entity);
void Update(T entity);
void Remove(T entity);
}
public class Repository : IRepository where T : class
{
protected readonly DbContext _context;
protected readonly DbSet _dbSet;
public Repository(DbContext context)
{
_context = context;
_dbSet = context.Set();
}
public async Task GetByIdAsync(int id)
{
return await _dbSet.FindAsync(id);
}
public async Task<IEnumerable> GetAllAsync()
{
return await _dbSet.ToListAsync();
}
public async Task<IEnumerable> FindAsync(Expression<Func> predicate)
{
return await _dbSet.Where(predicate).ToListAsync();
}
public async Task AddAsync(T entity)
{
await _dbSet.AddAsync(entity);
}
public void Update(T entity)
{
_dbSet.Update(entity);
}
public void Remove(T entity)
{
_dbSet.Remove(entity);
}
}
Unit of Work Pattern Maintains a list of objects affected by a business transaction and coordinates writing changes to the database.
public interface IUnitOfWork : IDisposable
{
IRepository Products { get; }
IRepository Categories { get; }
IRepository Orders { get; }
Task SaveChangesAsync();
Task BeginTransactionAsync();
Task CommitTransactionAsync();
Task RollbackTransactionAsync();
}
public class UnitOfWork : IUnitOfWork
{
private readonly AppDbContext _context;
private IDbContextTransaction _transaction;
public UnitOfWork(AppDbContext context)
{
_context = context;
Products = new Repository(_context);
Categories = new Repository(_context);
Orders = new Repository(_context);
}
public IRepository Products { get; private set; }
public IRepository Categories { get; private set; }
public IRepository Orders { get; private set; }
public async Task SaveChangesAsync()
{
return await _context.SaveChangesAsync();
}
public async Task BeginTransactionAsync()
{
_transaction = await _context.Database.BeginTransactionAsync();
}
public async Task CommitTransactionAsync()
{
await _transaction.CommitAsync();
}
public async Task RollbackTransactionAsync()
{
await _transaction.RollbackAsync();
}
public void Dispose()
{
_transaction?.Dispose();
_context.Dispose();
}
}
Usage Example:
public class ProductService
{
private readonly IUnitOfWork _unitOfWork;
public ProductService(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
}
public async Task CreateProductWithCategoryAsync(Product product, Category category)
{
await _unitOfWork.BeginTransactionAsync();
try
{
await _unitOfWork.Categories.AddAsync(category);
await _unitOfWork.SaveChangesAsync();
product.CategoryId = category.Id;
await _unitOfWork.Products.AddAsync(product);
await _unitOfWork.SaveChangesAsync();
await _unitOfWork.CommitTransactionAsync();
}
catch
{
await _unitOfWork.RollbackTransactionAsync();
throw;
}
}
}
Benefits:
- Separation of concerns
- Testability (easy to mock)
- Centralized data access logic
- Transaction management
- Reduced coupling
Note: DbContext already implements Unit of Work pattern, so this is often considered over-engineering for simple applications.