Learn how to implement the Unit of Work pattern combined with Repository Pattern in C# to manage database transactions safely, ensuring data consistency with the "all-or-nothing" principle.
No table of contents available for this article
When working with applications using Entity Framework and Repository Pattern, a common challenge is ensuring data consistency when operating with multiple repositories simultaneously. This is where the Unit of Work pattern shines.
What is Unit of Work?
Unit of Work is a design pattern that groups multiple CRUD (Create, Read, Update, Delete) operations into a single transaction. This pattern operates on the "all-or-nothing" principle - meaning all operations must succeed, or if any operation fails, the entire transaction rolls back to its original state.
Think of it like a bank transfer: when you transfer $100 from account A to account B, the system must perform two operations - deduct from A and add to B. If adding to B fails but the deduction succeeded, your money would "disappear." Unit of Work ensures this never happens.
Repository Pattern Refresher
Before diving deep into Unit of Work, let's review Repository Pattern. A repository is an intermediate layer between business logic and data access layer, responsible for performing database operations for a specific entity.
There are two common approaches:
Non-Generic Repository (One Repository per Entity): Each entity has its own repository. For example: EmployeeRepository handles only Employee-related operations, CustomerRepository handles only Customer.
Generic Repository (One Repository for All Entities): Uses generics to create a shared repository for all entities. Basic operations like GetAll, GetById, Insert, Update, Delete are defined once and reused.
In practice, projects often combine both: Generic Repository for common operations, and Non-Generic Repository for entity-specific business logic.
The Problem Without Unit of Work
Consider this Generic Repository code:
csharp
public class GenericRepository<T> : IGenericRepository<T> where T : class
{
public EmployeeDBContext _context = null;
public DbSet<T> table = null;
public GenericRepository()
{
this._context = new EmployeeDBContext();
table = _context.Set<T>();
}
public GenericRepository(EmployeeDBContext _context)
{
this._context = _context;
table = _context.Set<T>();
}
public IEnumerable<T> GetAll()
{
return table.ToList();
}
public T GetById(object id)
{
return table.Find(id);
}
public void Insert(T obj)
{
table.Add(obj);
}
public void Update(T obj)
{
table.Attach(obj);
_context.Entry(obj).State = EntityState.Modified;
}
public void Delete(object id)
{
T existing = table.Find(id);
table.Remove(existing);
}
public void Save()
{
_context.SaveChanges();
}
}The problem lies here: each repository creates its own DbContext instance. When working with EmployeeRepository and ProductRepository simultaneously, each repository maintains its own separate change tracker.
This leads to a dangerous scenario: if SaveChanges() from EmployeeRepository fails but ProductRepository succeeds, Product data gets saved while Employee does not. The result is an inconsistent database state.
The Solution: Implementing Unit of Work
Unit of Work solves this problem by maintaining a single DbContext instance for the entire application and sharing it across repositories:
csharp
public class UnitOfWork : IDisposable
{
private EmployeeDBContext _context = new EmployeeDBContext();
private GenericRepository<Employee> employeeRepository;
private GenericRepository<Product> productRepository;
public GenericRepository<Employee> EmployeeRepository
{
get
{
if (this.employeeRepository == null)
this.employeeRepository = new GenericRepository<Employee>(_context);
return employeeRepository;
}
}
public GenericRepository<Product> ProductRepository
{
get
{
if (this.productRepository == null)
this.productRepository = new GenericRepository<Product>(_context);
return productRepository;
}
}
public void Save()
{
_context.SaveChanges();
}
private bool disposed = false;
protected virtual void Dispose(bool disposing)
{
if (!this.disposed)
{
if (disposing)
{
_context.Dispose();
}
}
this.disposed = true;
}
public void Dispose()
{
Dispose(true);
System.GC.SuppressFinalize(this);
}
}Key points of this pattern:
A single DbContext (_context) is created within UnitOfWork
All repositories are initialized with this same DbContext instance
The UnitOfWork's Save() method is the only place that calls SaveChanges()
Implements IDisposable for proper resource cleanup
Usage in Controller
csharp
public class EmployeeController : Controller
{
private UnitOfWork unitOfWork = new UnitOfWork();
public ActionResult Create(Employee employee, Product product)
{
unitOfWork.EmployeeRepository.Insert(employee);
unitOfWork.ProductRepository.Insert(product);
// Single Save() for all changes
unitOfWork.Save();
return RedirectToAction("Index");
}
protected override void Dispose(bool disposing)
{
unitOfWork.Dispose();
base.Dispose(disposing);
}
}Benefits of Unit of Work
Ensures data consistency through transaction mechanism
Reduces database connections by using a single DbContext
Makes code easier to test since UnitOfWork can be mocked
Clear separation between business logic and data access
Conclusion
Unit of Work is an essential pattern when building enterprise applications with Entity Framework. By maintaining a single DbContext and centralizing transaction management, this pattern ensures data always remains in a consistent state, following the "all succeed or all rollback" principle.
References:
Martin Fowler - Patterns of Enterprise Application Architecture (Unit of Work pattern)
Microsoft Docs - Implementing the Repository and Unit of Work Patterns