Today we continue with what could be understood as the second part of the post about the repository pattern. I want to share with you an important design pattern that sometimes goes unnoticed, but is fundamental in many enterprise and web applications: the Unit of Work Pattern.
Table of Contents
1 - What is the Unit of Work Pattern?
The Unit of Work Pattern is a design pattern used to group multiple operations (usually database operations) into a single "unit of work," ensuring that all these operations are executed or none are executed.
Sounds a lot like transactions, right? Actually, it's an abstraction layer above database transactions.
Note: as with the repository pattern where DbSet
is its equivalent, DbContext
is the unit of work pattern. However, both I personally, and a large number of developers, consider that DbSet
and DbContext
are directly the database, so we like to have an abstraction just above them.
2 - Why Should We Use the Unit of Work Pattern?
It's easier if we see an example: Let's say you have an e-commerce application. A user places an order, which involves several steps such as checking inventory, creating the order, deducting the inventory, and finally, sending a confirmation email. Each of these steps modifies your application state. (in a monolith; in distributed architectures it's different).
If something goes wrong in the process, you need to ensure that the database does not end up in an inconsistent state. This is where the Unit of Work Pattern is useful. It guarantees that all operations are performed successfully, or if one fails, then all operations are rolled back.
This logic can be applied to any other application.
In summary, the important thing is to apply Unit Of Work to every operation that needs to alter more than one table in the database, although there can also be scenarios where we call third-party APIs, but in 99% of cases, it's exclusive to the database.
3 - Implementing the Unit of Work Pattern in C#
Before we start, don't forget this code is part of a course, which has all the code available on GitHub.
To implement the unit of work pattern in C#, it is advisable to do so having already implemented (or while implementing) the repository pattern. That's how we're implementing it in this course.
First, let's set the context. In the previous post, we converted the user part to the repository pattern; in this case, we're going to convert WorkingExperience
.
public interface IWorkingExperienceRepository{ Task Insert(List<Workingexperience> workingExperiences);}public class WorkingExperienceRepository : IWorkingExperienceRepository{ private readonly CursoEfContext _context; public WorkingExperienceRepository(CursoEfContext cursoEfContext) { _context = cursoEfContext; } public async Task Insert(List<Workingexperience> workingExperiences) => await _context.Wokringexperiences.AddRangeAsync(workingExperiences);}
As we can see, we're not executing _context.SaveChanges()
because we will perform this save when we save the unit of work itself.
Now what we need to do is the implementation of Unit Of Work itself. For that, we simply create an interface called IUnitOfWork
and its corresponding class, which should inject our DbContext
and have the method to save changes.
public interface IUnitOfWork : IDisposable{ Task<int> Save();}public class UnitOfWork : IUnitOfWork{ private readonly CursoEfContext _context; public UnitOfWork(CursoEfContext context) { _context = context; } public async Task<int> Save() => await _context.SaveChangesAsync(); public void Dispose() { _context.Dispose(); }}
For now, this code doesn't do much; it just implements IDisposable
, and the reason is because DbContext
implements IDisposable
, so we must implement it as well. Here is a post where I explain IDisposable in detail.
What we will do now is to include, both in the interface and in the class, the repositories that we have created with the repository pattern.
public interface IUnitOfWork : IDisposable{ IUserRepository UserRepository { get; } IWorkingExperienceRepository WorkingExperienceRepository { get; } Task<int> Save();}public class UnitOfWork : IUnitOfWork{ public IUserRepository UserRepository { get; } public IWorkingExperienceRepository WorkingExperienceRepository { get; } private readonly CursoEfContext _context; public UnitOfWork(CursoEfContext context, IUserRepository userRepository, IWorkingExperienceRepository workingExperienceRepository) { _context = context; UserRepository = userRepository; WorkingExperienceRepository = workingExperienceRepository; } public async Task<int> Save() => await _context.SaveChangesAsync(); public void Dispose() { _context.Dispose(); }}
- Note: If you followed the previous post, you may recall that information was saved inside the repository. Make sure to remove the
savechanges
statement.
An intermediate point, but let's not forget, is to add everything we have created to the dependency injection container.
builder.Services.AddScoped<IUserRepository, UserRepository>();builder.Services.AddScoped<IWorkingExperienceRepository, WorkingExperienceRepository>();builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();
Now, what we have to do is refactor our previous code implementation to use the unit of work pattern.
Under the hood, it will work the same, but the implementation will be much easier to understand, maintain, and test.
//Original Codepublic class RelationsController : Controller{ private readonly CursoEfContext _context; public RelationsController(CursoEfContext context) { _context = context; } [HttpPost("InsertDataExample1")] public async Task InsertDataExample1() { User user1 = new User() { Email = $"{Guid.NewGuid()}@mail.com", UserName = "id1" }; List<Workingexperience> workingExperiences1 = new List<Workingexperience>() { new Workingexperience() { UserId = user1.Id, Name = "experience 1", Details = "details1", Environment = "environment" }, new Workingexperience() { UserId = user1.Id, Name = "experience 2", Details = "details2", Environment = "environment" } }; await _context.Users.AddAsync(user1); await _context.Wokringexperiences.AddRangeAsync(workingExperiences1); await _context.SaveChangesAsync(); }}
What I will also do, to simplify, is move the logic to a service, to what would be the logic layer so as not to have it in the controller. This action is completely optional but keeps the code cleaner.
- Note: Inject the service into the dependency injection container.
Before continuing, you have to keep one thing in mind: until you insert the first value in the database (User in our case), you do not have access to the Id for reference (in WorkingExperience), so the most common thing to do is to create a virtual property in your entity named the same as the table where you are going to insert, for example, in this sample, WorkingExperiences
has the userId
field, so we create a virtual User
property that references the table's entity.
public class Workingexperience{ public int Id { get; set; } public int UserId { get; set; } public virtual User User { get; set; } [MaxLength(50)] public string Name { get; set; } public string Details { get; set; } public string Environment { get; set; } public DateTime? StartDate { get; set; } public DateTime? EndDate { get; set; }}
As I said, this is a common practice and different ORMs can detect that the User Id field relates to your UserId property.
- Note: In Dapper, for example, you need to
BeginTransaction
and from there you get the Id, etc. It's a bit more complex.
However, this action can cause problems if your API returns the entity itself, which you shouldn't do (Difference between DTO and Entity), as it can cause circular reference issues at the level of the serializer you are using.
To avoid this, you need to include configuration for the Serializer:
builder.Services.AddControllers().AddJsonOptions(options =>{ options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;});
Now, everything should be working, we update the service to use IUnitOfWork
and refer to the user:
public class InsertUser{ private readonly IUnitOfWork _unitOfWork; public InsertUser(IUnitOfWork unitOfWork) { _unitOfWork = unitOfWork; } public async Task<bool> Execute(int id) { User user = new User() { Email = $"{Guid.NewGuid()}@mail.com", UserName = $"id{id}" }; List<Workingexperience> workingExperiences = new List<Workingexperience>() { new Workingexperience() { User = user, Name = $"experience1 user {id}", Details = "details1", Environment = "environment" }, new Workingexperience() { User = user, Name = $"experience user {id}", Details = "details2", Environment = "environment" } }; _ = await _unitOfWork.UserRepository.Insert(user); await _unitOfWork.WorkingExperienceRepository.Insert(workingExperiences); _ = await _unitOfWork.Save(); return true; }}
And we see that it works correctly.
With this, we have managed to implement transactions using the repository pattern.
If there is any problem you can add a comment bellow or contact me in the website's contact form