Skip to content

Repository & Unit of Work: Decoupling Data Access

Repository & Unit of Work: Decoupling Data Access

The Repository Pattern is a structural pattern that decouples the data access layer from the business logic layer. The Unit of Work Pattern coordinates the work of multiple repositories by creating a single database context shared by all of them.

🏗️ The Problem

If your business logic (Services/Controllers) contains direct database calls (e.g., using _context.Users.Add(user)), it becomes hard to test and maintain. If you change your ORM (e.g., switch from EF Core to Dapper), you have to change your business logic in many places.

🚀 The .NET Implementation

This is one of the most common architecture patterns in modern .NET applications.

1. The Repository Interface

public interface IUserRepository
{
    Task<User?> GetByIdAsync(int id);
    Task AddAsync(User user);
    void Update(User user);
}

2. The Concrete Repository (using EF Core)

public class UserRepository : IUserRepository
{
    private readonly MyDbContext _context;

    public UserRepository(MyDbContext context)
    {
        _context = context;
    }

    public async Task<User?> GetByIdAsync(int id) => await _context.Users.FindAsync(id);

    public async Task AddAsync(User user) => await _context.Users.AddAsync(user);

    public void Update(User user) => _context.Users.Update(user);
}

3. The Unit of Work (The Manager)

public interface IUnitOfWork : IDisposable
{
    IUserRepository Users { get; }
    Task<int> CompleteAsync(); // SaveChanges in one transaction
}

public class UnitOfWork : IUnitOfWork
{
    private readonly MyDbContext _context;
    public IUserRepository Users { get; private set; }

    public UnitOfWork(MyDbContext context)
    {
        _context = context;
        Users = new UserRepository(_context);
    }

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

🛠️ Real-World Usage (Client Service)

public class UserService
{
    private readonly IUnitOfWork _uow;

    public UserService(IUnitOfWork uow)
    {
        _uow = uow;
    }

    public async Task CreateUserAsync(User user)
    {
        await _uow.Users.AddAsync(user);
        // Add more operations here if needed
        await _uow.CompleteAsync(); // All changes are saved together!
    }
}

💡 Why use Repository & UoW?

  • Testability: You can easily mock the IUnitOfWork for unit testing your services without a database.
  • Maintainability: All data access logic is in one place.
  • Transaction Management: Unit of Work ensures that multiple changes are saved in a single transaction (all or nothing).