Section 3 of 5

Unit of Work

🎯 What You'll Learn

  • What is Unit of Work
  • Creating UnitOfWork interface
  • Implementing UnitOfWork
  • Transaction management
  • Using with repositories

What is Unit of Work?

The Unit of Work pattern maintains a list of changes and coordinates writing them to the database in a single transaction.

Why Use Unit of Work?

  • Transaction management: Single SaveChanges() call
  • Consistency: All changes succeed or fail together
  • Performance: Batch database operations
  • Coordination: Manage multiple repositories

Unit of Work Interface

IUnitOfWork.cs C#
public interface IUnitOfWork : IDisposable
{
    IProductRepository Products { get; }
    IOrderRepository Orders { get; }
    ICustomerRepository Customers { get; }
    
    Task<int> SaveChangesAsync();
}

Unit of Work Implementation

UnitOfWork.cs C#
public class UnitOfWork : IUnitOfWork
{
    private readonly InvenTrackDbContext _context;
    private IProductRepository _products;
    private IOrderRepository _orders;
    private ICustomerRepository _customers;

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

    public IProductRepository Products =>
        _products ??= new ProductRepository(_context);

    public IOrderRepository Orders =>
        _orders ??= new OrderRepository(_context);

    public ICustomerRepository Customers =>
        _customers ??= new CustomerRepository(_context);

    public async Task<int> SaveChangesAsync()
    {
        return await _context.SaveChangesAsync();
    }

    public void Dispose()
    {
        _context.Dispose();
    }
}

Register Unit of Work

Program.cs C#
builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();

Using Unit of Work

OrdersController.cs C#
public class OrdersController : Controller
{
    private readonly IUnitOfWork _unitOfWork;

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

    [HttpPost]
    public async Task<IActionResult> Create(Order order)
    {
        // Add order
        await _unitOfWork.Orders.AddAsync(order);

        // Update product stock
        foreach (var item in order.Items)
        {
            var product = await _unitOfWork.Products.GetByIdAsync(item.ProductId);
            product.Quantity -= item.Quantity;
            await _unitOfWork.Products.UpdateAsync(product);
        }

        // Save all changes in one transaction
        await _unitOfWork.SaveChangesAsync();

        return RedirectToAction("Index");
    }
}

Modified Repository (No SaveChanges)

ProductRepository.cs C#
public class ProductRepository : IProductRepository
{
    private readonly InvenTrackDbContext _context;

    public async Task AddAsync(Product product)
    {
        await _context.Products.AddAsync(product);
        // No SaveChangesAsync() here!
    }

    public Task UpdateAsync(Product product)
    {
        _context.Products.Update(product);
        // No SaveChangesAsync() here!
        return Task.CompletedTask;
    }
}

Key Takeaways

  • Unit of Work: Coordinates multiple repositories
  • Single transaction: One SaveChangesAsync() call
  • Consistency: All or nothing
  • Repositories: Don't call SaveChanges()
  • UnitOfWork: Calls SaveChanges() once
  • Pattern combination: Works with Repository Pattern