Let's talk today about the MediatR library, which in theory should help us make our applications cleaner and more decoupled, since, of course, it uses the mediator pattern.
Table of Contents
1 - What is MediatR?
MediatR is simply a library that implements the mediator pattern in C#, which allows us to communicate objects or classes in an indirect way. Therefore, to understand MediatR we must 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 manages the control and interaction of objects.
Basically, the MediatR library is that mediator. What we do instead of Class 1 calling Class 2, is that MediatR takes care of knowing, using handlers and types, where the call should go.
To see this more clearly, let's take a look at the following example, which is always easier with images.

Basically, in the image we can see an endpoint that simulates updating an item in a store, simulating changing the price. Obviously, our use case has a series of rules that we must maintain, which are validations, we save in the database, and it's possible we may need to invoke other use cases, for example, if the price drops by more than 30%, all users who have that item in their wishlist will receive an email. This use case can call more use cases, etc., and so on infinitely.
This structure makes our application very coupled, in other words, calls are made everywhere and everything is very messy.
This is where the mediator pattern comes in, basically giving us this object that acts as a mediator, which is in charge of tying it all together, but at the same time, everything is separated.

As we see in the image, EVERYTHING calls and is called from our mediator, including the endpoint.
Also, in the use cases, there's a change: we are no longer calling use cases directly; instead, we're notifying that something happened, and it will be the mediator that takes care of notifying whoever needs to be notified.
This is where MediatR enters. In C#, MediatR is the library that manages all this communication, knows which service needs to be called, and so on.
2 - Advantages of using the mediator pattern
The advantages are clear at first glance. The main one is decoupling: we're no longer making direct calls from one class to another, but instead using the mediator. This reduces dependencies and, of course, helps with maintenance and potentially with unit testing.
Many people will tell you the mediator pattern is essential for implementing CQRS, and that without MediatR (in .NET) it can't be done. The truth is, I don't subscribe to this view. You can apply CQRS without the mediator pattern perfectly well.
And finally, there's the subject of events: when a use case finishes (or really at any moment), we can (if we choose) have a notification command with the changes, which allows us to execute other processes asynchronously, without needing to have 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 cover a simple example to make the usage clear. As always, the code is available on GitHub; in fact, to compare, it's available both in a version with MediatR and one without.
We'll simulate the same example as before, where we modify an item. For simplicity, it's all in a single project, and in 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 first by understanding how things would be without using it.
For that, we have use cases. Other companies may call them services or whatever they want, I call things by their name. In our case we have two: UpdateItem and NotifyWishlist. The code is pretty straightforward. Something like this will 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 keep things simple, if something fails, I throw an exception, but you know I'm a fan of using Result<T>. Here's a link with the explanation and implementation.
In this case, we're executing the notification use case in the same process as the item update, which is not a very good practice. To avoid this wait, we could use a background task with Hangfire. But this is where the power of MediatR comes in, as we'll see shortly.
And then, the controller is nothing special, it just calls 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 happens if we implement the mediator pattern in this example.
3.2 - Example implementing the mediator pattern
Now let's move on to the code example using the MediatR library. As always, the first thing is to go to NuGet and install the MediatR package.
Once installed, we'll follow the "recommended" structure when using MediatR.
As I mentioned before, it's very common to separate by Commands and Queries, which means by modifications or queries. The response usually has the "response" term, and then each Command or Query needs a handler, which will execute the corresponding logic.
The first thing we'll do is create a command, since we're executing a modification. For that we'll 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 setting the type that will be returned by the handler we execute.
Now let's just 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;
    }
}It's similar but there are some differences, let's look at them in detail.
- First, the handler inherits from IRequestHandler<UpdateItemCommand, bool>, which is the command we created and the return type specified in the command. TheIRequestHandlerinterface is what MediatR uses to identify which handler goes with the type we've created.
- Next, instead of injecting our notification use case, we're injecting the IMediatorinterface. We'll see why in a moment.
- The Handlemethod is the entry point to our code, and in theory the only one that should be public. This is where the decoupling mentioned before kicks in, only this method and this logic are executed. If you notice, this method receives a parameter that's the first generic of IRequestHandler and the return type is the last one.- If our example was IRequestwith no generic type, Handle would simply not return anything (justasync Task Handle).
 
- If our example was 
- Validation and database insertion logic 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 in a MediatR pipeline, which we will also see later.
- Finally, the part where in my opinion MediatR shines: we're no longer calling the notification use case, but instead publishing a domain event. Here MediatR acts as a kind of in-memory service bus.
To publish these messages, we have to do two things. First, create our object to publish, which is a regular 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 handler for the notification, which will read the 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 MediatR logic is this. All that's left is to include it all in the dependency container, which is done with the AddMediatR(Asssembly) method in:
builder.Services.AddMediatR(cfg =>
{
    cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly()); 
});
And finally, create the controller that's going to 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're injecting the ISender interface, which is also part of MediatR and the one that does all the magic to call the corresponding handler for each event when executed.
To replicate the scenario we had before, that's all we have to do. Simple and easy to understand, although some may say that MediatR requires a lot of boilerplate code. Aside from this detail, there are no issues.
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 bit further, although this post won't cover it in detail, we can look at MediatR pipelines, which are nothing more than applying the decorator pattern in Mediatr.
With this feature, we can specify that handlers (specific or all) have a pipeline in MediatR, meaning that something happens both before and after.
Here's 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, the pipeline here contains 4 elements (in addition to the handler), and each of these IPipelineBehaviours allows you to configure code to be executed while the request or response is happening. They are executed in order.
Basically, it's the same thing you can do with middlewares and filters, but directly inside mediatr, so they act as a substitute for putting a global error handler, or for example, cache in one of the handlers, and so on.
Note: in the video we'll go more in detail into this scenario, which is taken from this GitHub repository.
4 - Using MediatR and the mediator pattern in companies
The implementation of the mediator pattern (like many other patterns) has been taken to an absolutely ridiculous extreme. Let me explain: I have mentioned that the mediator is in charge of managing all these calls, which at first seems great, but in practice it isn't so perfect.
Even though it's not the fault of the pattern or the library, but rather how it is used.
We find projects where, from a MediatR handler, another handler is being called, because before returning information to the user, it needs whatever that other handler is doing to be executed.
So, referring to 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 anything? Yes! The first image we saw, where everything was intertwined, only now on steroids, because not only is everything intertwined, we also have MediatR in the middle.
As you can see, there's a red line crossing from one use case to another. Does this remind you of anything? Yes! The first image we saw, where everything was intertwined, only now on steroids, because not only is everything intertwined, we also have MediatR in the middle.
In many cases, the new handler being called is used only by the current handler, which means that logic shouldn't be in a handler, but in a regular class.
But as I say, it gets taken to an extreme: "If it's not in a handler, it's wrong", and that assumption is ridiculous.
I've worked for years (and in many companies), so I've seen a lot in programming; a handler calling another handler can be problematic, but the wackiest thing I've ever seen is a custom abstraction on top of MediatR that allowed multiple generic types as input parameters. Obviously it didn't stop there, the project was quite large, and there were handlers injecting another 10 or 12, because, as I said, implementations go to ridiculous extremes, everything ends up poorly implemented, and in the end, what one day helped becomes just a hindrance.
Another very important point, which at first glance may not seem a big deal, is that MediatR does not make code navigation easy. For example, when you publish an event, you can't just hit F12 to see where that code goes; instead, you have to search text in the project or look up usages of the event you published. As I said, it's not a big problem if the project isn't huge and is well implemented, but in cases like the previous one, you could end up spending hours trying to find exactly what you're looking for.
I mentioned this on a Twitter post: I have never found a project where MediatR really helps. In the end they all turn into a mess of projects (csproj), excessive interface usage, SRP, all things I'm a fan of, but as always it's taken to absurdity and madness, which just leads to overengineering of code that should be much easier to read and understand.
Using MediatR or the mediator pattern, when well implemented, is wonderful. But these implementations quickly stray off course, making working in projects with this pattern very difficult. So, personally, although I know its advantages, I try to avoid its use.
If there is any problem you can add a comment bellow or contact me in the website's contact form
 
                    