⚙️ Mastering the Unit of Work Pattern in C# with Entity Framework Core
"Do everything or do nothing." — That’s the promise of the Unit of Work pattern.
When building robust applications in C#, managing database operations efficiently becomes a key concern — especially when you're performing multiple related operations. This is where the Unit of Work (UoW) pattern shines.
In this article, you’ll learn:
What Unit of Work is and why it matters
How it relates to the Repository Pattern
How to implement it using EF Core
Clean architecture and real-world scenarios
🧠 What Is the Unit of Work Pattern?
The Unit of Work is a behavioral design pattern that maintains a list of business transactions and coordinates the writing out of changes and the resolution of concurrency problems.
Imagine you’re saving a new Order
, adding multiple OrderItems
, and adjusting Inventory
— you want all these operations to succeed or fail together. The Unit of Work ensures that all database operations are treated as a single transaction.
If something fails during the operation, rollback everything.
If everything succeeds, commit once.
🏗️ Why Use Unit of Work in C#?
When working with Entity Framework Core, you already get some transaction management out of the box. But when your architecture involves multiple repositories or services working together, it becomes essential to coordinate them with a common unit of work.
✅ Benefits:
Atomic operations — Ensures all changes are committed or none are.
Reduced code duplication — Centralized transaction management.
Improved testability — Easily mock the UoW in unit tests.
Separation of concerns — Keeps the business layer clean.
🔄 Relationship with Repository Pattern
The Repository pattern acts as a layer between your business logic and data access code. Each entity typically has its own repository — for example, ProductRepository
, CategoryRepository
.
The Unit of Work groups these repositories under a common interface and manages the transactional behavior across them.
You inject this into your services or controllers, and call CompleteAsync()
to persist the changes.
🛠️ Implementing Unit of Work with EF Core
Let’s walk through a minimal but solid implementation.
1. Create a Generic Repository
public interface IRepository<T> where T : class
{
Task<T?> GetByIdAsync(object id);
Task<IEnumerable<T>> GetAllAsync();
Task<IEnumerable<T>> FindAsync(Expression<Func<T, bool>> predicate);
Task AddAsync(T entity);
Task AddRangeAsync(IEnumerable<T> entities);
void Update(T entity);
void Remove(T entity);
void RemoveRange(IEnumerable<T> entities);
}
2. Implement Repository
public class Repository<T> : IRepository<T> where T : class
{
protected readonly AppDbContext _context;
protected readonly DbSet<T> _dbSet;
public Repository(AppDbContext context)
{
_context = context;
_dbSet = _context.Set<T>();
}
public async Task<T?> GetByIdAsync(object id)
{
return await _dbSet.FindAsync(id);
}
public async Task<IEnumerable<T>> GetAllAsync()
{
return await _dbSet.ToListAsync();
}
public async Task<IEnumerable<T>> FindAsync(Expression<Func<T, bool>> predicate)
{
return await _dbSet.Where(predicate).ToListAsync();
}
public async Task AddAsync(T entity)
{
await _dbSet.AddAsync(entity);
}
public async Task AddRangeAsync(IEnumerable<T> entities)
{
await _dbSet.AddRangeAsync(entities);
}
public void Update(T entity)
{
_dbSet.Update(entity);
}
public void Remove(T entity)
{
_dbSet.Remove(entity);
}
public void RemoveRange(IEnumerable<T> entities)
{
_dbSet.RemoveRange(entities);
}
}
3. Create Unit of Work Interface
public interface IUnitOfWork
{
int Commit();
void Rollback();
Task CommitAsync();
Task RollbackAsync();
}
4. Implement Unit of Work
public class UnitOfWork : IUnitOfWork
{
private readonly AppDbContext _dataContext;
public UnitOfWork(AppDbContext dataContext)
{
_dataContext = dataContext;
}
public int Commit()
{
return _dataContext.SaveChanges();
}
public async Task CommitAsync()
{
await _dataContext.SaveChangesAsync();
}
public void Rollback()
{
_dataContext.Dispose();
}
public async Task RollbackAsync()
{
await _dataContext.DisposeAsync();
}
}
📦 Usage in Controller
[ApiController]
[Route("api/[controller]")]
public class ProductController : ControllerBase
{
private readonly IUnitOfWork _unitOfWork;
private readonly IRepository<Product> _productRepository;
public ProductController(IRepository<Product> productRepository, IUnitOfWork unitOfWork)
{
_productRepository = productRepository;
_unitOfWork = unitOfWork;
}
// GET: api/product
[HttpGet]
public async Task<IActionResult> GetAll()
{
var products = await _productRepository.GetAllAsync();
return Ok(products);
}
// GET: api/product/5
[HttpGet("{id}")]
public async Task<IActionResult> GetById(int id)
{
var product = await _productRepository.GetByIdAsync(id);
if (product == null)
return NotFound();
return Ok(product);
}
// POST: api/product
[HttpPost]
public async Task<IActionResult> Create([FromBody] Product product)
{
if (!ModelState.IsValid)
return BadRequest(ModelState);
await _productRepository.AddAsync(product);
await _unitOfWork.CommitAsync();
return CreatedAtAction(nameof(GetById), new { id = product.Id }, product);
}
// PUT: api/product/5
[HttpPut("{id}")]
public async Task<IActionResult> Update(int id, [FromBody] Product product)
{
if (id != product.Id)
return BadRequest("Product ID mismatch");
var existing = await _productRepository.GetByIdAsync(id);
if (existing == null)
return NotFound();
// Update allowed fields
existing.Name = product.Name;
existing.Price = product.Price;
_productRepository.Update(existing);
await _unitOfWork.CommitAsync();
return NoContent();
}
// DELETE: api/product/5
[HttpDelete("{id}")]
public async Task<IActionResult> Delete(int id)
{
var product = await _productRepository.GetByIdAsync(id);
if (product == null)
return NotFound();
_productRepository.Remove(product);
await _unitOfWork.CommitAsync();
return NoContent();
}
}
register dependency injection in Program.cs
builder.Services.AddScoped(typeof(IUnitOfWork), typeof(UnitOfWork));
builder.Services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
🔍 Real-World Use Case
Let’s say you're building an e-commerce application. When a user places an order:
Save the order entity
Update inventory
Record payment transaction
Notify shipment service
These steps must succeed or fail together — this is exactly what Unit of Work enables.
🧪 Testing the Unit of Work
By abstracting UoW and repositories, you can inject mock implementations during unit testing. This removes the database dependency and makes your tests faster and more reliable.
🧵 Summary
✅ Unit of Work coordinates multiple operations in a single transaction
🧱 Works best with Repository Pattern
💡 Increases maintainability, testability, and separation of concerns
🧰 Easily implemented with EF Core
🏁 Final Thoughts
The Unit of Work pattern is an architectural best practice for data-heavy applications. Whether you’re building APIs or enterprise systems, adopting UoW brings structure and reliability to your codebase.
I hope you found this guide helpful and informative.
Thanks for reading!
If you enjoyed this article, feel free to share it and follow me for more practical, developer-friendly content like this.