Be very careful about the distinction between the mediator pattern and Mediatr, the library that provides tools to implement a version of the mediator pattern. In this answer, take note of when I used "mediator" and when I used "Mediatr".
TL;DR
I get the feeling that your judgment of Mediatr (and the mediator pattern) is based on some inflated expectation of how good you think it should be, and then judging it harshly on failing to meet that inflated expectation. If you look at the Mediatr GitHub page, it signals that its goals are much smaller than what you're trying to judge it by:
Simple, unambitious mediator implementation in .NET
And that's really different from what you're bringing up here as your concerns.
- Extra dependency (Mediatr itself)
If you're talking about package dependency, then package count is not a meaningful metric. Less is not inherently better. There is an added cost to adding packages to a codebase, but in turn they bring something to the table. The implicit point being made here is that packages should only be added if they add more than they subtract, at which point they're a net positive.
If you're talking about injected dependencies, i.e. as part of your "instead of just calling the commands directly" mention in the question, then the direct answer is that you only need to inject one mediator, instead of having to inject several different handlers each.
Sure, if your class only calls one handler, then replacing that one injected handler with an injected mediator is a neutral operation in terms of reducing the number of injected dependencies, but it still isn't a negative like you're claiming.
- The commands, controllers, etc. are tightly coupled to Mediatr (they won't work without it)
There's nothing stopping you from calling command/query handlers directly, but then you're not using the mediator pattern. Either you have a mediator, or you don't. The concept of having commands/queries and distinct handlers is separate from whether you access them via a mediator or not.
If your concern is that the injected mediator dependency is an interface that comes from the Mediatr library, and you would prefer for it to be a homebrew interface; nothing's stopping you from creating that.
But that's a different question. That's not "why use a mediator pattern?", that's more "should we always write full abstraction layers around every library we ever use?".
The answer to that latter question often ends up being a pragmatic one. Some libraries are considered to be external helpers that are a dime a dozen; at which point abstraction is very much desired. Other libraries are so foundational and trustworthy that the effort of writing a wrapper around them far outweighs the potential benefits, to the point where you might (a) not be able to efficiently write the wrapper without copy/pasting the library structure anyway and (b) not actually move away from the library and leverage that abstraction that you spent so long creating.
Often, we call these latter libraries "frameworks" to indicate that they are considered part of the backbone of the application, to the point where you can't meaningfully wrap them in an abstract wrapper.
Mediatr is often treated like a framework here, which is why devs tend not to get caught up on the fact that they're injecting a Mediatr interface as opposed to a mediator interface that they wrote on your own.
That doesn't mean you can't/shouldn't do it, but it might be a colossal waste of effort with a pay off that you never benefit from.
- Extra cognitive load - you need to know what Mediatr is doing / where the commands are that are being called by Mediatr to do the work, instead of just pressing F12 on the reference in the controller.
The separation of command and handler is intentional, specifically to avoid tight coupling between the two. There can be more than one handler. Not at the same time, but in the codebase; and your startup config might be composing a different set of handlers based on e.g. your user token or some kind of external configuration setting, or you might be relying on Mediatr's ability to decide handler priority based on relative assembly location.
In the cases where I don't have an expectation of multiple handlers, I make the handler a nested class of the command/query. I.e.
public class MyCommand
{
public int MyField { get; init; }
public class MyCommandHandler : IHandler<MyCommand>
{
public async Task Handle(MyCommand command)
{
// ...
}
}
}
This means you can still just as easily look up the handling logic based on the command, or vice versa.
The "extra cognitive load" argument is often a facile one. This tends to indicate that you're not familiar with something yet. Yes, it's technically correct that needing to learn something new introduces an additional cognitive load, but we should not be judging the introduction of a library solely by people who have never worked with it.
More importantly, we should judge whether using the library (when already knowing how to use it) request more/less/the same effort as doing the same thing in homebrew code without relying on the library.
Mediatr, at its very core, acts like a DI container, except that it looks handler (= "dependency") based on an input param type (i.e. your command/query). If that's all you use it for, yeah you could probably whip up something like that on your own in an hour or two, if you're comfortable with generics and either reflection or manually registering all handlers.
However, Mediatr has a boatload of additional features, such as DI container integration (for injected dependencies into your handlers), streams, notifications (i.e. multiple handlers for one notification), configurable pipeline steps, ...
Do you have to use those features? No. But it's not the library's fault for you not using the tools that it provides, nor should we conclude that the library should be avoided just because your personal use case might be a trivial one.
- More code - using Mediatr requires writing Mediator specific code (at minimum types to derive from and references to them so that Mediatr knows which classes are handlers).
Again we strike on the difference between CQRS and the mediator pattern. While they are a match made in heaven, they are two separate things.
The only thing that Mediatr (and mediators in general) require is that the handler uses a marker interface that indicates what it handles. Even that's not a hard requirement for the mediator pattern in general, but if you don't configure it via reflection, then the logical consequence is that you have to explicitly list which command/query goes to which handler.
I mean, technically you could even skip that and just do it using reflection at runtime, but that's not particularly easier. That's a sort of laziness that requires a more complicated implementation, which is counterintuitive.
- Performance overhead - This is probably small, but it is an overhead compared to just a simple method call.
As long as you don't resolve this using reflection on the fly, the performance overhead is so negligible that it probably took you more time to write that mention than the Mediatr overhead is ever going to cost you (as a life total).
but does having a rule that you must use Mediatr really make the actions any smaller than a rule that you must put all the logic in a separate command class / service class and just call a single method?
Well, no, but that's like saying "do I really need to buy a car if I design my own mode of transportation?"
Cars are just one tool to get you from point A to point B. A lot of people like being able to buy a car and not having to design one from scratch. Maybe you're not one of those people. That's fine, but please don't extend your personal observations as if they should apply to everyone else.
The controller pattern itself, when implemented properly, requires [..]
Cart before horse. "when implemented properly" entails designing your code with the appropriate patterns for the behavior you want, which can include making use of the mediator pattern to connect one component to another.
The mediator pattern seems to be designed to solve the problem of lots of components all calling each other and having dependencies on each other so you have a mediator to coordinate things but in ASP.Net it has the MVC pattern where there is only one entry point (the controller) and the problem doesn't seem to exist.
First of all, it is not recommended to daisy-chain commands (or queries) through Mediatr. While technically possible to do so, since each handler can in and of itself be a consumer of the mediator at the same time, this just enables a spaghetti codebase, sidestepping the organizational problem that the mediator problem is trying to help you solve, instead of actually solving it.
When you start daisy chaining, you get into several potential hard to catch issues like recursive pipeline steps that are being executed too many times, or conditionally infinitely recursing patterns which deadlock your application with no compiler warning whatsoever (only to be detected during runtime).
As to your observation about controllers, it's mostly irrelevant. The flow of control should generally be one-way, not "everyone calling everyone else". Whether the consumer of a mediator is a controller or any other kind of class is an irrelevant distinction.
I get the feeling that your judgment of Mediatr (and the mediator pattern) is based on some inflated expectation of how good you think it should be, and then judging it harshly on failing to meet that inflated expectation. If you look at the Mediatr GitHub page, it signals that its goals are much smaller than what you're trying to judge it by:
Simple, unambitious mediator implementation in .NET
And that's really different from what you're bringing up here as your concerns.