Without MediatR - Request/response, subtyping

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

Request/response, subtyping

What happens sending a subtype of a request?

With MediatR

A handler defined as:

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

receives instances of both Ping and subtypes of Ping:

record Ping : IRequest<string>;
record SubTypeOfPing : Ping;

code

Without MediatR

Method dispatching in C# is polymorhic by design, so no surpsises that everything works out-of-the-box.

This equally succeds both with Ping and SubTypeOfPing:

record SubTypeOfPing : Ping;
record Ping;

class PingHandler : IPingHandler
{
    Task<string> IPingHandler.Handle(Ping request) => 
        Task.FromResult("Pong");
}


var response = await _client.UsePingHandler(new Ping());
Assert.Equal("Pong", response);

response = await _client.UsePingHandler(new SubTypeOfPing());
Assert.Equal("Pong", response);

code

FAQs

Which approach is preferrable?

Answer
C# exhibits a strong behavioral subtyping, so the OOP approach is compliant with the Liskov Substitution Principle, which states that subtypes must be substitutable for their base types without altering the correctness of the program.
So, this behavior is natively supported by the language and consistently applied.

With MediatR the polymorphic dispatch relies on the capabilities of the underlying dependency injection library.
The behavior is a bit more sensitive and depending on the setup you might encounter some surprises.

As an example, the following would result in an InvalidOperationException, despite AddTransient<IRequestHandler<Ping, string>, PingHandler>() looks like a legit registration:

file record SubTypeOfPing : Ping;
file record Ping : IRequest<string>;

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

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

var mediator = new Mediator(serviceProvider);
mediator.Send(new SubTypeOfPing()); // throws an exception

code

You may read more in the discussion at Issue - Polymorphic dispatch not working.

What happens if a handler for the subtype is also registered?

What if a handler for SubTypeOfPing is also registered?

public class SubTypeOfPingHandler : IRequestHandler<SubTypeOfPing, string>
{
    public Task<string> Handle(SubTypeOfPing request, CancellationToken cancellationToken)
    {
        return Task.FromResult("This is the handler for the subtype");
    }
}

Which handler will get the request, SubTypeOfPingHandler or PingHandler?

Answer
It all depends on the order of registration, because MediatR does not support registering multiple handlers for the same request.

References

Comments

GitHub Discussions