DEV Community

Cover image for Why You Should Avoid Command Handlers Calling Other Commands?
Rahul Nath
Rahul Nath

Posted on • Originally published at rahulpnath.com

Why You Should Avoid Command Handlers Calling Other Commands?

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.'

CQRS Pattern

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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; }
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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));
    }
}
Enter fullscreen mode Exit fullscreen mode

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)