Explain the Unit of Work and Repository patterns.

4 minintermediateEF-Corerepositoryunit-of-workpatterns

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.