We implement the Unit of Work pattern in ASP.NET Core

We implement the Unit of Work pattern in ASP.NET Core

Hello, Habre!

Today we will analyze how to implement the pattern Unit of Work in ASP.NET Core. Instead of long theoretical considerations, let’s see why it is needed at all, and how to correctly apply it in practice.

Why is Unit of Work needed at all?

You have probably come across a situation where several database operations need to be wrapped into one transaction. For example, when you create a user, you need to add it to several tables. What if something went wrong? One of the operations failed and the data is already partially added? It helps here Unit of Work. It ensures that all changes pass through one point, and are confirmed all at once, or rolled back.

But why exactly? Unit of Workrather than just transactions through DbContext? The answer is simple – the pattern allows you to work with several repositories at the same time.

IUnitOfWork interface

Let’s start with the basics – the interface IUnitOfWorkwhich will manage our transactions.

public interface IUnitOfWork : IDisposable
{
    IRepository UserRepository { get; }
    IRepository OrderRepository { get; }
    
    void Commit();
    void Rollback();
}

A couple of notes:

  • IDisposable correct release of resources is necessary. This means that when you are done with a transaction, Dispose will automatically close all connections and free memory. Let’s not forget to call the method!

  • Commit and Rollback are methods responsible for confirming or rolling back transactions.

Now we have an interface, let’s go to the main one. DbContext.

The basis of the operation is DbContext

as i said Unit of Work it can’t do much by itself if it doesn’t have a connection to the database. This is where it comes to the rescue DbContextwhich is responsible for all operations with the database ASP.NET Core.

public class AppDbContext : DbContext
{
    public DbSet Users { get; set; }
    public DbSet Orders { get; set; }

    public AppDbContext(DbContextOptions options)
        : base(options) { }
    
    public void BeginTransaction()
    {
        Database.BeginTransaction();
    }

    public void CommitTransaction()
    {
        Database.CommitTransaction();
    }

    public void RollbackTransaction()
    {
        Database.RollbackTransaction();
    }
}

Here we see several ways:

  • To the beginning – Starts a transaction. This is the first step before making changes.

  • CommitTransaction – Confirms all changes.

  • RollbackTransaction – Rolls back changes if something goes wrong.

These methods are the basis of the pattern Unit of WorkBut even more important is how to properly integrate them into the business logic.

Implementation of Unit of Work

Now let’s collect ours Unit of Work into a single mechanism.

public class UnitOfWork : IUnitOfWork
{
    private readonly AppDbContext _context;
    private IRepository _userRepository;
    private IRepository _orderRepository;

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

    public IRepository UserRepository
    {
        get { return _userRepository ??= new Repository(_context); }
    }

    public IRepository OrderRepository
    {
        get { return _orderRepository ??= new Repository(_context); }
    }

    public void Commit()
    {
        _context.SaveChanges();
        _context.CommitTransaction();
    }

    public void Rollback()
    {
        _context.RollbackTransaction();
    }

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

Pay attention to:

  • Lazy initialization repositories. This means that we only build repositories when we really need them.

  • Commit calls the method SaveChangeswhich saves all changes to the database and then confirms the transaction. In case of error – rollback.

When is Unit of Work not the best choice?

Unit of Work is a great tool for managing transactions, but it’s not always worth using. For example, if there is a program with small and simple operations, adding an extra layer of abstraction will only complicate the code. In such cases, it is better to use default transaction through DbContext.

Also, if there are too many repositories and dependencies, Unit of Work can only become a performance bottleneck. Therefore, always evaluate how justified its use is.

Repositories

Unit of Work without repositories is like a bicycle without wheels. They manage concrete entities and are responsible for CRUD operations. Example repository:

public class Repository : IRepository where T : class
{
    private readonly AppDbContext _context;
    private readonly DbSet _dbSet;

    public Repository(AppDbContext context)
    {
        _context = context;
        _dbSet = context.Set();
    }

    public void Add(T entity)
    {
        _dbSet.Add(entity);
    }

    public void Update(T entity)
    {
        _dbSet.Update(entity);
    }

    public void Delete(T entity)
    {
        _dbSet.Remove(entity);
    }

    public IEnumerable GetAll()
    {
        return _dbSet.ToList();
    }

    public T GetById(int id)
    {
        return _dbSet.Find(id);
    }
}

This repository is universal and can work with any entities. All transactions through DbSet.

How it looks in practice

Now let’s look at a real use case Unit of Work in the controller:

public class UserController : Controller
{
    private readonly IUnitOfWork _unitOfWork;

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

    [HttpPost]
    public IActionResult CreateUser(UserViewModel model)
    {
        try
        {
            _unitOfWork.UserRepository.Add(new User { Name = model.Name });
            _unitOfWork.Commit();
            return Ok("User created successfully.");
        }
        catch (Exception ex)
        {
            _unitOfWork.Rollback();
            return BadRequest($"Error: {ex.Message}");
        }
    }
}

We add the user via UserRepository and fix the transaction through Commit. If something goes wrong, the transaction is rolled back.

How to test it?

Transaction testing is an important part of working with Unit of Work. A library is ideal for this Moq:

[Test]
public void CreateUser_ShouldCommitTransaction_WhenUserIsValid()
{
    var mockUnitOfWork = new Mock();
    var controller = new UserController(mockUnitOfWork.Object);

    var result = controller.CreateUser(new UserViewModel { Name = "Test User" });

    mockUnitOfWork.Verify(u => u.Commit(), Times.Once);
}

Here we check that the method Commit is called when the user is successfully added.


Conclusion

Now you know how to implement and use the pattern Unit of Work in ASP.NET Core. But remember: this pattern is not always necessary, and its use must be justified by the project architecture. If you have any questions or have something to share, write in the comments.

Taking this opportunity, I would like to remind you about the open lessons that will soon be held as part of the C# ASP.NET Core developer course:

Related posts