Reinventando a Roda: Criando seu próprio MediatR - Parte 1

Quando o homem inventou a roda Logo Deus inventou o freio, Um dia, um feio inventou a moda E toda roda amou o feio. Zeca Baleiro é um gênio. Se você discorda, você está errado. A roda toda abraçou o MediatR. Virou moda. É uma das bibliotecas mais usadas no mundo do dotnet. Porém Jimmy Bogard, autor da lib resolveu colocar um freio na nossa alegria e resolveu transformar essa biblioteca em um produto pago: https://www.jimmybogard.com/automapper-and-mediatr-going-commercial/. Não critico. Open Source geralmente é um belo de um "Venha a nós e ao Vosso Reino, nada!": Centenas de milhares de empresas usando, milhões de desenvolvedores reclamando e ninguém contribuindo. Simples assim. Nem eu, nem você e muito menos as nossas empresas pensam nisso. É fato! Para manter uma biblioteca como essa, o autor ou a autora (e sim, geralmente é uma pessoa só ou um grupo bem pequeno de pessoas) despende horas e horas e horas de trabalho e muita dedicação, dividindo seu tempo entre a empresa, projeto e a familia. "Nem relógio trabalha de graça!" já diria meu velho pai! Nem ele, nem eu e nem você. A conta chegou e o grande Jimmy Bogard tomou a decisão que só ele poderia tomar. Eu apoio! E, acima de tudo agradeço por todo excelente trabalho feito. Coloquei muito sistema em produção usando o MediatR. Sou grato por isso. E acredito que você também. Pois bem, seguimos. Antes de tudo preciso destacar aqui que esse é o primeiro de uma série de três posts. Preferi dividir o assunto em três partes pois fiquei traumatizado com o post Reinventando a Roda: Criando uma Rede Neural com csharp já que foram muitos e muitos dias de estudo que se transformaram em muitos dias de escrita de código e que acarretaram num post que leva aproximadamente 35 minutos para a leitura completa. Oremos. Nessa primeira parte quero abordar a criação e invocação do Command Handler (IHandler e ISender) apenas. E quero trazer algumas melhorias que eu julguei interessantes. Pelo menos para mim são interessantes. No segundo post nós vamos avançar incluindo um Filter Handler, equivalente ao Pipeline Behavior. E por fim, na terceira parte, vamos adicionar a feature de envio de notificações, o famigerado Publish! Eu sei... existem várias outras features como stream por exemplo, mas quero focar nessas três apenas! Se tudo der certo e nada der errado, crio uma DLC do post incluindo um sistema automágico de validação de Request. É algo que eu já poderia entregar na primeira parte, mas acho que o post ficaria grande demais (e eu nem sei se essa é uma boa ideia. Veremos...). Certo, eu acredito que nem todo mundo está familiarizado com a biblioteca MediatR e com o pattern Mediador. Esse post não vai abordar o conceito em si, para isso acesse https://refactoring.guru/design-patterns/mediator - um dos sites mais incríveis que eu já vi na vida. Aliás, a imagem de capa desse post foi usurpada e modificada digitalmente pelo ChatGPT. Espero que eles não me processem, mas o crédito é todo, todinho deles! Eu também não vou me aprofundar na biblioteca, vou falar apenas superficialmente, porém caso queira saber mais sobre, recomendo esse post Playground: MediatR do grande William Santos. É importante destacar que o código abaixo - apesar de achar ele bem escrito e funcional - não foi feito para ser usado em produção. Pelo menos nesse estágio de desenvolvimento. Aliás, em nenhum estágio, já que eu não tenho nenhum interesse de manter esse código. E esse é um dos fatores fundamentais para a escolha de um produto/framework/serviço: alguém vai precisar manter, atualizar, corrigir erro e acima de tudo, evoluir o código. Não é o meu caso. Ok, vamos ao que viemos. Pegue aquela xícara de café marota e vem comigo. Crie um projeto do tipo Class Library. Dê o nome que preferir, mas escolha um nome pomposo. Eu dei o nome de TheMediator.Core. Robusto. Certo! Vamos começar com o Handler. Um handler recebe um command (no nosso caso chamaremos de request) e pode ou não devolver um Response. Logo, precisamos ter dois métodos, um que retorna valor e o outro retorna void. Para representar esse Handler criaremos duas interfaces: Arquivo IHandler.cs: public interface IHandler { Task HandleAsync(TRequest request, CancellationToken cancellationToken); } public interface IHandler { Task HandleAsync(TRequest request, CancellationToken cancellationToken); } A primeira interface recebe dois tipos genéricos que vão representar o request (TRequest) e o response (TResponse). A outra interface espera apenas o request. E é aqui que temos uma grande mudança em relação ao MediatR: Por design, ele espera que o request implemente uma interface chamada IRequest ou IRequest. Dessa forma temos duas limitações: a primeira é que eu não posso utilizar um tipo primitivo como request (int, string, etc.) e segundo que, caso o handler retorne um valor (response), essa informação precisa estar definida no request. A minha abordagem deixa livre para que possamos ut

Apr 11, 2025 - 01:26
 0
Reinventando a Roda: Criando seu próprio MediatR - Parte 1

Quando o homem inventou a roda
Logo Deus inventou o freio,
Um dia, um feio inventou a moda
E toda roda amou o feio.

Zeca Baleiro é um gênio. Se você discorda, você está errado.

A roda toda abraçou o MediatR. Virou moda. É uma das bibliotecas mais usadas no mundo do dotnet.
Porém Jimmy Bogard, autor da lib resolveu colocar um freio na nossa alegria e resolveu transformar essa biblioteca em um produto pago: https://www.jimmybogard.com/automapper-and-mediatr-going-commercial/.

Não critico. Open Source geralmente é um belo de um "Venha a nós e ao Vosso Reino, nada!": Centenas de milhares de empresas usando, milhões de desenvolvedores reclamando e ninguém contribuindo. Simples assim.

Nem eu, nem você e muito menos as nossas empresas pensam nisso. É fato!

Para manter uma biblioteca como essa, o autor ou a autora (e sim, geralmente é uma pessoa só ou um grupo bem pequeno de pessoas) despende horas e horas e horas de trabalho e muita dedicação, dividindo seu tempo entre a empresa, projeto e a familia.

"Nem relógio trabalha de graça!" já diria meu velho pai! Nem ele, nem eu e nem você. A conta chegou e o grande Jimmy Bogard tomou a decisão que só ele poderia tomar. Eu apoio! E, acima de tudo agradeço por todo excelente trabalho feito. Coloquei muito sistema em produção usando o MediatR. Sou grato por isso. E acredito que você também.

Pois bem, seguimos.

Antes de tudo preciso destacar aqui que esse é o primeiro de uma série de três posts. Preferi dividir o assunto em três partes pois fiquei traumatizado com o post Reinventando a Roda: Criando uma Rede Neural com csharp já que foram muitos e muitos dias de estudo que se transformaram em muitos dias de escrita de código e que acarretaram num post que leva aproximadamente 35 minutos para a leitura completa. Oremos.

Nessa primeira parte quero abordar a criação e invocação do Command Handler (IHandler e ISender) apenas. E quero trazer algumas melhorias que eu julguei interessantes. Pelo menos para mim são interessantes.

No segundo post nós vamos avançar incluindo um Filter Handler, equivalente ao Pipeline Behavior.

E por fim, na terceira parte, vamos adicionar a feature de envio de notificações, o famigerado Publish!

Eu sei... existem várias outras features como stream por exemplo, mas quero focar nessas três apenas!

Se tudo der certo e nada der errado, crio uma DLC do post incluindo um sistema automágico de validação de Request. É algo que eu já poderia entregar na primeira parte, mas acho que o post ficaria grande demais (e eu nem sei se essa é uma boa ideia. Veremos...).

Certo, eu acredito que nem todo mundo está familiarizado com a biblioteca MediatR e com o pattern Mediador. Esse post não vai abordar o conceito em si, para isso acesse https://refactoring.guru/design-patterns/mediator - um dos sites mais incríveis que eu já vi na vida. Aliás, a imagem de capa desse post foi usurpada e modificada digitalmente pelo ChatGPT. Espero que eles não me processem, mas o crédito é todo, todinho deles!

Eu também não vou me aprofundar na biblioteca, vou falar apenas superficialmente, porém caso queira saber mais sobre, recomendo esse post Playground: MediatR do grande William Santos.

É importante destacar que o código abaixo - apesar de achar ele bem escrito e funcional - não foi feito para ser usado em produção. Pelo menos nesse estágio de desenvolvimento. Aliás, em nenhum estágio, já que eu não tenho nenhum interesse de manter esse código. E esse é um dos fatores fundamentais para a escolha de um produto/framework/serviço: alguém vai precisar manter, atualizar, corrigir erro e acima de tudo, evoluir o código. Não é o meu caso.

Ok, vamos ao que viemos. Pegue aquela xícara de café marota e vem comigo.

Crie um projeto do tipo Class Library. Dê o nome que preferir, mas escolha um nome pomposo. Eu dei o nome de TheMediator.Core. Robusto.

Certo! Vamos começar com o Handler. Um handler recebe um command (no nosso caso chamaremos de request) e pode ou não devolver um Response. Logo, precisamos ter dois métodos, um que retorna valor e o outro retorna void.

Para representar esse Handler criaremos duas interfaces:

Arquivo IHandler.cs:

public interface IHandler<in TRequest, TResponse>  
{  
  Task<TResponse> HandleAsync(TRequest request, CancellationToken cancellationToken);  
}  

public interface IHandler<in TRequest>  
{  
  Task HandleAsync(TRequest request, CancellationToken cancellationToken);  
}

A primeira interface recebe dois tipos genéricos que vão representar o request (TRequest) e o response (TResponse). A outra interface espera apenas o request.

E é aqui que temos uma grande mudança em relação ao MediatR: Por design, ele espera que o request implemente uma interface chamada IRequest ou IRequest. Dessa forma temos duas limitações: a primeira é que eu não posso utilizar um tipo primitivo como request (int, string, etc.) e segundo que, caso o handler retorne um valor (response), essa informação precisa estar definida no request.

A minha abordagem deixa livre para que possamos utilizar qualquer tipo de dado como request. Isso diminui a verbosidade do código já que, no caso do MediatR precisaríamos de algo como:

public record ProductResponse(Guid Id, string Name, decimal Price);

public record SearchProductRequest(string Filter) : IRequest<IReadOnlyCollection<ProductResponse>?>;

public class SearchProductHandler(IProductsRepository repository)
    : IHandler<SearchProductRequest, IReadOnlyCollection<ProductResponse>?>
{
    public Task<IReadOnlyCollection<ProductResponse>?> HandleAsync(
        SearchProductRequest request,
        CancellationToken cancellationToken)
    {
        return repository.Search(request.Filter, cancellationToken);
    }
}

Gosto pero no mucho...

No meu caso temos:

public record ProductResponse(Guid Id, string Name, decimal Price);

public class SearchProductHandler(IProductsRepository repository)
    : IHandler<string?, IReadOnlyCollection<ProductResponse>>
{
    public Task<IReadOnlyCollection<ProductResponse>> HandleAsync(
        string? query,
        CancellationToken cancellationToken)
    {
        return repository.Search(query, cancellationToken);
    }
}

Isso me dá mais flexibilidade. E temos menos código.

Gosto.

Avancemos!

O próximo passo é começarmos a implementar o nosso dispatcher ou o famigerado ISender. Essa interface representa o processo de envio do comando. Dado um request encontra-se o handler correspondente:

Arquivo ISender:

public interface ISender
{
    Task<TResponse> SendAsync<TRequest, TResponse>(TRequest request,
        CancellationToken cancellationToken);

    Task SendAsync<TRequest>(TRequest request, CancellationToken cancellationToken);
}

Repararam que essa interface segue a mesma ideia do IHandler tendo dois métodos, um para quando o processo retornar um resultado e outro para quando o resultado é um void? E todos utilizam um CancellationToken. E se você não sabe para que serve um CancellationToken eu fiz um post explicando: Async/Await: Para que serve o CancellationToken?. Divirta-se!

No MediatR, caso você utilize um request em mais de um handler ele vai executar o primeiro handler registrado e ignorar o restante (acho que tem como configurar para gerar uma exception caso mais de um handler seja registrado com um request. Apenas acho. Se souber de algo, poste aí nos comentários).

Sinceramente no me gusta...

Na minha implementação é possível sim ter mais de um handler usando o mesmo request contanto que o response seja diferente.

Para isso, precisamos ter um controle de todos os handlers registrados pela aplicação.
Minha ideia aqui é ter uma representação da estrutura de tipos de um Handler. Basicamente podemos ter um Handler com um TRequest e TResponse ou apenas com um TResponse.

Sendo assim, criei um record para representar essa estrutura de dados:

Arquivo TypeInspector.cs

internal record ServiceTypes(Type MainType, Type RequestType, Type ResponseType);

A primeira pergunta que você deve estar fazendo é: ResponseTypenão poderia aceitar nulo?
Sim, poderia. Mas sempre que posso eu evito o uso do null. Existem inúmeros motivos para isso, porém não vou trazê-los agora. Sendo assim, para atender esse cenário, criei um tipo vazio - Void - para representar situações onde o Handler não tem um TResponse:

Arquivo TypeInspector.cs

internal class Void;

Uma outra pergunta que surge é, por que não deu o nome de HandlerType para a propriedade MainType?
Simples: Essa estrutura vai ser usada em outras frentes como por exemplo no IFilterHandler que veremos na parte 2 desse post.

Certo! Agora precisamos criar uma classe onde vamos obter informações sobre os tipos de serviço registrados pelo sistema.

Arquivo TypeInspector.cs

internal static class TypeInspector
{
    public static IEnumerable<Type> GetTypesByAssembly(Assembly assembly, params Type[] types)
    {
        var handlerTypes = assembly.GetTypes()
            .Where(t => t.IsClass && t is { IsAbstract: false, IsGenericType: false })
            .Where(t => t.GetInterfaces().Any(i =>
                i.IsGenericType &&
                types.Contains(i.GetGenericTypeDefinition())));

        return handlerTypes;
    }

    public static ServiceTypes GetServiceTypes(
        Type mainType,
        params Type[] types)
    {
        foreach (var type in types)
        {
            var interfaceType = mainType.GetInterfaceType(type);
            if (interfaceType is null) continue;

            var genericArguments = interfaceType.GetGenericArguments();
            if (genericArguments.Length == 1) // IHandler
                return new(mainType, genericArguments[0], typeof(Void));

            return new(mainType, genericArguments[0], genericArguments[1]); // IHandler
        }

        throw new InvalidOperationException(
            $"The handler {mainType} does not implement IHandler or IHandler.");
    }

    private static Type? GetInterfaceType(this Type interfaceType, Type mainType)
        => interfaceType
            .GetInterfaces()
            .FirstOrDefault(i =>
                i.IsGenericType &&
                i.GetGenericTypeDefinition() == mainType);
}

Vou explicar método a método o que a classe estática TypeInspector faz.

O método GetTypesByAssembly vai ser usado para obter todas as classes que implementam uma ou mais interfaces pré-definidas. Imagine que, ao carregar nossa aplicação, nós vamos varrer os assemblies à procura de classes que implementam IHandler<,> e IHandler<> (no próximo post vamos incluir o IFilterHandler<,> e IFilterHandler<>).

Talvez você não esteja acostumado com a notação <,> ou <>, mas isso é simples. A primeira define uma interface com dois tipos genéricos, e a segunda define uma interface com um tipo genérico apenas. Logo, consigo fazer esse filtro nos tipos do assembly obtendo uma lista de interfaces que o tipo implementa, e dentro dessa lista é verificado se os tipos são iguais aos que foram informados como parâmetro do método.

Esse método vai facilitar muito a nossa vida. Não vamos precisar registrar os Handlers um por um.

Já o método GetServiceTypes retorna uma estrutura com os tipos genéricos do nosso Handler. A gente informa o tipo do Handler e obtemos os tipos do TRequest e TResponse passados na interface IHandler, ou ainda, obtemos apenas o tipo TRequest passado na interface IHandler.

Em resumo, se eu tenho um Handler public class CreateProductHandler : IHandler, esse método vai retornar um ServiceTypes com os valores MainType= CreateProductHandler, RequestType = ProductRequest e ResponseType = ProductResponse;

Caso o Handler não tenha um response, o ResponseType vai ser Void. Sacaram o motivo de eu ter criaro a classe Void?

Por fim temos o método GetInterfaceType que é uma extensão incluída no tipo Type para nos ajudar a trazer uma a interface que o Handler implementou. Por exemplo, ao passarmo typeof(CreateProductHandler) o retorno seria IHandler e a partir disso podemos seguir a diante e obter os tipos do TRequest e do TResponse que no caso são ProductRequest e ProductResponse.

Por hora fechamos nossa classe TypeInspector.

Nesse momento precisamos ter uma classe para registrar no sistema de injeção de dependências todos os serviços que foram encontrados na nossa aplicação. Para tal, criaremos uma classe HandlerTypeManager:

Arquivo HandlerTypeManager.cs

public class HandlerTypeManager(IServiceCollection services)
{
    private readonly HashSet<ServiceTypes> _handlersServiceTypes = new();

    public void AddServicesFromAssemblies(params Assembly[] assemblies)
    {
        if (assemblies is null || assemblies.Length == 0)
            throw new ArgumentNullException(nameof(assemblies), "Assemblies cannot be null or empty.");

        var handlerTypes = assemblies
            .Select(assembly => TypeInspector.GetTypesByAssembly(assembly, typeof(IHandler<,>), typeof(IHandler<>)))
            .SelectMany(handlers => handlers);

        foreach (var handlerType in handlerTypes)
            AddHandler(handlerType);
    }

    internal Type GetHandler<TRequest, TResponse>()
        => GetHandlerTypeByRequestResponse<TRequest, TResponse>();

    internal Type GetHandler<TRequest>()
        => GetHandlerTypeByRequestResponse<TRequest, Void>();

    private void AddHandler(Type handlerType)
    {
        var handlerServiceTypes = TypeInspector.GetServiceTypes(handlerType, typeof(IHandler<,>), typeof(IHandler<>));

        var notExists =  _handlersServiceTypes.Add(handlerServiceTypes);
        if(!notExists)
            throw new InvalidOperationException(
                $"Cannot register {handlerType}!.\nThe handler {handlerServiceTypes.MainType} is already registered with the same request {handlerServiceTypes.RequestType} and response {handlerServiceTypes.ResponseType} types.");

        services.AddSingleton(handlerType);
    }

    private Type GetHandlerTypeByRequestResponse<TRequest, TResponse>()
    {
        var requestType = typeof(TRequest);
        var responseType = typeof(TResponse);
        var type = _handlersServiceTypes.FirstOrDefault(h => h.RequestType == requestType && h.ResponseType == responseType);
        if (type is null)
            throw new InvalidOperationException(
                $"No handler found for request type {typeof(TRequest)} and response type {typeof(TResponse)}.");

        return type.MainType;
    }
}

Vamos destrinchá-la:

Recebemos um IServiceCollection services no construtor primário. Vamos utilizar esse services para efetuar a injeção de dependência.
Em seguida temos um HashSet _handlersServiceTypes. É aqui que vamos armazenar todos os ServiceTypes encontrados nos assemblies informados pela aplicação. Adiante eu explico o motivo pelo qual eu utilizei um HashSet, mas se você conhece o funcionamento dele, já deve ter sacado...

O método AddServicesFromAssemblies recebe uma lista de assemblies onde vamos obter todos os serviços que implementamIHandler<,> e IHandler<>. Os serviços encontrados serão incluídos na estrutura de dados do tipo HashSet _handlersServiceTypes pelo método AddHandler. Esse método vai ser muito importante. Ele é a porta de entrada para o nosso Mediator...

O método AddHandler como o próprio nome sugere, utiliza o TypeInspector para obter o ServiceTypes. Vimos como isso é feito logo acima. Em seguida tentamos incluir no nosso HashSet. Caso já exista um ServiceType com o mesmo MainType, RequestType e ResponseType, eu lanço uma exceção. Como informei anteriormente, é possível que um mesmo request seja utilizado em vários handlers contanto que seu response seja diferente. E por fim, incluo o tipo no sistema de injeção de dependência utilizando um services.AddSingleton(handlerType). (Se fosse necessário criar esse sistema de DI na mão a gente tava fudido ferrado)

Os métodos GetHandler() e GetHandler() invocam o método GetHandlerTypeByRequestResponse(). Nesse momento acredito que você tenha percebido a utilidade do Void. Eu recomendo muito: páre de usar null...

O método GetHandlerTypeByRequestResponse() é bem simples, ele tenta encontrar o MainType no HashSet _handlersServiceTypes usando o FirstOrDefault do Linq. Caso não encontre, lança uma exceção.

Os métodos de Get serão utilizados na implementação do ISender. Guenta ae que você vai ver a utilidade deles logo mais...

Beleza. Temos todo mecanismo necessário para registrar e obter os Handlers. A seguir precisamos criar uma implementação para a interface ISender. Essa implementação é bem simples mas muito eficaz e acima de tudo performática.

Arquivo ISender.cs

internal class Sender(IServiceProvider serviceProvider, HandlerTypeManager handlerTypeManager) : ISender
{
    public Task<TResponse> SendAsync<TRequest, TResponse>(TRequest request,
        CancellationToken cancellationToken)
    {
        var handlerType = handlerTypeManager.GetHandler<TRequest, TResponse>();
        var service = (IHandler<TRequest, TResponse>)serviceProvider.GetRequiredService(handlerType);
        return service.HandleAsync(request, cancellationToken);
    }

    public Task SendAsync<TRequest>(TRequest request, CancellationToken cancellationToken)
    {
        var handlerType = handlerTypeManager.GetHandler<TRequest>();
        var service = (IHandler<TRequest>)serviceProvider.GetRequiredService(handlerType);
        return service.HandleAsync(request, cancellationToken);
    }
}

No construtor primário recebemos um IServiceProvider serviceProvider e um HandlerTypeManager handlerTypeManager.

Os métodos Task SendAsync(TRequest request, CancellationToken cancellationToken) e Task SendAsync(TRequest request, CancellationToken cancellationToken) fazem praticamente a mesma coisa, sendo que um retorna um TResponse e o outro não.
Eles obtem o handlerType utilizando o handlerTypeManager.GetHandler (viu para que serve? ;p) e usam o IServiceProvider para obter do sistema de injeção de dependência uma instância correspondente ao tipo do handler informado.
Eu falei algumas vezes desse sistema de injeção de dependência acreditando que você aí saiba para que ele serve e como ele funciona. Se não sabe, recomento muito a leitura desse post: https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection.

Pronto! A classe Sender é "apenas" isso. sem mágicas ou malabarismos. Utilizamos apenas csharp na sua mais pura essência. Temos nosso Mediator praticamente finalizado. Falta apenas uma coisa...

Para facilitar o uso do nosso Mediator vamos criar um método de extensão que adiciona uma funcionalidade no IServiceCollection. É bem simples e fácil de entender. E o melhor, ajuda muito quem vai consumir nossa lib.

Arquivo DependencyInjection.cs

public static class DependencyInjection
{
    public static IServiceCollection AddTheMediator(
        this IServiceCollection services, Action<HandlerTypeManager> configurator)
    {
        var handlerTypeManager = new HandlerTypeManager(services);
        configurator(handlerTypeManager);

        services.AddSingleton<HandlerTypeManager>(_ => handlerTypeManager);
        services.AddSingleton<ISender, Sender>();

        return services;
    }
}

Esse método é bem simples, ele recebe o IServiceCollection services e uma Action que eu chamei de configurator.

Eu crio uma instância do HandlerTypeManager passando o services e repasso essa instância para o configurador. Esse configurador será utilizado pela aplicação que vai usar o Mediator. Abaixo eu mostro isso na prática.

Em seguida eu registro o HandlerTypeManager como singleton no sistema de injeção de dependência, retornando o handlerTypeManager que foi configurado pela aplicação.
E por último eu adiciono o ISender no sistema de injeção de dependência onde ele vai receber uma instância da classe Sender. Também como singleton.

Aqui temos um caso interessante: Na classe Sender eu espero um IServiceProvider serviceProvider. Para muitos pode ser uma novidade, mas o próprio sistema de injeção de dependência injeta uma instância do ServiceProvider para nós. Isso facilita muito nosso desenvolvimento.

Pronto!!! Temos nosso próprio Mediator. Que maravilha. E nem doeu. Não tem todas as funcionalidades ainda, mas já podemos usar em produção no nosso playground.

Bora testar?

Crie um projeto Web Minimal API. Melhor escolha para desenvolver APIs em dotnet.

Referencie a nossa super lib. Vamos adicionar e configurar nosso Mediator.

Arquivo Program.cs

using TheMediator.Core;

var builder = WebApplication.CreateBuilder(args);  

builder.Services.AddTheMediator(configurator =>  
{  
    configurator.AddServicesFromAssemblies(typeof(Program).Assembly);  
});

Viu que simples. Aquela Action lá de cima nos possibilita configurar nossa biblioteca de uma forma mais simples. Num futuro podemos expor mais configurações como por exemplo o ciclo de vida dos Handlers, ou a forma que vamos despachar as Notifications ou ainda qual implementação podemos usar de validação de commands. Dá para fazer muita coisa.
Aliás, essa abordagem não é nada original. É um padrão, se você for ver...

// Esse código não precisa ser adicionado nos testes... serve apenas para exemplo...
builder.Services.AddOpenApi("Minha API", configurator =>  
{  
    configurator.OpenApiVersion = OpenApiSpecVersion.OpenApi2_0;  
});

Pronto, configuramos nossa biblioteca. Vamos criar nosso primeiro Handler?

Arquivo HelloHandler.cs

public class HelloHandler : IHandler<string, string>
{
    public Task<string> HandleAsync(string request, CancellationToken cancellationToken)
    {
        return Task.FromResult($"Hello {request}");
    }
}

Nesse momento eu recomendo que você coloque um break point na linha configurator.AddServicesFromAssemblies(typeof(Program).Assembly); rode a aplicação e vá executando passo-a-passo. Isso vai te ajudar a compreender todo o fluxo, desde o carregamento dos tipos, até a inclusão deles na nossa lista de Handlers. É bem divertido e mostra como o sistema de gestão de tipos do csharp é muito flexível.

Beleza. Tudo certo, nada resolvido! Vamos criar um endpoint maroto para testar de verdade.

app.MapGet("/hello",
        async (
            [FromServices] ISender sender,
            CancellationToken cancellationToken) =>
        {
            var request = "Mediator!!!!!";
            var response = await sender.SendAsync<string, string>(request, cancellationToken);
            return new { message = response };
        })
    .WithName("hello");

Boa! Agora é rodar a aplicação, invocar o endpoint http://localhost:/hello e correr para o abraço. Mas antes eu recomendo colocar um break point na linha var request = "Mediator!!!!!"; e um outro break point na linha return Task.FromResult($"Hello {request}"); do handler HelloHandler.

Se tudo der certo e nada der errado, teremos a seguinte saída:

{
  "message": "Hello Mediator!!!!!"
}

Viu só? Recomendo que crie vários handlers e brinque debugando o código. Crie handlers com e sem TResponse, crie com tipos primitivos, tipos complexos, tente registrar mais de um handler com o mesmo TRequest, enfim, teste bastante. Brinque bastante! No repositório eu deixei um playground pronto com vários handlers. Recomendo dar uma olhadinha com calma!!

Enfim chegamos ao final da primeira parte do post Reinventando a Roda: Criando seu próprio MediatR. O que foi feito aqui já é algo muito útil. Ainda temos melhorias para fazer, então aguarde a parte 2 e 3 do post.

E caso queira baixar o projeto, acesse https://github.com/angelobelchior/TheMediator!

Críticas, dúvidas e sugestões são aceitas. Se serão respondidas, aí é uma outra história...

Forte abraço, até a próxima!