Without MediatR - Request/response, multiple registration

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

Request/response, multiple handlers, with reply

With MediatR

This pattern is not supported by MediatR. By design, Request/response messages are dispatched to a single handler.

Withot MediatR

There are no reasons why a message cannot be dispatched to multiple class instances, and their replies reported to the caller.

Indeed, this is a so common pattern in parallel / distributing computing that it has a name, MapReduce.

.NET supports this style: think for example to Task.WhenAll Method: it creates a task returning a collection of all the results of the completed Task<TResult>, for you to reduce.

OOP offers multiple patterns for implementing this style: for example, you might define the handlers as observers of your client with GoF Observer Pattern, or representing multiple targets as a single one, with Composite Pattern.

The idea of MapReduce is so pervasive that it is even embedded in C#. Given a collection of handlers, you can easily map the invocations to each instance with Select, and reduce the results with Aggregate:

handlers
    .Select(h => h.Handle(message))
	.Aggregate((accumulator, current) => combine(accumulator, current))`

So, let’s translate the dispatch to multiple handlers, to plain OOP, without using MediatR.
If you only want to collect all the return values, just use the map part of MapReduce:

class Client
{
    private readonly IEnumerable<IMyHandler> _handlers;

    internal Client(IEnumerable<IMyHandler> handlers)
    {
        _handlers = handlers;
    }

    internal void DispatchToAll()
    {
        var message = new SomeRequest("my message");

        var dispatchAll = _handlers.Select(h => h.DoSomething(message));

        Task.WhenAll(dispatchAll).Wait();
    }
}

code

You can combine this with the classical GoF Composite Pattern, so Client would not even know it is dealing with multiple handlers. In the following snippet, Handlers deals with MapReduce, offering Client an ordinary IMyHandler interface, identical to the one of a single handler:

class Handlers : IMyHandler
{
    private readonly IEnumerable<IMyHandler> _handlers;

    internal Handlers(IEnumerable<IMyHandler> handlers)
    {
        _handlers = handlers;
    }
    
    Task IMyHandler.DoSomething(SomeRequest request)
    {
        var dispatchAll = _handlers.Select(h => h.DoSomething(request));
        Task.WhenAll(dispatchAll).Wait();
        return Task.CompletedTask;
    }
}

class Client
{
    private readonly IMyHandler _handler;

    internal Client(IMyHandler handler)
    {
        _handler = handler;
    }

    internal async Task DispatchToAll()
    {
        var message = new SomeRequest("my message");

        await _handler.DoSomething(message);
    }
}

code

If you need to aggregate all the results to a single value, you are free to write your custom logic in Handlers, even to inject it as a replacable Strategy.

For the sake of simplicity, let’s have handlers returning booleans, and a call-site reducing the return values with a trivial and. Of course, there are no inherent constraints to scale this up to whatever complex data structure and domain logic:

class ValidatorA : IMyValidator
{
    bool IMyValidator.Validate(SomeRequest request)
    {
        // domain logic
        return true;
    }
}

class ValidatorB : IMyValidator
{
    bool IMyValidator.Validate(SomeRequest request)
    {
        // domain logic
        return false;
    }
}

Using again a Composite Pattern, the MapReduce logic would be:

class Validators : IMyValidator
{
    private readonly IEnumerable<IMyValidator> _handlers;

    internal Validators(IEnumerable<IMyValidator> handlers)
    {
        _handlers = handlers;
    }
    
    bool IMyValidator.Validate(SomeRequest request) => 
        _handlers
            .Select(h => h.Validate(request))  // map
            .Aggregate((acc, i) => acc && i);  // reduce
}

For the Client point of view, Validators is an ordinary handler — that’s loosly-coupling at work. Under the hood, Validate(request) dispatches request to all the validators, and returns back a consolidated, reduced value:

class Client
{
    private readonly IMyValidator _validator;

    internal Client(IMyValidator validator)
    {
        _validator = validator;
    }

    internal string DispatchToAll(SomeRequest request) => 
        _validator.Validate(request) 
            ? "all good!" 
            : "sorry, the request is not valid";
}

code

FAQs

Can’t this be applied with MediatR too?

Question
What about performing that MapReduce in a MediatR handler?

Answer
That would not work.
You can think of defining a handler for SomeRequest, and use its code to programmatically dispatch the same request to other handlers. But it’s not hard to realize you will be back to the same starting problem: there is no way to use MediatR for invoking those handlers; again, Request/response messages are dispatched to a single handler.
It the request dispatch is done manually, without the support to the MediatR pipelines, this would just defy the reason why MediatR was used in the first place.

C# already provides a very powerful and flexible message dispatching mechanism. It is type safe, checked at compile time, compatible by design with all the possible other OOP and FP patterns.
MediatR wrapped the native C# event dispatch with an abstraction, foundamentally reinventing it in a less flexible flavor.

I tend to listen to code: if something is hard to develop, either I’m trying to design the wrong solution, or I’m using the wrong tool.

References

Comments

GitHub Discussions