Let's talk today about the MediatR library, which in theory should help us make our applications cleaner and more decoupled, since it uses the mediator pattern.
Index
1 - What is MediatR?
MediatR is simply a library that implements the mediator pattern in C#, which allows us to communicate objects or classes indirectly. So to understand MediatR, we need to first understand what the mediator pattern is.
1.1 - What is the mediator pattern?
The mediator pattern is a design pattern used to reduce the complexity of communication between multiple classes. What it does is provide us with a mediator that handles the control and interaction of objects.
Basically, the MediatR library is that mediator. What we do is, instead of Class 1 calling Class 2, MediatR knows where to direct the call using handlers and types.
To see it a bit better, let's look at the following example, as it's always easier with images.
Basically, in the image we can see an endpoint simulating updating an item in a store, which simulates changing the price. Obviously, our use case has a series of rules we must maintain, which are validations, saving to the database, and possibly invoking other use cases. For example, if the price drops by more than 30%, all users who have that item in their wishlist will get an email. This use case may call other use cases, and so on, without end.
This structure makes our application very coupled, in other words, it makes calls everywhere and everything is a mess.
This is where the mediator pattern comes in, basically giving us an object that acts as the mediator, bringing everything together, but at the same time, everything is separated.
As shown in the image, EVERYTHING is called from and calls to our mediator, including the endpoint.
Additionally, in use cases, we see a change: we are no longer directly calling use cases but notifying that something has happened, and it will be the mediator that is responsible for notifying whoever needs to be notified.
This is where MediatR comes in. In C#, MediatR is the library that takes care of all this communication, knowing which service needs to be invoked, etc.
2 - Advantages of using the mediator pattern
The advantages are clear at first glance. The main one is decoupling: we are no longer calling from one class to another, but using the mediator, which reduces dependencies and of course helps with maintenance and, potentially, unit testing.
Many people will tell you that the mediator pattern is essential for CQRS and that without MediatR (in .NET) you cannot implement it. In truth, I don't agree. CQRS can be applied without the mediator pattern just fine.
And finally, events. When a use case finishes, or really at any moment, we (if we want) have a notification command with the changes, which lets us run other processes asynchronously without having background tasks running constantly, and of course, without needing a service bus as part of the infrastructure.
3 - Implementing the mediator pattern in C# with MediatR
Now let's move to the more practical part, and we'll see it with a simple example to make the use clear. As always, the code is available on GitHub; in fact, to compare, it's included both with MediatR and without MediatR.
What we're going to simulate is the previous example, where we modify an element. For simplicity, everything is in a single project, and from the repository, we only have the interface: the implementation is a Fake, to save time:
public interface IDatabaseRepository{ Task<bool> UpdateItem(int itemId, decimal newPrice, string title); Task<ItemDto> GetItemById(int id);}/// <summary>/// This is only to simulate the example./// </summary>public class FakeDatabaseRepository : IDatabaseRepository{ public Task<bool> UpdateItem(int itemId, decimal newPrice, string title) { return Task.FromResult(true); } public Task<ItemDto> GetItemById(int id) { return Task.FromResult(new ItemDto(id, 12, "Title string")); }}
Note: ItemDto
is very simple, just Id, price, and title.
3.1 - Implementation without MediatR
Most of you are here because you're not familiar with the mediator pattern, so in my opinion, the easiest way to understand what we're doing is to first understand how it would be without using it.
For this, we have use cases—other companies may call them services, or whatever—but I like to call things by their name. In our case, we have two: UpdateItem
and NotifyWishlist
. The code is quite simple; something like this would do the job:
public class UpdateItem{ private readonly IDatabaseRepository _databaseRepository; private readonly NotifyWishlist _notifyWishlist; public UpdateItem(IDatabaseRepository databaseRepository, NotifyWishlist notifyWishlist) { _databaseRepository = databaseRepository; _notifyWishlist = notifyWishlist; } public async Task<bool> Execute(ItemDto itemToUpdate) { if (itemToUpdate.Title.Length > 200) throw new Exception("Title must be less than 200 characters"); if (itemToUpdate.Price <= 0) throw new Exception("It can't be free"); ItemDto existingItem = await _databaseRepository.GetItemById(itemToUpdate.Id); await _databaseRepository.UpdateItem(itemToUpdate.Id, itemToUpdate.Price, itemToUpdate.Title); decimal percentageDifference = ((itemToUpdate.Price - existingItem.Price) / existingItem.Price) * 100; if (percentageDifference <= -30) { await _notifyWishlist.Execute(itemToUpdate.Id); } return true; }}public class NotifyWishlist{ public Task Execute(int id) { //We dont need this working for the example. Console.WriteLine("Logic to get who is wishlisting that ID and send the emails"); return Task.FromResult(true); }}
To simplify, if something doesn't work, I throw an exception, but you know I'm more in favor of using Result<T>
. Here's a link with the explanation and implementation.
In this case, we're running the notification use case in the same process as updating the item, which is not good practice. To avoid this wait, we could use a background task with hangfire. But this is where the strength of MediatR
comes in, as we will see in a moment.
And then the controller is nothing special, we simply call the use case:
[ApiController][Route("[controller]")]public class DefaultExampleController : ControllerBase{ public readonly UpdateItem _UpdateItem; public DefaultExampleController(UpdateItem updateItem) { _UpdateItem = updateItem; } [HttpPut("item")] public async Task<bool> UpdateItem(ItemDto itemDto) => await _UpdateItem.Execute(itemDto);}
This would be a normal or default process. Now let's see what this example looks like if we implement the mediator pattern.
3.2 - Example Implementing the mediator pattern
Now let's look at the code example using the MediatR library. As always, start by going to Nuget and installing the MediatR
package.
Once installed, we'll follow the "recommended" structure when using MediatR.
As I said before, it's very common to separate by Commands and Queries, which basically means by modifications or queries. The response usually has the term "response," and each Query or Command requires a handler that will be responsible for executing the corresponding logic.
The first thing we will do is create a command since we are going to execute a modification. For this, we do the following:
public record UpdateItemCommand : IRequest<bool>{ public required int Id { get; init; } public required decimal Price { get; init; } public required string Title { get; init; }}
As you can see, this command inherits from IRequest<T>
, and this is important because when we create a command, we can specify it with IRequest
or IRequest<T>
. If we indicate the type, we're defining what type the handler will return when executed.
Now we simply create the handler, very similar to the previous use case:
public class UpdateItemCommandHandler : IRequestHandler<UpdateItemCommand, bool>{ private readonly IDatabaseRepository _databaseRepository; private readonly IMediator _mediator; public UpdateItemCommandHandler(IDatabaseRepository databaseRepository, IMediator mediator) { _databaseRepository = databaseRepository; _mediator = mediator; } public async Task<bool> Handle(UpdateItemCommand request, CancellationToken cancellationToken) { if (request.Title.Length > 200) throw new Exception("Title must be less than 200 characters"); if (request.Price <= 0) throw new Exception("It can't be free"); ItemDto existingItem = await _databaseRepository.GetItemById(request.Id); await _databaseRepository.UpdateItem(request.Id, request.Price, request.Title); await _mediator.Publish(new ItemUpdated() { Id = request.Id, NewPrice = request.Price, NewTitle = request.Title, OldPrice = existingItem.Price, OldTitle = existingItem.Title }, cancellationToken); return true; }}
Similar but with some differences. Let's see those differences in detail.
- First of all, the handler inherits from
IRequestHandler<UpdateItemCommand, bool>
, which is the command we created and the return type specified in the command. TheIRequestHandler
interface is what MediatR uses to identify that this handler is matched to the type we created. - If we look further, we see that we haven't injected our notification use case, but instead, we're injecting the
IMediator
interface, and we'll see why in a moment. - The
Handle
method is the entry point of our code, and theoretically the only one that should be public. Here is where the decoupling mentioned above comes in: only this method and this logic will be executed. If you notice, this method receives an input parameter which is the first generic of IRequestHandler and the return type is the last one.- If our example was
IRequest
without a generic type, Handle simply doesn't return anything (justasync Task Handle
).
- If our example was
- The validation and insertion to the database part is the same, although you'll see many examples where validation is done with Fluent Validations, which allows you to separate, once again, the logic, decoupling the code, or within a MediatR pipeline that we'll also see later.
- Finally, the part where, in my opinion, MediatR shines: we are no longer calling the notification use case, but instead publishing a domain event. Here, MediatR acts as an in-memory service bus (producer/consumer pattern).
To publish these messages, we have to do two things. First, create our object to publish, which is a normal object that inherits from INotification
.
public record ItemUpdated : INotification{ public required int Id { get; init; } public required decimal NewPrice { get; init; } public required decimal OldPrice { get; init; } public required string NewTitle { get; init; } public required string OldTitle { get; init; }}
And then we have a notification handler, which will read this event and process it:
public class ItemUpdatedEventHandler : INotificationHandler<ItemUpdated>{ //We dont need this working for the example. public async Task Handle(ItemUpdated notification, CancellationToken cancellationToken) { decimal percentageDifference = ((notification.NewPrice - notification.OldPrice) / notification.NewPrice) * 100; if (percentageDifference <= -30) { Console.WriteLine("Logic to get who is wishlisting that ID and send the emails"); } }}
And that's it, the main logic of MediatR is this. What remains is to include everything in the dependency container, which is done with the AddMediatR(Assembly)
method in:
builder.Services.AddMediatR(cfg =>{ cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly()); });
And finally, create the controller that will execute our handler.
[ApiController][Route("[controller]")]public class MediatRExampleController : ControllerBase{ private readonly ISender _sender; public MediatRExampleController(ISender sender) { _sender = sender; } [HttpPut("item")] public async Task<bool> UpdateItem(ItemDto itemDto) => await _sender.Send(new UpdateItemCommand() { Id = itemDto.Id, Price = itemDto.Price, Title = itemDto.Title });}
Here we can see that we inject the ISender
interface, which is also part of MediatR and is responsible for all the magic to call the corresponding handler for each event when executed.
To replicate the scenario we had before, that's all we need to do. Simple and easy to understand, although some might say MediatR requires a lot of boilerplate code. Aside from this detail, it's fine.
3.2.1 - Custom Pipeline in MediatR
This is the code example; as we see, it's very simple using MediatR. If we want to go a little further, although in this post we won't cover it in much detail, we can look at MediatR pipelines, which are essentially applying the decorator pattern in MediatR.
With this feature, we can specify that handlers (specific or all) have a pipeline inside MediatR, meaning that something happens both before and after.
Here is an example:
services.AddMediatR(cfg => { cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly()); cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(UnhandledExceptionBehaviour<,>)); cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(AuthorizationBehaviour<,>)); cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(ValidationBehaviour<,>)); cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(PerformanceBehaviour<,>));});
As we see here, the pipeline contains 4 elements (in addition to the handler), and each IPipelineBehaviour
allows configuring code to run as the request or response happens, and they run in order.
Basically, it's the same thing we can do with middlewares and filters but directly within MediatR, so they act as substitutes for adding a global error handler, or for example, caching in one of the handlers, etc.
Note: in the video, we'll go into more detail on this scenario, which is taken from this GitHub repository.
4 - The use of MediatR and the mediator pattern in companies
The implementation of the mediator pattern (like many other patterns) has sometimes gone to absolutely ridiculous extremes. Let me explain: I mentioned that the mediator is responsible for managing all these calls, which at first sounds great, but in practice, it's not always so good.
Although it's not the fault of the pattern or the library, but of how it's used.
We encounter projects where from a MediatR handler, another handler is called, because before returning information to the user, whatever that other handler does needs to be executed first.
So, for the previous image, we have something like the following:
As you can see, there's a red line crossing from one use case to another. Does this remind you of something? Yes! The first image we saw, where everything was interconnected—now even worse, because not only is everything tangled, but we also have MediatR in between.
In many cases, the new handler being called is only being used by the current handler, which means this logic shouldn't be in a handler but in a normal class.
But as I say, people push it to the extreme: "if it's not in a handler, it's wrong," and that assumption is ridiculous.
I've worked for many years (and in many companies) and that means I've seen a lot in programming; having one handler call another can be a problem, but the most bizarre thing I've seen is a custom abstraction on top of MediatR that allows several generic types as input parameters. Of course, it doesn't stop there: the project was quite large, and there were handlers injecting 10 or 12 others, because as I say, implementations are pushed to ridiculous extremes. Everything ends up badly implemented, and what once helped now just makes things cumbersome.
Another very important point, which at first glance you might think is not a big problem, is that MediatR does not make code navigation easy. For example, when you publish an event, you can't hit F12 to see where that code goes; instead, you have to search for the event text in the project or search for usages. As I say, it's not a big problem if the project isn't huge and is well implemented, but in cases like the above, you can spend hours trying to find exactly what you're looking for.
I said this in a twitter post: I've never found a project where MediatR truly helps; in the end they all end up a mess of projects (csproj), excessive use of interfaces, SRP, and although I'm a fan, as always it's pushed to the absurd and various craziness that only end up overengineering code that should be much easier to read and understand.
Using MediatR or the mediator pattern, when implemented well, is wonderful. These implementations get derailed very quickly, making it hard to work with projects where this pattern is applied, so personally, even though I know its advantages, I tend to avoid using it.
If there is any problem you can add a comment bellow or contact me in the website's contact form