We use cookies to provide you with a great user experience, analyze traffic and serve targeted promotions.   Learn More   Accept
Card image cap

Understanding and Implementing UnitOfWork pattern in ASP.NET Core

Design Patterns  • Posted 5 months ago

In a previous article, we have seen the importance of using a Repository pattern for segregating the data layer responsibilities in an application and how we can implement a Repository pattern in asp.net core by means of dependency injection. In this article, we go one step forward and look at how we can further improve the design and efficiency of the data transactions across the application repository layers by clustering related repository components into a single unit, called as the unit of work pattern.

What is a Unit of Work ?

A Unit of work is used for a single purpose: to ensure that when there are multiple repository components which need to be invoked or processed for a single request share a common database context. That way we can reduce the number of times a database connection is made for transaction when these repository components are used separately. For example, let's assume the sequence of transactions which happen when a user does a create order operation: a new order entry needs to be made to the orders entity, a new notification entry might be placed into a notification entity, and other related sub operations which follow the master transaction of creating an order entry. When in such cases, each entity is transacted via separate repositories which have separate context object scope, this can cause tens of individual transactions onto the database for a single master transaction. And there can be a chance of failure of one or all of these transactions and such things are expected if not desired to go as a single bunch of operations under a single transaction scope which ensures a total success or a total failure. In such cases, the UnitOfWork pattern is quite useful.

A UnitOfWork pattern is just a simple class with one method for Saving the context state, along with properties representing every related Repository which need to be processed as a whole. And all these Repository classes receive the same context object reference on which the single Save() method works on.

A UnitOfWork class contains a single Save method followed by properties of all the Repository types to be grouped for a single context instance

For example, consider the ReadersApi class with its UserRepo and ReadersRepo classes which have operations working on the Users and Readers entities respectively.

namespace ReadersApi.Providers
{
    public interface IUserRepo
    {
    }

    public class UserRepo : IUserRepo
    {
        MyContext context;
        
        public UserRepo(MyContext ctx)
        {
            this.context = ctx;
        }

	...
    }
}

namespace ReadersApi.Providers
{
    public interface IReaderRepo
    {
    }

    public class ReaderRepo : IReaderRepo
    {
        public ReaderRepo(MyContext ctx)
        {
        }
	
	...
    }
}

And these Repositories work on their own instance of MyContext object. In order to implement the UnitOfWork pattern, we group all the common CRUD (Create, Retrieve, Update and Delete) operations which are generally observed on these entities into a generic repository.

namespace ReadersApi.Providers
{
    public interface IRepo<T> where T : class
    {
        IEnumerable<T> Find(Expression<Func<T, bool>> predicate);
        void Add(T entity);
        void Remove(T entity);
    }

    public class Repo<T> : IRepo<T> where T : class
    {
        protected readonly DbSet<T> _entities;
        public Repo(DbContext context)
        {
            _entities = context.Set<T>();
        }

        public IEnumerable<TEntity> Find(Expression<Func<T, bool>> predicate)
        {
            return _entities.Where(predicate);
        }
        public void Add(T entity)
        {
            _entities.Add(entity);
        }
        public void Remove(T entity)
        {
            _entities.Remove(entity);
        }
    }
}

The above is an interface IRepo which accepts a generic type reference T of a class type, and declares three methods each to query, add and delete records from the entity. And we have an implementation Repo which works on the context passed onto it and the entity set is created by using the context.Set() method, which returns the specific DbSet of type T which is passed from the database.

Now that we have the generic repository, we shall link this repository with the User and Reader Repositories as below.

namespace ReadersApi.Providers
{
    public interface IReaderRepo : IRepository<Reader>
    {
    }

    public class ReaderRepo : Repository<Reader>, IReaderRepo
    {
        public ReaderRepo(MyContext ctx) : base(ctx)
        {
        }
    }
}

namespace ReadersApi.Providers
{
    public interface IUserRepo : IRepo<User>
    {
    }

    public class UserRepo : Repo<User>, IUserRepo
    {        
        public UserRepo(MyContext ctx) : base(ctx)
        {
        }
    }
}

Observe that the Interfaces IUserRepo and IReaderRepo each extend the IRepo interface with User and Reader type reference passed respectively. And their implementations UserRepo and ReaderRepo extend the Repo class with their respective types passed. And we pass the whatever reference we receive at the constructor of the derived type MyContext towards the base constructor. This makes the link complete.

The UnitOfWork class for this setup is shown below:

namespace ReadersApi.Providers
{
    public interface IUnitOfWork
    {
        IReaderRepo ReaderRepo { get; }
        IUserRepo UserRepo { get; }
        void Save();
    }
    
    public class UnitOfWork : IUnitOfWork
    {
        private IRepository<User> userRepo;
        private IRepository<Reader> readerRepo;
        private MyContext context;

        public UnitOfWork(MyContext context)
        {
            this.context = context;
        }

        public IReaderRepo ReaderRepo => new ReaderRepo(context);
        public IUserRepo UserRepo => new UserRepo(context);

        public void Save()
        {
            this.context.SaveChanges();
        }
    }
}

The UnitOfWork class implements the IUnitOfWork interface which itself has a single method Save() and two properties of types IReaderRepo and IUserRepo respectively. And in their implementation in the UnitOfWork class, we pass the DatabaseContext through the constructor and then have the Save() method invoke context.Save() on the same context. And this context is also passed onto the instances of ReaderRepo and UserRepo classes respectively which are available to the calling classes as properties.

_Now this setup is complete by declaring the IUnitOfWork type as a Scoped service inside the Startup.cs (since DbContext can only be passed onto a transient or scoped service; because DbContext service itself is a scoped ones).

Now that it is done, the ReaderController and UserController (the endpoints for the respective transactions) now receive IUnitOfWork instance through the constructor in place of their respective repositories and have their respective repositories be called through the properties.

namespace ReadersApi.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class UserController : ControllerBase
    {
        IUnitOfWork repo;

        public UserController(IUnitOfWork repo)
        {
            this.repo = repo;
        }

        [HttpGet]
        [Route("allreaders")]
        public IEnumerable<User> GetReaders()
        {
            return repo.UserRepo.Find(x => x.Id != 0);
        }
    }
}

In this way, we can implement the UnitOfWork pattern in our applications which can help reduce redundant Repository class functionality and improves the transactional cost on the database as well.

Also Read:

Implementing Repository Pattern in ASP.NET Core

Getting Started with EFCore and Database Context

Published 5 months ago

Sponsored Links