Without MediatR - Request/response

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

Request/response

With MediatR

The request and the handler are defined as:

public class Ping : IRequest<string> { }

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

Invocation is done with:

class Client
{
    private readonly IMediator _mediator;

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

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

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

For this to work, the MediatR instance has to be informed that PingHandler is the handler of requests of type IRequest<Ping, string>:

var serviceProvider =
    new ServiceCollection()
        .AddTransient<IRequestHandler<Ping, string>, PingHandler>()
        .BuildServiceProvider();

mediator = new Mediator(serviceProvider);

Without MediatR

The handler can implement a domain-based interface, defining a Ping() method:

interface IPingHandler
{
    string Ping();
}

It then directly implements it:

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

Invocation is done with:

class Client
{
    private readonly IPingHandler _pingHandler;

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

    void ping_request_response()
    {
        var response = _pingHandler.Ping();

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

code

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. It is a legit implementation of the Dependency Inversion Principle, isn’t it?

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

Client depends on an abstraction of PingHandler

With MediatR Client depends on IMediator 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. This created an implicit, global coupling: as a matter of fact, Client can reach the whole application.
class Client
{
    private readonly IMediator _mediator;

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

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

With the OOP approach, Client has a restricted visibility of the external world, constrainted by confines of the IPingHandler interface. It it not forced to depend on any method it does not use. This adherence to the Interface Segregation Principle, is widely regarded as a hallmark of sound design.

  • With MediatR, 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)

There is no chance to infer the relationship between Client and Ping from the signature, but reading the implementation code.

The signature with the OOP approach is honest. The loose-coupled dependency to PingHandler is explicit and self-documenting. This makes the code compliant with the Explicit Dependencies Principle, which is considered a design best practice..

  • Because your handler must implement the interface defined by an external library, its Handle method has to return a Task even for strictly synchronous.

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:

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: it does not have the message!

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);
}

While there are benefits in clustering the parameters of a method in a separate class (for example, with Data clamp), the idea that only with it is the code performing a proper message oriented architecture is foundamentally a myth
In fact, technically speaking, in OOP there is no real difference between method calling and message passing. The “message” is not a class used to group the method parameters, but the method itself. In Simula, the first object-oriented, language, method call is described with object.message(arg1, arg2). In Smalltalk a protocol (that in C# we would call an interface) is defined as “the complete set of messages an object responds to”.

A message is a name that can be sent from one object to another, possibly with additional objects as arguments” (from What is Method Dispatch). Probably not the most familiar notion for us C# programmers, but it makes sense.

References

Comments

GitHub Discussions