DEV Community

Cover image for Publishing domain events with MediatR
Pierre Bouillon
Pierre Bouillon

Posted on

Publishing domain events with MediatR

When implementing your business logic inside your handler, you may want to perform some actions as a side effect of your processing.

Having those in your handler might bloat your code or result in duplication if you want to perform some of them in several handler.

MediatR Notifications might come handy in such cases, let's see how !

Setup

First, let's create setup our example. We will create a simple web API using .NET 6:

~$ dotnet new webapi -o MediatrNotification
Enter fullscreen mode Exit fullscreen mode

In your favorite editor, remove any occurrences of the initial boilerplate (WeatherForecast.cs and Controllers/WheatherForecastController.cs)

Finally, add MediatR and initialize it:

~$ dotnet add package MediatR
~$ dotnet add package MediatR.Extensions.Microsoft.DependencyInjection
Enter fullscreen mode Exit fullscreen mode
// Program.cs
+ using MediatR;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
+ builder.Services.AddMediatR(typeof(Program));
Enter fullscreen mode Exit fullscreen mode

Use case

For our demo, we will have a single endpoint, allowing someone to order a bouquet of flowers.

Based on that example, we can create our request in a new file PlaceBouquetOrderRequest.cs:

// PlaceBouquetOrderRequest.cs
public class PlaceBouquetOrderRequest : IRequest<Guid>
{
    public DateTime DueDate { get; init; }
    public int FlowersCount { get; init; }
    public string? Note { get; init; }
}
Enter fullscreen mode Exit fullscreen mode

and initialize the handler alongside it:

// PlaceBouquetOrderRequest.cs
public class PlaceBouquetOrderRequestHandler : IRequestHandler<PlaceBouquetOrderRequest, Guid>
{
    public Task<Guid> Handle(PlaceBouquetOrderRequest request, CancellationToken cancellationToken)
    {
        var orderId = new Guid();

        // Send the order to the merchant

        return Task.FromResult(orderId);
    }
}
Enter fullscreen mode Exit fullscreen mode

Finally, create the associated controller in Controllers/BouquetController.cs:

// BouquetController.cs
[Route("api/[controller]")]
public class BouquetController : ControllerBase
{
    private readonly IMediator _mediator;

    public BouquetController(IMediator mediator)
        => _mediator = mediator;

    [HttpPost("order")]
    public async Task<IActionResult> PlaceBouquetOrder([FromBody] PlaceBouquetOrderRequest request)
    {
        var orderId = await _mediator.Send(request);
        return Ok(orderId);
    }
}
Enter fullscreen mode Exit fullscreen mode

Adding the side effects

Our app is running great but now our client want us to also send an event to the merchant's calendar so that he can have an overview of its schedule.

Please note that we won't perform any validation here since that's not the goal of this tutorial

Let's go back to PlaceBouquetOrderRequest.cs and add the additional changes:

public class PlaceBouquetOrderRequestHandler : IRequestHandler<PlaceBouquetOrderRequest, Guid>
{
    public Task<Guid> Handle(PlaceBouquetOrderRequest request, CancellationToken cancellationToken)
    {
        var orderId = new Guid();

        // Send the order to the merchant
       SendReminderToCalendarAt(request.DueDate);

        return Task.FromResult(orderId);
    }

   private void SendReminderToCalendarAt(DateTime dueDate)
   {
       // Send a reminder to the merchant's calendar
   }
}
Enter fullscreen mode Exit fullscreen mode

The problem

Unfortunately, there are a couple issues that you might find from there:

  • Our PlaceBouquetOrderRequestHandler, once in charge of placing bouquets orders, is now also in charge of scheduling reminders: its scope is growing outside its original responsibility
  • The SendReminder logic could be reused somewhere else and would require either to duplicate the method or to extract it into a dedicated service. However, creating a service might result in an object altering the structure of the code, designed around handlers.

A solution

If we take a moment to think about it, the action requested is more about "doing something when an order has been placed" rather that just sending a reminder.

Fortunately MediatR has such an object to represent those events and handling them, they are called Notifications.

Let's create one to solve our case !

In a new BouquetOrderPlacedEvent.cs, create the following event:

// BouquetOrderPlacedEvent.cs
public class BouquetOrderPlacedEvent : INotification
{
    public Guid OrderId { get; init; }
    public DateTime DueDate { get; init; }
}
Enter fullscreen mode Exit fullscreen mode

We can now create an event handler able to process those kind of notifications:

// BouquetOrderPlacedEvent.cs
public class BouquetOrderPlacedEventHandler : INotificationHandler<BouquetOrderPlacedEvent>
{
    public Task Handle(BouquetOrderPlacedEvent notification, CancellationToken cancellationToken)
    {
        SendReminderToCalendarAt(notification.DueDate);

        return Task.CompletedTask;
    }

    private void SendReminderToCalendarAt(DateTime dueDate)
    {
        // Send a reminder to the merchant's calendar
    }
}
Enter fullscreen mode Exit fullscreen mode

And replace our former logic in our handler by the emission of this event:

// PlaceBouquetOrderRequestHandler.cs
public class PlaceBouquetOrderRequestHandler : IRequestHandler<PlaceBouquetOrderRequest, Guid>
{
    private readonly IPublisher _publisher;

    public PlaceBouquetOrderRequestHandler(IPublisher publisher)
        => _publisher = publisher;

    public Task<Guid> Handle(PlaceBouquetOrderRequest request, CancellationToken cancellationToken)
    {
        var orderId = new Guid();

        // Send the order to the merchant

         _publisher.Publish(new BouquetOrderPlacedEvent
        {
            OrderId = orderId,
            DueDate = request.DueDate
        });

        return Task.FromResult(orderId);
    }
}
Enter fullscreen mode Exit fullscreen mode

Going further

If we plan on handling new kinds of orders, we can generalize our event to an OrderPlacedEvent to abstract it from the kind of order it is:

// BouquetOrderPlacedEvent.cs
public abstract class OrderPlacedEvent : INotification
{
    public Guid OrderId { get; init; }
    public DateTime DueDate { get; init; }
}

public class BouquetOrderPlacedEvent : OrderPlacedEvent { }
Enter fullscreen mode Exit fullscreen mode

We can then make our handler generic so that it can handle any event derived from the base class OrderPlacedEvent:

public class OrderPlacedEventHandler<TOrderPlacedEvent> : INotificationHandler<TOrderPlacedEvent>
    where TOrderPlacedEvent : OrderPlacedEvent
{
    public Task Handle(TOrderPlacedEvent notification, CancellationToken cancellationToken)
    {
        SendReminderToCalendarAt(notification.DueDate);

        return Task.CompletedTask;
    }

    private void SendReminderToCalendarAt(DateTime dueDate)
    {
        // Send a reminder to the merchant's calendar
    }
}
Enter fullscreen mode Exit fullscreen mode

Note that if we had just changed our handler's definition to public class OrderPlacedEventHandler : INotificationHandler<OrderPlacedEvent>, MediatR would not have correctly route the event to our handler. You can read more about it on this issue.

Take aways

And voilà, we moved our logic into a dedicated handler, that might later also handle order of a different types. We also kept the logic of our handler as close as possible to its current use case and would also probably have reduced the handler's dependencies if we really implemented the communication with a third party calendar service.

Discussion (2)

Collapse
rafalpienkowski profile image
Rafal Pienkowski

It’s a lovely article describing the capabilities of MediatR. A thing that could interest you is the term “bounded context” introduced in Eric Evan's book. Also, a very underrated topic related to the bounded context is called “context map”.
In your example, I see two contexts: ordering and scheduling. Both are separated. That is why it makes sense to implement the logic in separate handlers. You introduce an event called “OrderPlacedEvent” in the Ordering context that has a side effect in the Scheduling context. The dependency between that two contexts could be reverted. A “ThingScheduledEvent” could make the job done. Now the The scheduling context is an “Upstream” context, and we could schedule things from other places without any change the handler is responsible for scheduling.

Collapse
pbouillon profile image
Pierre Bouillon Author

Thanks a lot for your feedback!

I didn't knew about the terminology, I will definitely check it out, it makes even more sense with a name on it!