One of the patterns that I keep coming back to when building ASP NET Applications is the Command and Query Responsibility Segregation (CQRS) pattern. Fundamentally, the pattern separates the code to read (Query) and the write (Command) to the data store.
By separating the Commands and Queries, the code is more focused on the task performed. If you are familiar with the Create-Read-Update-Delete (CRUD) pattern, you can think of Queries as 'R' and Commands for 'CUD.'
The MediatR library provides an in-process messaging solution and enables applying CQRS pattern to our application code. The library support Commands, Queries, Notifications, Events, and a lot more make it easy to follow the CQRS pattern.
Let's see an example below of a Command handler for creating a new Order in an application.
The CreateOrderCommand
is issued from the UI to create a new Order. The CreateOrderHandler
handles this command and creates a new Order in the database, as shown below.
public class CreateOrderHandler : IRequestHandler<CreateOrderCommand, Unit>
{
private readonly IMediator _mediator;
private readonly OrderContext _context;
public CreateOrderHandler(IMediator mediator, OrderContext context)
{
_mediator = mediator;
_context = context;
}
public async Task<Unit> Handle(
CreateOrderCommand request, CancellationToken cancellationToken)
{
var order = request.ToOrder();
_context.Order.Add(order);
await _context.SaveChangesAsync();
return Unit.Value;
}
}
The Problem
Now let's say that when a new Order is created, we need to send a notification to the Shipping department and send an Email.
The easiest way to implement this, one might think, is to add in two more commands - SendShippingNotificationCommand
and SendOrderEmailCommand
and invoke these commands from the CreatedOrderHandler
as shown below.
public async Task<Unit> Handle(
CreateOrderCommand request, CancellationToken cancellationToken)
{
var order = request.ToOrder(rep);
_context.Order.Add(order);
await _context.SaveChangesAsync();
await _mediator.Send(new SendShippingNotificationCommand(order.Id));
await _mediator.Send(new SendOrderEmailCommand(order.Id));
return Unit.Value;
}
It works for now. As we build the application, let's say there are other code ways to create a new Order. e.g.
Converting a Quote to an Order
Manual Approval for Quotes that are above a certain price threshold etc.
Now these actions are to be modeled as different Command handlers - QuoteToOrderHandler
, ApproveQuotePriceHandler
etc. These handlers can easily read the Quote object and update the necessary properties to make it an Order.
But what about the side effects of creating an order? Sending notification to Shipping and sending an email?
We have to duplicate the calls to send the respective commands in both handlers above and in any more that creates a new order.
More than duplicating the code, we also need to keep track of the business processes to perform any time a new Order is created. It soon becomes a mess and hard to track.
Let's see how we can fix it!
Raise Domain Events
The problem with the above code is that it is coupling the action and reaction of creating a new Order. We can easily separate this using the concept of Events - Domain Events to be more precise.
When an order is created, we can publish an Event - OrderCreatedEvent
public class OrderCreatedEvent: INotification
{
public OrderCreatedEvent(int id)
{
Id = id;
}
public int Id { get; }
}
The handlers where an Order is created is no longer concerned about sending a shipping notification or email or anything else. All it does is to publish an event. This event is a Domain Event and is of importance to the business too.
If you notice the business talk, it will be When an Order is created, send a notification, an email, etc. So modeling our code as well around the event helps decouple these activities.
public async Task<Unit> Handle(
CreateOrderCommand request, CancellationToken cancellationToken)
{
var order = request.ToOrder(rep);
_context.Order.Add(order);
await _context.SaveChangesAsync();
await _mediator.Publish(new OrderCreatedEvent(order.Id));
return Unit.Value;
}
For these Events, you can create zero or more handlers. The handlers can perform the business processes in reaction to the event.
In the below example, I have a single handler, which then invokes the other actions required when an Order is created.
public class OrderCreatedEventHandler: INotificationHandler<OrderCreatedEvent>
{
public Task Handle(OrderCreatedEvent orderEvent, CancellationToken cancellationToken)
{
await _mediator.Send(new SendShippingNotificationCommand(order.Id));
await _mediator.Send(new SendOrderEmailCommand(order.Id));
}
}
Depending on the application use-case, this can be split into multiple handlers and have each handler perform a specific action. MediatR supports different Publish strategies that you can use.
With the code now decoupled using Events, we can raise an OrderCreatedEvent
any time it happens and be assured that event handlers will invoke all the related business processes. We no longer have to duplicate this logic in multiple command handlers or track all the associated business processes.
So the next time you are invoking a command from another command handler, take a step back and think. Is there a Domain Event that to be extracted here? Raise that event and let that drive the business process associated with that event!
Avoid calling Commands from other Command Handlers!
Top comments (0)