So far we have looked at how Repository pattern can greatly help in segregating the data layer responsibilities in an application and we have also looked at how we can implement a simple Repository architecture in an AspNetCore application by means of dependency injection.
While the repository approach works great for applications with independent entities and their transactions, things get complicated when a set of database commit operations on one entity depend on a commit operation on another entity; a kind of transactional scope.
In such cases, we can’t just go and call on SaveChanges() method for each entity separately but instead 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 a UnitOfWork. And this approach is called as the UnitOfWork 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.
Let’s take the example of a sequence of transactions which happen when a user places an order in an online store. You have the orders entity, the items entity and the user_notifications entity where – a new order entry needs to be made to the orders entity, a new notification entry might be placed into user_notifications entity, and other related sub operations which follow the master transaction of creating an order entry.
In such cases, when we try saving each entity with 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.
A similar example can be of a Logger component where you would need to push all the Trace logs for a functionality under a TraceLogs entity and then push the overall operation Log onto a Log entity. You can’t just save two entities separately; 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
Should we use UnitOfWork?
Like all design patterns, UnitOfWork too runs the risk of turning application components components and when not used unnecessarily it can turn into an anti-pattern.
One should only group all the Repositories representing entities which are related to one another and depend on one another into a single UnitOfWork.
Unless an application contains such a scenario, UnitOfWork isn’t required to be implemented.
An anti-pattern is a term used for design patterns which create unnecessary problems in the application when implemented without any necessity.
The DbContext class provided by the EFCore is built based on UnitOfWork and Repository patterns and can be used by injecting inside the Controller classes directly without having to write custom Repository layers or custom UnitOfWork implementations.
Having said that, implementing custom repository classes for Entities provides several benefits when working with more complex microservices or applications.
The Unit of Work and Repository layers encapsulate the data layer from the application and domain-model layers so that it is decoupled from the higher levels. Implementing these patterns can facilitate the use of mock repositories simulating access to the database.
Implementing UnitOfWork with ASP.NET Core
For example, consider the ReadersApi class with its UserRepository and ReadersRepo classes which have operations working on the Users and Readers entities respectively.
#UserRepository#
namespace ReadersApi.Providers
{
public interface IUserRepository
{
}
public class UserRepository : IUserRepository
{
MyDbContext context;
public UserRepository(MyDbContext ctx)
{
this.context = ctx;
}
...
}
}
#ReaderRepository#
namespace ReadersApi.Providers
{
public interface IReaderRepository
{
}
public class ReaderRepository : IReaderRepository
{
public ReaderRepository(MyDbContext ctx)
{
}
...
}
}
These Repositories work on their own instance of MyDbContext object, which is injected via DI and is used to perform operations onto the database using Entity Framework Core. 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 IRepository<T> where T : class
{
IEnumerable<T> Find(
Expression<Func<T, bool>> predicate);
void Add(T entity);
void Remove(T entity);
}
public class Repository<T> : IRepository<T> where T : class
{
protected readonly DbSet<T> _entities;
public Repository(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 IRepository interface 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 Repository 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 IReaderRepository
: IRepository<Reader>
{
}
public class ReaderRepository
: Repository<Reader>, IReaderRepository
{
public ReaderRepository(MyDbContext ctx)
: base(ctx)
{
}
}
}
namespace ReadersApi.Providers
{
public interface IUserRepository
: IRepository<User>
{
}
public class UserRepository
: Repository<User>, IUserRepository
{
public UserRepository(MyDbContext ctx)
: base(ctx)
{
}
}
}
The Interfaces IUserRepository and IReaderRepository each extend the IRepository interface with User and Reader type reference passed respectively. Their implementation classes UserRepository and ReaderRepository extend the Repository class with their respective types passed.
We pass the whatever MyDbContext instance we receive at the constructor of the derived type onto the base constructor, which completes our UnitOfWork setup.
We finally have the UnitOfWork class which groups all the repositories in the application – UserRepository and ReaderRepository together and exposes an interface for the lower application layers to access these via a single instance.
namespace ReadersApi.Providers
{
public interface IUnitOfWork
{
IReaderRepository ReaderRepository { get; }
IUserRepository UserRepository { get; }
void Save();
}
public class UnitOfWork : IUnitOfWork
{
private IRepository<User> userRepo;
private IRepository<Reader> readerRepo;
private MyDbContext context;
public UnitOfWork(MyDbContext context)
{
this.context = context;
}
// Repositories are exposed via
// getter properties to the calling components
public IReaderRepository ReaderRepository
{
get
{
return new ReaderRepository(context);
}
}
public IUserRepository UserRepository
{
get
{
return new UserRepository(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 IReaderRepository and IUserRepository respectively. We pass the DatabaseContext through the constructor and then have the Save() method invoke context.Save() on the passed MyDbContext instance.
Since this context is also passed onto the instances of ReaderRepository and UserRepository classes respectively, we are virtually tying up all the tranactions happening over the context onto a single instance.
We 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.
The ReaderController and UserController classes, which are the endpoints for respective entities 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.UserRepository.Find(x => x.Id != 0);
}
}
}
In this way, we can implement a simple UnitOfWork architecture in our applications which can help improve the transactional cost on the database for dependent entity transactions.
Found this article helpful? Please consider supporting!