In this blog post, we will discuss the "Unit of Work" pattern and how it can be used to implement domain events in a DDD application. For that, we will also discuss how we can leverage middleware to implement the "Unit of Work" pattern in a .NET application. A lot of things are going to happen in this blog post, so let's get started.
What is the "Unit of Work" pattern?
The Unit of Work pattern is useful because it centralizes the management of data persistence and transactions in an application. This pattern is particularly beneficial when working with complex applications where multiple operations need to be performed on a data store, such as a database. By employing the Unit of Work pattern, you can ensure that all changes are executed in a single transaction, which simplifies error handling and helps maintain data consistency and integrity.
Considering you have an e-commerce system, we have the following requirements if the user places an order:
- Create a new order with the selected products.
- Do the payment.
- Update the inventory to decrease the quantity.
- Dispatch the order to the shipping service, aka warehouse
From the user's point of view, that is one thing, but your system is probably and rightfully designed in a way that those are different concerns of your application. What happens now if we have services that do the transaction handling on their own? Now the following code is simplified and does not strictly follow DDD principles, but it is a good example of how things can go wrong.
public class OrderService
{
private readonly OrderRepository _orderRepository;
private readonly ProductRepository _productRepository;
private readonly UserRepository _userRepository;
public OrderService(OrderRepository orderRepository, ProductRepository productRepository, UserRepository userRepository)
{
_orderRepository = orderRepository;
_productRepository = productRepository;
_userRepository = userRepository;
}
public async Task PlaceOrderAsync(Order order, List<Product> products, User user)
{
// Create a new order
_orderRepository.AddOrder(order);
await _orderRepository.SaveChangesAsync();
// Update product inventory
foreach (var product in products)
{
_productRepository.DecreaseQuantity(product.Id, product.Quantity);
await _productRepository.SaveChangesAsync();
}
// Update the user's account balance
_userRepository.ChargeAccount(user.Id, order.TotalAmount);
await _userRepository.SaveChangesAsync();
}
}
What happens if products
consist of five elements and we get an exception in element two? Well, we somehow saved the order and decreased the quantity of the first product, but we did not update the user's account balance. So we have an order with a product that is not in stock, and the user's account is not charged. This is not good.
The Unit of Work pattern solves this problem by centralizing the management of data persistence and transactions in an application. Oversimplified it would look like this:
public class OrderService
{
private readonly OrderRepository _orderRepository;
private readonly ProductRepository _productRepository;
private readonly UserRepository _userRepository;
private readonly IUnitOfWork _unitOfWork;
public OrderService(OrderRepository orderRepository, ProductRepository productRepository, UserRepository userRepository, IUnitOfWork unitOfWork)
{
_orderRepository = orderRepository;
_productRepository = productRepository;
_userRepository = userRepository;
_unitOfWork = unitOfWork;
}
public async Task PlaceOrderAsync(Order order, List<Product> products, User user)
{
// Create a new order
_orderRepository.AddOrder(order);
// Update product inventory
foreach (var product in products)
{
_productRepository.DecreaseQuantity(product.Id, product.Quantity);
}
// Update the user's account balance
_userRepository.ChargeAccount(user.Id, order.TotalAmount);
// Save all changes
_unitOfWork.SaveChangesAsync();
}
}
Even if we have a problem in the middle of the loop, we can still rollback the changes and the user's account is not charged and the product is still in stock. This is a very simplified example, but it shows the power of the Unit of Work pattern.
To sum it up: A unit of work is a collection of operations that are treated as a single unit. We either want them all to succeed or all to fail.
Middleware and the Unit of Work pattern
If we have a look at the code above, we can see that we have a dependency on IUnitOfWork
in our OrderService
. This is not a good thing, because there is no need for the service to know such details. Hello SOLID principles! Probably if you are in a WebAPI application you would use a middleware to handle the Unit of Work pattern.
What is a middleware?
A middleware is a piece of software that sits in the processing pipeline between the web server and your application. Middleware components can handle various aspects of request processing, such as authentication, caching, routing, and logging. They can either process a request and pass it to the next middleware component in the pipeline or generate a response and short-circuit the pipeline.
This will be invoked on every request a user makes to your application. So we can use this to handle the Unit of Work pattern. We could create a middleware that will handle the Unit of Work pattern and will commit the changes to the database if everything goes well. If something goes wrong, we can roll back the changes.
How can we implement the Unit of Work pattern with middleware?
We can use the IMiddleware
interface to implement our middleware. This interface has a single method InvokeAsync
that will be invoked on every request. The method takes a RequestDelegate
and a HttpContext
as parameters. The RequestDelegate
is a delegate that will be invoked to continue the pipeline. The HttpContext
contains all the information about the request and the response. We can use this to handle the Unit of Work pattern.
public class UnitOfWorkMiddleware : IMiddleware
{
private readonly IUnitOfWork _unitOfWork;
private readonly ILogger<UnitOfWorkMiddleware> _logger;
public UnitOfWorkMiddleware(IUnitOfWork unitOfWork, ILogger<UnitOfWorkMiddleware> logger)
{
_unitOfWork = unitOfWork;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
try
{
await next(context);
await _unitOfWork.SaveChangesAsync();
}
catch (Exception)
{
_logger.LogError("An error occurred while saving the changes to the database.");
throw;
}
}
}
We can now register our middleware in our main method:
app.UseMiddleware<UnitOfWorkMiddleware>();
This InvokeAsync
method gets invoked every time a request is made to our application. So after everything is done (so our Controller code is executed), we can commit the changes to the database. If something went wrong, we don't commit the changes at all! With this, we have an atomic transaction that will either succeed or fail.
I never showed you what an implementation of the IUnitOfWork
interface looks like, but it is pretty simple. It has a method SaveChangesAsync
that will commit the changes to the database. If you have Entity Framework in your application, you already have the Unit Of Work pattern inside the DbContext
. So instead of the interface, you can leverage what you already have anyway (if all of your entities are under one DbContext):
public class UnitOfWorkMiddleware : IMiddleware
{
private readonly ApplicationDbContext _unitOfWork;
private readonly ILogger<UnitOfWorkMiddleware> _logger;
public UnitOfWorkMiddleware(ApplicationDbContext unitOfWork, ILogger<UnitOfWorkMiddleware> logger)
{
_unitOfWork = unitOfWork;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
try
{
await next(context);
await _unitOfWork.SaveChangesAsync();
}
catch (Exception)
{
_logger.LogError("An error occurred while saving the changes to the database.");
throw;
}
}
}
To recap before we continue: With the Unit of Work pattern as well as the usage of middleware, we can make sure that all changes to the database are atomic. We separated all the necessary stuff where it belonged. Our domain services don't have to know anything about this. But one problem is still there: Our OrderService
still has too much to handle and knows everything. So we have to change that. And this is where Domain Events come in.
Domain Events
Domain events are important occurrences within a system that signify something significant has happened. They help to communicate changes between different parts of a system, like different components or services. We will leverage the MediatR library to handle our domain events. MediatR is a simple mediator implementation in .NET. Furthermore, we will create a base class for all entities our program will use. The sole purpose is to have some code that handles:
public abstract class Entity
{
private readonly List<INotification> _domainEvents = new List<INotification>();
public int Id { get; set; }
public IReadOnlyCollection<INotification> DomainEvents => _domainEvents.AsReadOnly();
protected void AddDomainEvent(INotification domainEvent) => _domainEvents.Add(domainEvent);
}
INotification
comes from the MediatR library. It is a marker interface that is used to identify domain events. We can now create a domain event that will be raised when an order is placed:
public record OrderLineAddedEvent(int OrderId, OrderLine OrderLine) : INotification;
A handler can take this and react on it:
// MediatR event handlers
public class OrderCreatedEventHandler : INotificationHandler<OrderCreatedEvent>
{
public async Task Handle(OrderCreatedEvent notification, CancellationToken cancellationToken)
{
// Perform payment operation
}
}
Now there is a part missing in the whole picture. How does that play together with the Unit of Work pattern and the middleware I described earlier?
public class PublishMiddleware : IMiddleware
{
private readonly OrderDbContext _orderDbContext;
private readonly IMediator _mediator;
public UnitOfWorkMiddleware(OrderDbContext orderDbContext, IMediator mediator)
{
_orderDbContext = orderDbContext;
_mediator = mediator;
}
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
await next(context);
var changedEntities = _orderDbContext.ChangeTracker
.Entries<Entity>()
.Where(e => e.Entity.DomainEvents.Any())
.SelectMany(e => e.Entity.DomainEvents);
foreach (var changedEntity in changedEntities)
{
await _mediator.Publish(changedEntity);
}
}
}
And:
public class UnitOfWorkMiddleware : IMiddleware
{
private readonly OrderDbContext _orderDbContext;
public UnitOfWorkMiddleware(OrderDbContext orderDbContext)
{
_orderDbContext = orderDbContext;
}
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
await next(context);
await _orderDbContext.SaveChangesAsync();
}
}
Basically, we retrieve all events from changed entities and publish them. My assumption here is, that all entities that publish an event are also modified. Ot
We can now use this to handle our domain events. The second middleware will commit the changes to the database. The important and crucial part is that the order of middleware is important. The UnitOfWorkMiddleware
has to be invoked after the PublishMiddleware
.
app.UseMiddleware<PublishMiddleware>();
app.UseMiddleware<UnitOfWorkMiddleware>();
Otherwise, we save changes and publish our changes to other aggregates afterward, that then will not be stored to the underlying database. We created two middlewares because publishing and saving changes are two different concerns.
Conclusion
We built a small application that uses domain events to communicate between different parts of the system. We also used the Unit of Work pattern to make sure that all changes to the database are atomic. And every concern was separated into its own class. This is a very simple example, but it shows how you can leverage the power of domain events and the Unit of Work pattern to build a robust application.