Repository and Unit of Work Patterns in C

This article provides a practical guide to implementing the Repository and Unit of Work design patterns in C# web applications using Entity Framework Core. You will learn the core concepts behind these patterns, how to define generic and specialized repositories, how to coordinate transactions with a Unit of Work, and how to register and consume these services within an ASP.NET Core application.


Core Concepts

The Repository pattern acts as an in-memory collection of domain objects, isolating data access logic from business logic. It provides methods to query, add, and remove entities without exposing database queries (such as raw SQL or direct EF Core calls) to higher layers.

The Unit of Work pattern maintains a list of business objects affected by a transaction and coordinates writing out changes and resolving concurrency problems. In Entity Framework Core, DbContext already implements both patterns internally, but wrapping it in custom abstractions helps create testable codebases, decouple the domain from the ORM, and centralize transaction boundaries.


Step 1: Define the Generic Repository Interface

Create an abstraction defining common CRUD operations for your entities.

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 Remove(T entity);
}

Step 2: Implement the Generic Repository

Implement the generic interface using an EF Core DbContext.

using Microsoft.EntityFrameworkCore;

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 Remove(T entity) => _dbSet.Remove(entity);
}

Step 3: Create Entity-Specific Repositories

When an entity requires custom query methods, define a dedicated interface and implementation extending the generic base.

public interface IProductRepository : IRepository<Product>
{
    Task<IEnumerable<Product>> GetFeaturedProductsAsync();
}

public class ProductRepository : Repository<Product>, IProductRepository
{
    public ProductRepository(ApplicationDbContext context) : base(context) { }

    public async Task<IEnumerable<Product>> GetFeaturedProductsAsync()
    {
        return await _dbSet.Where(p => p.IsFeatured).ToListAsync();
    }
}

Step 4: Define and Implement the Unit of Work

The Unit of Work exposes individual repositories and manages the database transaction via a single save operation.

Interface

public interface IUnitOfWork : IDisposable
{
    IProductRepository Products { get; }
    Task<int> CompleteAsync();
}

Implementation

public class UnitOfWork : IUnitOfWork
{
    private readonly ApplicationDbContext _context;
    public IProductRepository Products { get; private set; }

    public UnitOfWork(ApplicationDbContext context)
    {
        _context = context;
        Products = new ProductRepository(_context);
    }

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

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

Step 5: Register Dependencies in ASP.NET Core

Register your DbContext and Unit of Work inside the dependency injection container in Program.cs.

builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();

Step 6: Consume the Unit of Work in Controllers

Inject IUnitOfWork into your API controllers or service classes to perform data operations atomically.

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly IUnitOfWork _unitOfWork;

    public ProductsController(IUnitOfWork unitOfWork)
    {
        _unitOfWork = unitOfWork;
    }

    [HttpGet("featured")]
    public async Task<IActionResult> GetFeatured()
    {
        var products = await _unitOfWork.Products.GetFeaturedProductsAsync();
        return Ok(products);
    }

    [HttpPost]
    public async Task<IActionResult> CreateProduct([FromBody] Product product)
    {
        await _unitOfWork.Products.AddAsync(product);
        await _unitOfWork.CompleteAsync();

        return CreatedAtAction(nameof(GetFeatured), new { id = product.Id }, product);
    }
}