Ermir Beqiraj
Backend architect. Systems, agents, infrastructure — from inside the work.
all writing

The repository pattern aims to create an abstraction layer on top of the data access layer, by doing so it creates a separation of concerns between the business logic and data storage mechanism. I’ll explain in simple terms how to use these patterns with a small example and what benefits come with them.

The hypothetical example for this article: a sample application with three protagonists — Owner, Car, and Service.

Repository Pattern

To implement the repository pattern, you need a separate class for each entity you want to manage. This class is responsible for all data-related operations for that entity. So here we will have 3 repositories — one per domain type. Since we’re writing for testability and abstraction, each class has a corresponding interface.

Creating repositories

Starting with the Owner entity:

public interface IOwnerRepository
{
    IEnumerable<Owner> GetAll();
    Owner Get(Guid Id);
    void Insert(Owner model);
    void Update(Owner model);
    void Delete(Owner model);
}

Implementation — the DbContext is injected via constructor:

public class OwnerRepository : IOwnerRepository, IDisposable
{
    private readonly ApplicationDbContext context;
    public OwnerRepository(ApplicationDbContext applicationDbContext)
    {
        context = applicationDbContext;
    }

    public Owner Get(Guid Id) => context.Owners.Find(Id);
    public IEnumerable<Owner> GetAll() => context.Owners.ToList();
    public void Insert(Owner model) => context.Owners.Add(model);
    public void Update(Owner model) => context.Entry(model).State = EntityState.Modified;
    public void Delete(Owner model) => context.Owners.Remove(model);
}

The same pattern applies to Car and Service repositories.

Notice that SaveChanges() is absent from create/update/delete. This is intentional — we want atomic transactional behavior. For example, every time a Service record is inserted, Car.LastService must update too, and both must commit or rollback together. That’s what Unit of Work is for.

Unit of Work

This layer creates and manages repositories and the shared DbContext:

public interface IUnitOfWork
{
    ICarRepository CarRepository { get; }
    IOwnerRepository OwnerRepository { get; }
    IServiceRepository ServiceRepository { get; }

    void Commit();
}

Implementation — ensures a single DbContext instance is shared across repositories:

public class UnitOfWork : IUnitOfWork, IDisposable
{
    private ICarRepository _carRepository;
    private IOwnerRepository _ownerRepository;
    private IServiceRepository _serviceRepository;
    private readonly ApplicationDbContext _context;

    public UnitOfWork(ApplicationDbContext context)
    {
        _context = context;
    }

    public ICarRepository CarRepository =>
        _carRepository ??= new CarRepository(_context);

    public IOwnerRepository OwnerRepository =>
        _ownerRepository ??= new OwnerRepository(_context);

    public IServiceRepository ServiceRepository =>
        _serviceRepository ??= new ServiceRepository(_context);

    public void Commit() => _context.SaveChanges();
}

Repository client (API controller)

Register in Startup.cs:

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

    services.AddScoped<IUnitOfWork, UnitOfWork>();
}

Use from a controller — the Post action updates two entities atomically via a single Commit() call:

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

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

    [HttpGet]
    public IActionResult Get()
    {
        var dbItems = _unitOfWork.ServiceRepository.GetAll();
        return Ok(dbItems);
    }

    [HttpPost]
    public IActionResult Post([FromBody] Service model)
    {
        var dbCar = _unitOfWork.CarRepository.Get(model.CarId);
        dbCar.LastService = DateTime.Now;

        _unitOfWork.CarRepository.Update(dbCar);
        _unitOfWork.ServiceRepository.Insert(model);

        _unitOfWork.Commit(); // single atomic commit
        return Ok();
    }
}

Generic Repository

For many CRUD-heavy applications, creating one repository per entity is overkill. The Generic Repository gives you a single reusable implementation for all entities.

Interface (constrained to entities derived from EntityBase):

public interface IRepository<T> where T : EntityBase
{
    T Get(Guid id);

    IEnumerable<T> Get(
        Expression<Func<T, bool>> filter = null,
        Func<IQueryable<T>, IOrderedQueryable<T>> orderBy = null,
        string[] include = null);

    void Insert(T entity);
    void Update(T entity);
    void Delete(T entity);
}

Implementation:

public class Repository<T> : IRepository<T> where T : EntityBase
{
    protected DbContext _context;
    protected DbSet<T> dbSet;

    public Repository(DbContext context)
    {
        _context = context;
        dbSet = _context.Set<T>();
    }

    public T Get(Guid id) => dbSet.Find(id);

    public virtual IEnumerable<T> Get(
        Expression<Func<T, bool>> filter = null,
        Func<IQueryable<T>, IOrderedQueryable<T>> orderBy = null,
        string[] include = null)
    {
        IQueryable<T> query = dbSet;

        if (filter != null)
            query = query.Where(filter);

        if (include != null)
            foreach (var item in include)
                query = query.Include(item);

        return orderBy != null ? orderBy(query).ToList() : query.ToList();
    }

    public void Insert(T entity) => dbSet.Add(entity);

    public void Update(T entity)
    {
        if (_context.Entry(entity).State == EntityState.Detached)
            dbSet.Attach(entity);
        _context.Entry(entity).State = EntityState.Modified;
    }

    public void Delete(T entity)
    {
        if (_context.Entry(entity).State == EntityState.Detached)
            dbSet.Attach(entity);
        dbSet.Remove(entity);
    }
}

The Unit of Work for the generic version follows the same shape — IRepository<Car> instead of ICarRepository, and new Repository<Car>(_context) as the implementation. DI registration and controller usage are identical to the non-generic version.

Benefits

  • Repository provides an abstraction so business logic works with simple collections, without caring about the real data storage mechanism
  • Testability — substitute any repository with a fake object that implements the interface
  • Repository also supports the objective of achieving a clean separation and one-way dependency between the domain and data mapping layers. — Martin Fowler

  • With Generic Repository, CRUD-heavy applications get up and running quickly with minimal boilerplate

Caveats

  • Repositories should always return IEnumerable, not IQueryable — the goal is an isolated query layer; you don’t want query composition leaking into other application layers
  • Repositories deal with domain objects, not view models. For dashboard/statistics queries, implement a separate provider (e.g., ServiceProvider.cs) and use .AsNoTracking() there
  • Mapping is a responsibility of the application layer — keep your mapper in the MVC/API project, not the repository project

Sample project

Full code at github.com/ermirbeqiraj/repository-generic-repository-uow.

Solution structure:

  • /Domain — POCO business entities
  • /Infrastructure/Domain.EF.SqlServer — EF Core configurations and DbContext
  • /RepositoryPattern — repository pattern + Unit of Work implementation, plus a sample Web API client
  • /GenericRepositoryPattern — generic repository + Unit of Work implementation, plus a sample Web API client
Ermir Beqiraj is a backend architect building AI-integrated infrastructure. This is his personal writing.