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
- 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.
- Repository Coordination: Instead of handling each repository independently, the Unit of Work ensures that changes across repositories are saved together.
- Performance Optimization:
The Unit of Work caches changes to objects and delays database writes until
SaveChanges
is called, reducing the number of database calls. - 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
- Repositories:
Each entity in the application typically has its own repository (e.g.,
ProductRepository
,CustomerRepository
) to manage CRUD operations for that entity. - Unit of Work Class:
The Unit of Work class contains references to all repositories and provides a single
Save
orCommit
method to save changes to the database. - 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
- Centralized Transactions: All database changes are coordinated through a single unit, simplifying rollback logic.
- Loose Coupling: The controllers interact with the Unit of Work instead of dealing with multiple repositories directly.
- Code Reusability: The pattern promotes reusability by abstracting repository and transaction logic.
- 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