Today, we are going to continue with what could be considered 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 several operations (usually involving a database) into a single "unit of work," ensuring that all those operations are either executed or not executed at all.
Sounds a lot like transactions, right? In reality, it is an abstraction layer on top of database transactions.
Note: As with the repository pattern, where DbSet
is its equivalent, DbContext
is the unit of work pattern. However, both myself and a vast number of developers consider DbSet
and DbContext
to be the database itself, so we prefer to have an abstraction layer right above it.
2 - Why Should We Use the Unit Of Work Pattern?
The easiest way is with an example: let’s say you have an e-commerce. A user places an order, which involves several steps such as checking inventory, creating the order, reducing the inventory, and finally, sending an email confirmation. Each of these steps modifies the state of your application (in a monolith; in distributed architectures it is different).
If something goes wrong in the middle of the process, you need to ensure the database is not left in an inconsistent state. This is where the Unit of Work Pattern is useful. It ensures that either all operations are completed successfully, or if any operation fails, all changes are rolled back.
This logic can be applied to any application.
In summary, it is important to use Unit Of Work for operations that require altering more than one table in the database, although there can also be scenarios where you call third-party APIs, but in 99 percent of cases, it will be exclusive to the database.
3 - Implementing the Unit of Work Pattern in C#
Before we get started, don’t forget this code is part of a course, and all code is available on GitHub.
To implement the unit of work pattern in C#, it’s recommended to already have (or be implementing) the repository pattern. That’s how it’s implemented in this course.
First, let’s set the context: in the previous post we converted the user part to use the repository pattern; in this case, we are 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 you can see, we are not calling _context.SaveChanges()
here because we will execute that save when we are ready to commit the unit of work.
Now what we need to do is actually implement the Unit Of Work. To do so, we simply create an interface called IUnitOfWork
and its corresponding class, which should inject our DbContext
and have a method for saving 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; in fact, what it does is implement IDisposable
, and the reason is that DbContext
implements IDisposable
, so we must implement it as well. Here is a post where I explain IDisposable in detail.
What we are going to do now is include, both in the interface and the class, the repositories we created using 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’ll remember that we used to save the information inside the repository. Make sure to remove the
savechanges
instruction.
An intermediate step, but let’s not forget, is to add everything we have created to the dependency container.
builder.Services.AddScoped<IUserRepository, UserRepository>();
builder.Services.AddScoped<IWorkingExperienceRepository, WorkingExperienceRepository>();
builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();
Now what we need to do is refactor our previous code implementation to use the unit of work pattern.
Internally it will work the same, but the implementation will be much easier to understand, maintain, and test.
//Original code
public 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();
}
}
To simplify further, I will also move the logic to a service - to the logic layer - so it’s not all in the controller. This step is completely optional but keeps your code cleaner.
- Note: Inject the service into the dependency container.
Before we continue, keep one thing in mind: until you insert the first value into the database (User in our case), you do not have access to the Id to create the reference (in WorkingExperience). So, what’s most common is to create a virtual property in your entity with the same name as the table you are referencing. For example, in this case, WorkingExperiences
has the field userId
, and we create a virtual User
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 mentioned, this is a common practice and most ORMs can detect that the User id refers to your UserId field.
- Note: In Dapper for example, you must use
BeginTransaction
, and only then you get the Id, etc. It is a bit more complex.
However, this action can cause problems if you are returning the entity from the API (which you shouldn’t, Difference between DTO and Entity), since it can lead to circular reference issues at the serializer level.
To avoid this, you need to configure the serializer accordingly:
builder.Services.AddControllers().AddJsonOptions(options =>
{
options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;
});
Now, everything is working; let’s modify the service to use IUnitOfWork
and reference 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 as you can see, 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