Without MediatR - Request/response, multiple registration

Arialdo Martini — 29/08/2023 — C# MediatR

Request/response, multiple registration

With MediatR

What happens if two different handlers for the same request are registered with MediatR?
Say you have HandlerA and HandlerB for the same request Echo:

public record Echo(string Message) : IRequest<string>;

public class HandlerA : IRequestHandler<Echo, string>
{
    public Task<string> Handle(Echo request, CancellationToken cancellationToken) => Task.FromResult(request.Message);
}

public class HandlerB : IRequestHandler<Echo, string>
{
    public Task<string> Handle(Echo request, CancellationToken cancellationToken) => Task.FromResult("doh!");
}

and that you record them in the IoC container:

cfg.Scan(scanner =>
{
    scanner.AssemblyContainingType(typeof(With));
    scanner.IncludeNamespaceContainingType<Echo>();
    scanner.WithDefaultConventions();
    scanner.AddAllTypesOf(typeof(IRequestHandler<,>));
});
            
cfg.For<IMediator>().Use<Mediator>();

Which response is returned by:

var echoResponse = await mediator.Send(new Echo("some message"));
        
Assert.Equal(???, echoResponse); 

Answer: it depends on the order the 2 handlers are defined in the file. Move HandlerB before HandlerA and the result will change.

That’s not a problem with IoC. The same also happens with a direct configuration of MediatR.

The issue is, by design MediatR dispatches the request to the last recorded handler, silently ignoring any other previously registered ones.

Without MediatR

This just does not apply to the OOP solution. You are in full control of which instance is injected into Client’s costructor.

Some IoC containers such as Autofac may optionally exhibit the same behavior, but you still have authority over the instances to get (for example, with the Autofac’s Default Registrations).

Furthermore, if you wish to deliver the same request to multiple handlers, that is easily done both

FAQs

The OOP solution creates coupling!

Doesn’t the OOP implementation create a strong coupling between Client and PingHandler?

Answer
No, it does not. Client and PingHandler are decoupled by IPingHandler.

Here’s a strong-coupled implementation:

class Client
{
    void ping_request_response()
    {
        var response = new PingHandler().Ping();

        Assert.Equal("Pong", response);
    }
}

Client directly depends on PingHandler and cannot be separated by it.
Injecting an instance of PingHandler would slightly improve the coupling, but not completely:

class Client
{
    private readonly PingHandler _pingHandler;

    internal Client(PingHandler pingHandler)
    {
        _pingHandler = pingHandler;
    }

    void ping_request_response()
    {
        var response = new PingHandler().Ping();

        Assert.Equal("Pong", response);
    }
}

In both the cases, Client depends on the PingHandler implementation.
In UML:

Client depends directly on PingHandler

This is high-coupling.

The conventional OOP implementation for loose coupling is based on the application of the Dependency Inversion Principle.
This mandates that

  1. High-level modules do not import anything from low-level modules, and both depend on abstractions.
  2. Abstractions do not depend on details. Details depend on abstractions.

Translated to our use case: Client should depend on an an interface, not directly on the implementation. In other words, coupling is obtained by interposing an IPingHandler between Client and PingHandler.

Client depends on an abstraction of PingHandler

class Client
{
    private readonly IPingHandler _pingHandler;

    internal Client(IPingHandler pingHandler)
    {
        _pingHandler = pingHandler;
    }

    internal string UsePingHandler()
    {
        // do work
        return _pingHandler.Ping();
    }
}

Isn’t this exactly what MediatR does?

MediatR too interposes an abstraction between Client and PingHandler. Therefore, isn’t it a legit implementation of the Dependency Inversion Principle?

Answer
No, it’s not. MediatR interposes an extra abstraction layer.

Client depends on an abstraction of PingHandler

With MediatR Client depends on IM-ediator which in turn depends on IRequestHandler<Ping, string>, implemented by PingHander.
It’s an unnecessary extra hop, that has negative effects.

But the more decoupling the better, right?

What’s the issue? An extra level of abstraction is harmless.

Answer
Unfortunately, it’s not. It brings some negative consequences:

  • Client now depends on IMediator, from which it can send whatever request to whatever handler.
class Client
{
    private readonly IMediator _mediator;

    internal Client(IMediator mediator)
    {
        _mediator = mediator;
    }

    void aync uses_ping_handler()
    {
        var response = await _mediator.Send(new LaunchRocket());
    }
}

This created an implicit, global coupling: as a matter of fact, Client can reach the whole application.
Technically, this is a violation of the Interface Segregation Principle.

  • The Client’s signatures are not honest anymore: you cannot infer which class Client depends on only by inspecting its signatures. Compare the constructors:
internal Client(IMediator mediator)

with

internal Client(IPingHandler handler)

This is a violation of the Explicit Dependencies Principle.

This is all theoretical

I don’t see any practical problem. Only academic fixations.

Answer
There are pragmatic consequences. You will find some examples in the next pages of this article:

  • If you record more than one handler, they will be silently ignored
  • You cannot send a request to multiple handlers
  • Sending a subclass of a Request results in a failure

The OOP solution violates CQRS!

The MediatR solution has got a Ping class to represent a Command or a Query object. This is CQRS. The OOP solution is inferior.

Answer
If you mean that methods should either be commands performing an action, or queries returning data without side effects, you mean CQS, not CQRS.
In fact, conforming to CQS does’t necessarily require a class for any request. You are free to define one, though, if you like:

file class Ping{}

file interface IPingHandler
{
    string Ping(Ping ping);
}                           

file class PingHandler : IPingHandler
{
    string IPingHandler.Ping(Ping ping) => "Pong";
}

Fittingly, the compiler warns that ping is unused and completely redundant. And this makes sense, if you think about it.
In MediatR for each and every method call a dedicated Request class must be defined, even if it does not have any property — so even if it physically transport no information at all &mdash. MediatR always needs a marker class to figure out which handler to dispatch the call to.
The plain OOP approach does not need that: method dispatching is directly performed with the native language mechanism based on methods, interfaces and classes, which is unsurprisigly sounder and more robust (for example, sending a subclass of a Request does not result in a failure, as it does with MediatR).

In conclusion: you are free, but not forced, to use a request class. You have the full flexibility to use any of the following approaches, and still be CQS compliant:

file interface IPeopleService
{
    string DoSomething(PersonRequest person);
}

file interface IPeopleService
{
    string DoSomething(Person person, bool someCondition, string message);
}

References

Comments

GitHub Discussions

[command-object]: