You probably don't need MediatR

Arialdo Martini — 18/12/2021 — C#

MediatR is a very popular library used to reduce dependencies between objects. It advocates an architecture based on very valuable design principles:

  • not allowing direct communication beetween objects, it promotes loose coupling
  • it leads toward a Message-Oriented Architecture
  • it supports an asynchronous in-process messaging
  • it promotes reuse object

These practices do resonate with me. Indeed, this seems to be something that should be completely obvious, a very reasonable way to build software. There is no doubt that the MediatR library is very successful and widely adopted among the developers who share these values.

While there are many articles about MediatR, only a minority of them focus on the drawbacks and help identify the cases where using it doesn’t make much sense.
This post is one of them.

In this article I wish to talk about when and why the adoption of MediatR might fail to deliver the expected outcome. In fact I think that, despite the good intentions, some ways of integrating MediatR can be detrimental in the pursue of the best design practices, and can even lead to anti-patterns.

Please excuse the fact that this article doesn’t cover the positive aspects of MediatR, only focusing on the flaws.

Show me the code

Refer to the Without MediatR series for hands-on code examples how to replace MediatR with plain OOP.

TL;DR for the impatient

The Mediator Pattern

MediatR presents itself as a

simple mediator implementation in .NET

But quickly reviewing what a Mediator Pattern is, reveals some surprises.
The Mediator Pattern is one of the classic behavioral patterns introduced by the seminal book Design Patterns: Elements of Reusable Object-Oriented Software, in 1994.

Do not be deceived by the academic reference. This won’t be an exercise in style. Indeed, most of the MediatR’s issues stem from the fact it deviates from the standard way the Mediator Pattern is implemented. So, please bear with me. Hopefully, the preamble will make it clear when and why using MediatR can deliver undesired outcomes.

The original GOF example

In its description of the motive for the pattern, the GOF book makes use of a specific example. I use the same example here, keeping the original names. This should make it easier to reference back to the original.
You can find a good summary of the book in Mihaylov Preslav’s Mediator Pattern Booknotes.

Consider the implementation of a dialog box in a graphical user interface, and assume it uses a collection of widgets such as buttons, menus, and entry fields.
Imagine the widgets have a set of dynamic and not trivial dependencies between them:

For example, a button gets disabled when a certain entry field is empty. Selecting an entry in a list of choices called a list box might change the contents of an entry field. Conversely, typing text into the entry field might automatically select one or more corresponding entries in the list box. Once text appears in the entry field, other buttons may become enabled that let the user do something with the text, such as changing or deleting the thing to which it refers.

That’s the messy dependency we wish to defend ourselves from:

A client class using a set of interdependent widgets

Encapsulate the complexity

Your client code needs to use the dialog box, but you want it to be saved from the complex inter-dependencies between widgets.

The gist of the Mediator Patter is that

you can avoid these problems by encapsulating collective behavior in a separate mediator object. A mediator is responsible for controlling and coordinating the interactions of a group of objects. The mediator serves as an intermediary that keeps objects in the group from referring to each other explicitly. [Your client code] only knows the mediator, thereby reducing the number of interconnections.

In the following diagram, aFontDialogDirector is our Mediator.

A client class using a set of interdependent widgets, via a Mediator

In the Mediator Pattern terminology, the widgets objects we want to abstract from are called Colleagues: they can interact with each other in many arbitrary ways, but our Client class doesn’t need to care, as their interaction is completely encapsulated inside the Mediator.
The Mediator is conventionally called Director, as its goal is to coordinate the Colleagues; it is injected into our client.

Implementing the Mediator Pattern

In summary: rather than getting the references to all the widgets, and having to deal with the complexity of their behavior, your client would conveniently receive just an instance of the intermediary mediator object.
The implementation is straighforward:

MyClient is decoupled from FontDialogDirector via the use of its interface

internal interface IFontDialogMediator
{
    void ShowDialog();
}

class FontDialogDirector : IFontDialogMediator
{
    private readonly ListBox _lisBox;
    private readonly Button _button;
    private readonly EntryField _entryField;

    FontDialogDirector(ListBox lisBox, Button button, EntryField entryField)
    {
        _lisBox = lisBox;
        _button = button;
        _entryField = entryField;
    }

    void IFontDialogMediator.ShowDialog()
    {
        // interacts with the 3 widgets,
        // taking care of the complexity of their interdependencies
    }
}

class MyClient
{
    private readonly IFontDialogMediator _fontDialogDirector;

    MyClient(IFontDialogMediator fontDialogDirector)
    {
        _fontDialogDirector = fontDialogDirector;
    }

    void DoStuff()
    {
        _fontDialogDirector.ShowDialog(); // encapsulates the complexity
    }
}

That’s it. This is plain old OOP, with a splash of dependency injection.

Observations

Notice the following facts:

  • Our Mediator object is related to the business use case we wished to encapsulate. It is domain-specific.

  • No surprises, then, that its interface is named after our specific domain language.

  • The same Mediator object could easily implement any other additional method our business use case needs. Each method would be conveniently named in terms of our domain language.

  • The project can easily include another different Mediator implementation, covering a distinct business use case. This different mediator would of course define a different set of business-driven methods, with their specific names.

  • MyClient can only interact with the font dialog. Should another mediator class exist, our MyClient would be conveniently kept decoupled from it. The received IFontDialogMediator mediator would not inadvertently expose MyClient to domain use cases that are not intended to be made available. In other words, by no means does injecting IFontDialogMediator make MyClient violate the Interface Segregation Principle.
    We will get back to this later.

MediatR

How does MediatR (the library) fit into this? Well, it doesn’t, really.

In the MediatR terminology, the class FontDialogDirector would be a Request Handler. MediatR requires that it implements a the specific interface IRequestHandler<U, V>, provided by the library. IRequestHandler<U, V> defines the method

Task<V> Handle(U request, CancellationToken cancellationToken)

As you see, this signature is not specific to your business case. Every handler using MediatR must implement the very same method, specialized via the type parameters U and V.

Very interestingly, according to MediatR you don’t inject FontDialogDirector into your client code. Instead, you are supposed to inject the instance of a third class, implementing IMediator, also provided by the library, to which you can send request objects. In turn, requests objects have to implement another interface, IRequest<V>, also provided by the library.

IRequest<V> is an odd interface: it’s defined empty, it contains no methods, and MediatR uses it via reflection, a practice that many consider a code smell, and which Microsoft reports as a violation of Code Quality Rule CA1040: Avoid empty interfaces.

Once sent to IMediator, your request objects would be finally dispatched to the Request Handler FontDialogDirector.

This might sound a bit confusing, and indeed it is. As a library that is supposed to implement of Mediator (the pattern), MediatR (the library) adds some extra layers of indirection which are not part of the original design.

A class diagram showing the extra layer implemented by MediatR

Compare this with the class diagram we implemented before:

MyClient is decoupled from FontDialogDirector via the use of its interface

MediatR’s motivation

One might ask why this design choice. A possible explaination is because MediatR just does not implement the Mediator Pattern, and has got a different motivation and goal.

Indeed, while the original GOF’s motivation is:

define an object that encapsulates how a set of objects interact […] and to vary their interaction independently.

reading the [MediatR Wiki][mediatr-documentation#basics], it sounds like the MediatR library’s goal is merely:

decoupling the in-process sending of messages from handling messages

This would explain why the example used in the MediatR wiki does not encapsulate any complex object interactions at all.

The basics with MediatR

Let’s analyze the MediatR Wiki code example in some detail:

class Ping : IRequest<string> { }

class PingHandler : IRequestHandler<Ping, string>
{
    public Task<string> Handle(Ping request, CancellationToken cancellationToken)
    {
        return Task.FromResult("Pong");
    }
}

class MyClass
{
    private readonly IMediator _mediator;
    
    MyClass(IMediator mediator)
    {
        _mediator = mediator;
    }
    
    void DoSomething()
    {
        var response = await _mediator.Send(new Ping());
        Debug.WriteLine(response); // "Pong";
    }
}

Here’s the same represented with a class diagram:

With MediatR

Observations

It is interesting to notice the following:

  • The whole construct is an indirect way to perform a method invocation, which resembles Service Locator:

var response = await _mediator.Send<MyCommand>(new MyCommand());

is essentially the same as:

var response = await _serviceProvider.GetRequiredService<IHandler<MyCommand>>()
  • In the Mediator Pattern lingo, PingHandler would be the Director, encapsulating the complexity of interactions between the Colleagues. Curiously, encapsulating complexity is not a topic the MediatR wiki mentions.

  • The Handler object does not implement an interface specific to our domain. On the contrary: our domain class now is forced to implement an interface defined in a third party library, which pertains more to the plumbing infrastructure than to the business logic.
    This apparently fails to satisfy one of the DDD tenets:

    “When a significant process or transformation in the domain is not a natural
    responsibility of an ENTITY or VALUE OBJECT, add an operation to the model
    as a standalone interface declared as a SERVICE.
    Define the interface in terms of the language of the model and make sure the
    operation name is part of the UBIQUITOUS LANGUAGE.
    Make the SERVICE stateless”
    (Eric Evans - Domain Driven Design, p.104)

    Indeed, in Domain-Driven Refactoring: Extracting Domain Services Jimmy Bogard clearly showed how to apply this principle to the Domain Services. Yet, MediatR’s Handlers keep being excluded, because they inevitably have to implement a domain unspecific, hard coded, single-method interface.

  • Our Mediator object cannot implement any additional methods. Handle() is the only method that is made available. There is no way to name the operation in terms of the domain language. In fact, the typical outcome of the adoption of MediatR is the spread of Send() and Handle() invocations, no matter the domain.
    Compare that to the sample class used in the Wikipedia Mediator Pattern’s page:
    class Mediator<T> {
      public void setValue(String storageName, T value) [...]
      public Optional<T> getValue(String storageName)  [...]
      public void addObserver(String storageName, Runnable observer) [...]
      void notifyObservers(String eventName) [...]
    }
    

    This is a very specific interface, based on the specific domain case. Implementing this with MediatR is just not possible.

  • Unexpectedly, MyClass does not receive the PingHandler, but an instance of IMediator. The consequences of this are brillantly dissected in an interesting post by Scott Hannen, No, MediatR Didn’t Run Over My Dog

  • When invoking Send() on IMediator, MyClass’ requests can be dispatched to a handler defined anywhere in the whole project. The Interface Segregation Principle has been violated, as IMediator acts similarly to a Service Locator. More on this later.

  • There is no way to infer what relation MyClass has with the handlers defined in the project, except by reading its implementation.
    MyClass’s constructor loses the ability to convey this information.
    This is a violation of the Explicit Dependencies Principle described by Microsoft in it Architectural principles, and a typical consequence of the Service Locator anti-pattern. You can read more about it in Mark Seemann’s classic Service Locator is an Anti-Pattern.

Back to plain OOP

Leaving aside that the example in MediatR (the library) is not based on Mediator (the pattern), it’s interesting to see how to get the same result with a simpler implementation.

Let’s see what happens getting rid of the extra layer of indirection, here represented in red:

With MediatR

and implementing instead:

With an interface

class Ping : IRequest<string> { }

class PingHandler : IRequestHandler<Ping, string>
{
    public Task<string> Handle(Ping request, CancellationToken cancellationToken)
    {
        return Task.FromResult("Pong");
    }
}

class MyClass
{
    private readonly IRequestHandler<Ping, string> _pingHandler;
    
    MyClass(IRequestHandler<Ping, string> pingHandler)
    {
        _pingHandler = pingHandler;
    }

    async Task DoSomething()
    {
        var response = await _pingHandler.Handle(new Ping());
        Debug.WriteLine(response); // "Pong";
    }
}

or, simplifying even further, and finally using domain names:

With an interface

record struct Ping; // The Query object

interface IPingHandler
{
    string Ping(Ping request);
}

class PingHandler : IPingHandler // Its handler
{
    string IPingHandler.Ping(Ping request) =>
        "Pong";
}

class MyClass
{
    private readonly IPingHandler _pingHandler;

    MyClass(IPingHandler pingHandler)
    {
        _pingHandler = pingHandler;
    }

    private void DoSomething()
    {
        var response = _pingHandler.Ping(new());
        Debug.WriteLine(response); // "Pong";
    }
}

Notice the main difference:

  • MyClass is not coupled with PingHandler: it depends on an abstraction, the IPingHandler interface. So, this implementation still decouples the in-process sending of messages from handling messages, which is the declared goal of MediatR.

  • MyClass’s constructor gets an instance of the query handler, rather than an instance of a 3rd component who knows how to reference the handler. That’s one level of indirection less. That’s the KISS principle in action.

  • MyClass does not depend on a 3rd party library.

  • This simplified implementation makes it apparent that the Request class PingRequest is fundamentally unnecessary: it was introduced by MediatR because of the extra level of indirection, as a way to infer which Handler to dispatch the call to. By using an interface, this can be further simplified.

More details

Glorified Method Invocation

Describing the Mediator Pattern, Ward Cunningham wrote:

When using MediatorPattern be careful to ensure that it does not become an (expensive) substitute for global variables and all the bad things that come with that AntiPattern. Ward Cunningham - Mediator Pattern

Compare the following:

// Implicit
IMediator mediator = GetMediatorSomehow();

var myClass = new MyClass(mediator);

with

// Explicit
IPingHandler ping = GetPingHandlerSomehow();

var myClass = new MyClass(ping);

While the the latter can only send Ping messages via IPingHandler, the former might emit the same, but also a Foo message for IFooHandler or any other message: dependencies just cannot be inferred from its interface.

This issue is akin to using a Service Locator: it changes the dependencies graph of a system from

Dependency without Service Locator

to

Dependency with Service Locator

Please, note how in the first diagram components are not tightly coupled: they can easily depend on abstractions by using interfaces, and therefore be decoupled as they should.

I took these 2 diagrams from Are you using MediatR? by Nam Duong. I suggest you to stop reading this post right away and take 3 minutes to check Nam’s article out. It’s the best, most concise and clearest explanation on this topic you can possibly find.

That MediatR usage resembles a Service Locator should not come as a surprise: despite being largely considered an anti-pattern (see Mark Seemann - Service Locator is an Anti-Pattern), the MediatR author believes it’s not and sees no problems using it in these circumstances.

Tweet by Jimmy Bogard

Jimmy provided more details about this in a comment and a dedicated post, to which Mark answered with a comprehensive comment.

It violates the Interface Segregation Principle

The Interface Segregation Principle states that

Clients should not be forced to depend upon interfaces that they do not use

As Scott Hannen notes:

Code that depends on MediatR is similar. We must look at every use of mediator and see what commands or queries are sent to it. There could be dozens of command and query handlers registered with MediatR. What restricts a class from sending a different command or query, something unrelated to the purpose of that class? Nothing. Those new dependencies are hidden, obscured behind the IMediator. MediatR opens the door to the same code smells as a service locator if we use it as a service locator.

Browsing code

There are 2 other unfortunate drawbacks when using MediatR.
The first is that browsing code is just less convenient.

With our hand-made Mediator, navigating from the Ping invocation gets you to its definition in IPingHandler or its implementation:

    private void DoSomething()
    {
        var response = _pingHandler.Ping(new());
        Debug.WriteLine(response); // "Pong";
    }

This does not work with MediatR: navigating from Send()

screenshot of a client using MediatR

will get you to an externally defined method:

definition of Send

One option you have is to navigate from PingRequest to its implementation; from there you can find its usages, and among them identify the handler. This is of course much less convenient than a single shortcut.

This happens also because the relationship between the client code and the mediator object (the Handler) is through the extra layer of indirection we’ve already seen:

A class diagram showing the extra layer implemented by MediatR

IntelliSense

The signature of ISender.Send() is:

Task<object?> Send(object request, CancellationToken cancellationToken = default)

The first parameter is a brutal object.
Therefore, expect IntelliSense to be helpless when trying to support you with MediatR.

Handlers are bound at runtime

The extra layer of indirection is the cause of another, possibly more subtle, drawback.

Checking if a PingRequest is handled by 0, 1 or more Request Handlers can only be done at runtime. The compiler has no way to help you making sure that a Request class is properly handled. You will probably need to mitigate this problem with some extra unit tests, as you cannot count on compilation errors.

Unused classes

The extra layers of indirection are also the reason why your Handler classes would be detected by the compiler as unused. This is a well known and annoying issue.

Unused classes

The classic workaround is to annotate all the handlers with the [UsedImplicitly] annotation, provided by the NuGet package JetBrains.Annotations. There are other worarounds you might use, which are described in Phil Scott’s UsedImplicitly on External Libraries).

This is specific to MediatR, not to the application of the Mediator Pattern: as a matter of fact, our simple homegrown Mediator Pattern implementation does not suffer from any of the issues above.

Interestingly, another popular package affected by the very same problem is AutoMapper, also by the same author.

Memory consumption and speed

Adam Renaud found out that calling the handler with MediatR incurs a relatively high overhead, which is 50x slower than calling the handler directly, with a much higher allocated memory.
In his (micro) benchmarks Adam measured that MediatR allocated over 1.67 GB of memory in 1 minute of execution, with over 2 seconds of GC time.
You can find more details in the post MediatR Performance Benchmarks.

Be aware, though, that Adam’s is a micro-benchmark: in real-world scenarios, these numbers shouldn’t probably worry you too much. In his video How slow is MediatR really? Nick Chapsas provides a very thorough analysis on that matter, which I suggest you to check out.

Conclusion

MediatR is not an implementation of the Mediator Pattern: it is instead an in-process bus, whose use is akin to the Service Locator pattern. It creates a coupling between your business code and interfaces externally defined, and in most of the cases an barely justifiable extra layer of indirection.
The chances are you can successfully implement a loosly coupled, message-oriented design replacing it with plain, old interfaces.

As the bottom line, I’d recommend @grauenwolf’s rule of thumb:

My advice is to ask people to build their solution without MediatR first. Then ask them to demonstrate, with specificity, what exactly they can’t do without adding MediatR.

References

For the review:

Comments

GitHub Discussions