The MVC Unit of Work Pattern with Example

The Unit of Work design pattern in C# MVC is used to manage database transactions and coordinate updates to multiple repositories.

It acts as a wrapper around a set of repository objects, ensuring that changes across multiple repositories are handled as a single transaction.

This is especially useful in complex systems where multiple entities are updated as part of a single operation.

Here’s a detailed breakdown of the Unit of Work pattern:


Key Concepts

  1. Transaction Management: The Unit of Work ensures that a set of operations is either completely successful or entirely rolled back in case of an error.
  2. Repository Coordination: Instead of handling each repository independently, the Unit of Work ensures that changes across repositories are saved together.
  3. Performance Optimization: The Unit of Work caches changes to objects and delays database writes until SaveChanges is called, reducing the number of database calls.
  4. Single Responsibility Principle: The Unit of Work encapsulates the transaction logic, keeping it separate from business logic and data access logic.

How It Works in C# MVC

  1. Repositories: Each entity in the application typically has its own repository (e.g., ProductRepository, CustomerRepository) to manage CRUD operations for that entity.
  2. Unit of Work Class: The Unit of Work class contains references to all repositories and provides a single Save or Commit method to save changes to the database.
  3. DbContext: In Entity Framework, the DbContext itself acts as a basic Unit of Work by tracking changes to entities and saving them as a transaction.

Implementation Steps

1. Create Repository Interfaces

public interface IGenericRepository<T> where T : class
{
    IEnumerable<T> GetAll();
    T GetById(int id);
    void Add(T entity);
    void Update(T entity);
    void Delete(int id);
}

2. Implement Generic Repository

public class GenericRepository<T> : IGenericRepository<T> where T : class
{
    private readonly DbContext _context;
    private readonly DbSet<T> _dbSet;

    public GenericRepository(DbContext context)
    {
        _context = context;
        _dbSet = context.Set<T>();
    }

    public IEnumerable<T> GetAll() => _dbSet.ToList();

    public T GetById(int id) => _dbSet.Find(id);

    public void Add(T entity) => _dbSet.Add(entity);

    public void Update(T entity) => _context.Entry(entity).State = EntityState.Modified;

    public void Delete(int id)
    {
        var entity = _dbSet.Find(id);
        if (entity != null) _dbSet.Remove(entity);
    }
}

3. Create the Unit of Work Interface

public interface IUnitOfWork : IDisposable
{
    IGenericRepository<Product> Products { get; }
    IGenericRepository<Customer> Customers { get; }
    void Save();
}

4. Implement the Unit of Work

public class UnitOfWork : IUnitOfWork
{
    private readonly DbContext _context;
    private IGenericRepository<Product> _products;
    private IGenericRepository<Customer> _customers;

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

    public IGenericRepository<Product> Products => 
        _products ??= new GenericRepository<Product>(_context);

    public IGenericRepository<Customer> Customers => 
        _customers ??= new GenericRepository<Customer>(_context);

    public void Save() => _context.SaveChanges();

    public void Dispose() => _context.Dispose();
}

5. Using Unit of Work in Controller

public class HomeController : Controller
{
    private readonly IUnitOfWork _unitOfWork;

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

    public ActionResult Index()
    {
        var products = _unitOfWork.Products.GetAll();
        return View(products);
    }

    [HttpPost]
    public ActionResult Create(Product product)
    {
        if (ModelState.IsValid)
        {
            _unitOfWork.Products.Add(product);
            _unitOfWork.Save();
            return RedirectToAction("Index");
        }
        return View(product);
    }

    protected override void Dispose(bool disposing)
    {
        _unitOfWork.Dispose();
        base.Dispose(disposing);
    }
}

Advantages

  1. Centralized Transactions: All database changes are coordinated through a single unit, simplifying rollback logic.
  2. Loose Coupling: The controllers interact with the Unit of Work instead of dealing with multiple repositories directly.
  3. Code Reusability: The pattern promotes reusability by abstracting repository and transaction logic.
  4. Testability: Mocking the Unit of Work is easier in unit tests compared to mocking individual repositories.

When to Use

  • Applications with multiple interconnected entities.
  • Scenarios requiring consistent transactions across multiple repositories.
  • Complex systems with modularized repository and data access layers.

This pattern integrates naturally with the MVC framework, improving maintainability and scalability of your application.

I hope this article helps you understand the use of the Unit of Work pattern in modern C# MVC applications.

~Cyber Abyss