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
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
// Program.cs
+ using MediatR;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
+ builder.Services.AddMediatR(typeof(Program));
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; }
}
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);
}
}
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);
}
}
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
}
}
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; }
}
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
}
}
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);
}
}
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 { }
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
}
}
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.
Top comments (2)
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.
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!