🧩 Unit of Work Pattern in .NET Core
|
|
📖 What is the Unit of Work Pattern?
The Unit of Work (UoW) pattern is a design pattern that coordinates
the work of multiple repositories by treating a set of operations as a
single transaction.
It ensures that either all operations succeed or none of them are committed,
maintaining data consistency.
🛠 Example in .NET Core with EF Core
1. Define Repositories
public interface IProductRepository
{
Task<IEnumerable<Product>> GetAllAsync();
Task AddAsync(Product product);
}
public interface IOrderRepository
{
Task<IEnumerable<Order>> GetAllAsync();
Task AddAsync(Order order);
}
2. Implement Repositories
public class ProductRepository : IProductRepository
{
private readonly AppDbContext _context;
public ProductRepository(AppDbContext context) => _context = context;
public async Task<IEnumerable<Product>> GetAllAsync() => await _context.Products.ToListAsync();
public async Task AddAsync(Product product) => await _context.Products.AddAsync(product);
}
public class OrderRepository : IOrderRepository
{
private readonly AppDbContext _context;
public OrderRepository(AppDbContext context) => _context = context;
public async Task<IEnumerable<Order>> GetAllAsync() => await _context.Orders.ToListAsync();
public async Task AddAsync(Order order) => await _context.Orders.AddAsync(order);
}
3. Define Unit of Work Interface
public interface IUnitOfWork : IDisposable
{
IProductRepository Products { get; }
IOrderRepository Orders { get; }
Task<int> CompleteAsync();
}
4. Implement Unit of Work
public class UnitOfWork : IUnitOfWork
{
private readonly AppDbContext _context;
public IProductRepository Products { get; }
public IOrderRepository Orders { get; }
public UnitOfWork(AppDbContext context, IProductRepository products, IOrderRepository orders)
{
_context = context;
Products = products;
Orders = orders;
}
public async Task<int> CompleteAsync() => await _context.SaveChangesAsync();
public void Dispose() => _context.Dispose();
}
5. Usage in Business Logic
public class OrderService
{
private readonly IUnitOfWork _unitOfWork;
public OrderService(IUnitOfWork unitOfWork) => _unitOfWork = unitOfWork;
public async Task PlaceOrder(Order order, Product product)
{
await _unitOfWork.Products.AddAsync(product);
await _unitOfWork.Orders.AddAsync(order);
await _unitOfWork.CompleteAsync(); // commits both in one transaction
}
}
✅ Advantages
- Ensures data consistency across multiple repositories.
- Centralizes transaction management.
- Improves maintainability and testability.
- Works well with Repository Pattern.
⚠️ Disadvantages
- Extra abstraction layer adds complexity.
- EF Core’s
DbContext already acts like a Unit of Work, so it can be redundant.
- Overkill for small/simple applications.
🧭 Best Practices
- Use UoW with Repository Pattern in complex domains.
- Keep UoW implementation thin — delegate to
DbContext.
- Inject UoW via Dependency Injection for testability.
- Combine with transactions for critical operations.
🔒 Precautions
- Don’t misuse UoW for trivial apps — DbContext may be enough.
- Ensure async/await is used to avoid blocking calls.
- Dispose UoW properly to release DbContext resources.
- Handle exceptions to avoid partial commits.
🎯 Summary
The Unit of Work Pattern is a powerful way to manage transactions across
multiple repositories in .NET Core. While EF Core’s DbContext
already provides UoW-like behavior, implementing it explicitly can improve
clarity, maintainability, and testability in larger projects.
⚖️ Repository vs Unit of Work vs Specification Pattern
📊 Comparison Table
| Aspect |
Repository Pattern |
Unit of Work Pattern |
Specification Pattern |
| Definition |
Abstracts data access logic, providing a collection-like interface for entities. |
Coordinates multiple repositories and ensures all changes are committed as a single transaction. |
Encapsulates query logic into reusable, composable specifications. |
| Main Purpose |
Decouple business logic from persistence logic. |
Maintain consistency across multiple operations. |
Centralize and reuse complex query logic. |
| When to Use |
Anytime you want clean separation of concerns and testability. |
When multiple repositories need to be updated in a single transaction. |
When queries are complex, repeated, or need to be combined dynamically. |
| Advantages |
Improves maintainability, testability, and readability. |
Ensures atomic commits, reduces risk of partial updates. |
Promotes DRY principle, improves readability of queries. |
| Disadvantages |
May duplicate EF Core’s DbSet functionality. |
EF Core’s DbContext already acts like a UoW, so can be redundant. |
Extra abstraction layer, may be overkill for simple queries. |
| Example in .NET Core |
_repository.Add(product); |
await _unitOfWork.CompleteAsync(); |
var spec = new ProductsByCategorySpec("Electronics"); |
🎯 Summary
- The Repository Pattern abstracts persistence logic.
- The Unit of Work Pattern ensures multiple repository operations are committed as one transaction.
- The Specification Pattern encapsulates query logic for reuse and composition.
Together, they form a powerful trio for building clean, testable, and maintainable architectures in .NET Core.