Unit Of Work In ASP.Net Core



This content originally appeared on DEV Community and was authored by Saumya-Ranjan-Mishra

Unit Of Work.. What is it ?

The Unit of Work pattern is a design pattern used to group multiple operations (usually repository calls) into a single transaction. It ensures that either all operations succeed or none are applied, maintaining data consistency.

It usually works alongside the Repository pattern, which abstracts data access logic.

As I said it can work with repository patten, I will keep the code fairly simple rather than over-complicating it with generic repository.

Setting up the repositories first…

entities
// Models/Product.cs
public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
}

// Models/Order.cs
public class Order
{
    public int Id { get; set; }
    public int ProductId { get; set; }
    public DateTime OrderDate { get; set; }
}

// Models/Log.cs
public class Log
{
    public int Id { get; set; }
    public string Message { get; set; }
    public DateTime Timestamp { get; set; }
}
set the DbContext
public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) {}

    public DbSet<Product> Products { get; set; }
    public DbSet<Order> Orders { get; set; }
    public DbSet<Log> Logs { get; set; }
}
Define the repositories
public interface IProductRepository
{
    Task<Product> GetByIdAsync(int id);
    Task AddAsync(Product product);
    void Update(Product product);
    void Delete(Product product);
}

public interface IOrderRepository
{
    Task AddAsync(Order order);
}

public interface ILogRepository
{
    Task AddAsync(Log log);
}
Implement the repositories
// Product repositories
public class ProductRepository : IProductRepository
{
    private readonly AppDbContext _context;
    public ProductRepository(AppDbContext context)
    {
        _context = context;
    }

    public async Task<Product> GetByIdAsync(int id)
    {
        return await _context.Products.FindAsync(id);
    }

    public async Task AddAsync(Product product)
    {
        await _context.Products.AddAsync(product);
    }

    public void Update(Product product)
    {
        _context.Products.Update(product);
    }

    public void Delete(Product product)
    {
        _context.Products.Remove(product);
    }
}
public class OrderRepository : IOrderRepository
{
    private readonly AppDbContext _context;
    public OrderRepository(AppDbContext context)
    {
        _context = context;
    }

    public async Task AddAsync(Order order)
    {
        await _context.Orders.AddAsync(order);
    }
}
public class LogRepository : ILogRepository
{
    private readonly AppDbContext _context;
    public LogRepository(AppDbContext context)
    {
        _context = context;
    }

    public async Task AddAsync(Log log)
    {
        await _context.Logs.AddAsync(log);
    }
}

Unit of Work Interface & Implementation

public interface IUnitOfWork : IDisposable
{
    IProductRepository Products { get; }
    IOrderRepository Orders { get; }
    ILogRepository Logs { get; }
    Task<int> CompleteAsync();
}
Implementation of IUnitOfWork
public class UnitOfWork : IUnitOfWork
{
    private readonly AppDbContext _context;

    public IProductRepository Products { get; }
    public IOrderRepository Orders { get; }
    public ILogRepository Logs { get; }

    public UnitOfWork(
        AppDbContext context,
        IProductRepository productRepository,
        IOrderRepository orderRepository,
        ILogRepository logRepository)
    {
        _context = context;
        Products = productRepository;
        Orders = orderRepository;
        Logs = logRepository;
    }

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

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

Inject the dependencies

Make sure you register the dependencies as AddScoped, so that all the repositories will share the same instance of DbContext in a particular http call.

services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

services.AddScoped<IProductRepository, ProductRepository>();
services.AddScoped<IOrderRepository, OrderRepository>();
services.AddScoped<ILogRepository, LogRepository>();
services.AddScoped<IUnitOfWork, UnitOfWork>();

Operations in controller

[ApiController]
[Route("api/[controller]")]
public class CheckoutController : ControllerBase
{
    private readonly IUnitOfWork _unitOfWork;

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

    [HttpPost]
    public async Task<IActionResult> Checkout([FromBody] ProductOrderDto dto)
    {
        var product = new Product { Name = dto.ProductName, Price = dto.Price };
        var order = new Order { ProductId = product.Id, OrderDate = DateTime.UtcNow };
        var log = new Log { Message = "New order placed", Timestamp = DateTime.UtcNow };

        await _unitOfWork.Products.AddAsync(product);
        await _unitOfWork.Orders.AddAsync(order);
        await _unitOfWork.Logs.AddAsync(log);

        // Single transaction
        await _unitOfWork.CompleteAsync();

        return Ok("Order placed successfully.");
    }
}

Congratulation! You have successfully implemented unit of work there by saving your DB from a possible inconsistent state.

Feel free to ask any question. Or ping in linkedin


This content originally appeared on DEV Community and was authored by Saumya-Ranjan-Mishra