MediatR is a very popular library used to reduce dependencies between objects. It advocates an architecture based on very valuable design principles:
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.
Refer to the Without MediatR series for hands-on code examples how to replace MediatR with plain OOP.
Despite the name, MediatR does not implement the Mediator Pattern at all: it’s a Command Dispatcher.
Most of the times it’s used as a glorified method invocation, similarly to Service Locator, which is often an anti-pattern.
Classes that use it are forced to depend on methods they don’t use, violating the Interface Segregation Principle.
This creates an implicit, global coupling.
They also tend to violate the Explicit Dependencies Principle: instead of explicitly requiring the collaborating objects they need, they get a global accessor to the whole domain.
Domain code cannot have interfaces named after the domain-driven language
The domain code is polluted with Send
and Handle
methods
Domain classes are forced to implement interfaces defined in MediatR, ending up being coupled with a 3rd party library
The compiler gets confused and marks classes as unused. Workarounds are hacks.
Invoking the handler directly is about to 50x faster and allocates way less memory than invoking it through MediatR.
Good news is: MediatR can be easily replaced with trivial OOP techniques
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.
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:
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.
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.
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:
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.
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.
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.
Compare this with the class diagram we implemented before:
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.
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:
It is interesting to notice the following:
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.
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.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.
MyClass
has with the handlers defined in the project, except by reading its implementation.MyClass
’s constructor loses the ability to convey this information.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:
and implementing instead:
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:
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.
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
to
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.
Jimmy provided more details about this in a comment and a dedicated post, to which Mark answered with a comprehensive comment.
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.
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()
will get you to an externally defined method:
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:
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.
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.
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.
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.
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.
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.
For the review: