Reinventando a Roda: Criando seu próprio MediatR - Parte 2
Nada do que foi será... De novo do jeito que já foi um dia... Confesso que não conheço muito a carreira do Lulu Santos, mas esse trecho define bem o que apresentaremos nesse post! Eu recomendo muito que você leia o post anterior Reinventando a Roda: Criando seu próprio MediatR - Parte 1. Nele eu explico a ideia por trás dessa implementação. Eu não vou explicar coisas que foram explicadas lá justamente para não deixar o post gigante!!!! Assumo aqui que você, de fato, leu o post anterior. Sendo assim, saiba que tudo aquilo que eu fiz foi refeito. Praticamente todo código que eu escrevi, foi reescrito. E o motivo talvez não seja forte o suficiente, mas é o que tem para hoje: Eu comecei com a ideia de criar um Mediator da forma mais simples possível - o que foi entregue no primeiro post - porém, ao avançar para a criação dos filtros de handlers percebi que precisava deixar o código mais organizado, limpo e acima de tudo testável. Pois bem, é o que será entregue nesse segundo post. Mas não se preocupe, eu vou repassar por todas as classes novamente, explicando as definições e decisões, e no fim tenho certeza de que você meu caro leitor, minha cara leitora, vai concordar que ficou melhor. (Inclusive me atiça a ideia de empacotar essa lib e distribuir... não sei... se você gosta da ideia, poste aí nos comentários...) Café tá caro... pega um Kisuco de Morango e vem comigo... Primeiramente, temos a seguinte estrutura do projeto: TheMediator.Core ├── DependencyInjection/ │ └── DependencyInjection.cs ├── Executors/ │ ├── FilterExecutor.cs │ └── HandlerExecutor.cs ├── Inspectors/ │ └── ServiceInspector.cs ├── Models/ │ ├── ServiceCategory.cs │ ├── ServiceDescriptor.cs │ └── Void.cs ├── Registries/ │ ├── FilterRegistry.cs │ └── HandlerRegistry.cs ├── IRequestFilter.cs ├── IRequestHandler.cs ├── ISender.cs └── Usings.cs Note que agora temos clareza de todos os componentes do nosso Mediator. DependencyInjection/DependencyInjection.cs: Configura a injeção de dependências do nosso querido TheMediator.Core. Executors/FilterExecutor.cs: Executa os filtros registrados para processar requisições antes ou depois de serem manipuladas. Executors/HandlerExecutor.cs: Executa os handlers registrados para processar as requisições (com ou sem retorno TRequest). O HandlerExecutor utiliza o FilterExecutor no processo de execução da request. Inspectors/ServiceInspector.cs: Inspeciona os tipos obtidos pelos assemblies passados como parâmetro e cria os seus respectivos serviços. Models/ServiceCategory.cs: Representa uma categoria de serviço, usada para organizar ou classificar serviços (Handler, Filter e no futuro Notification). Models/ServiceDescriptor.cs: Descreve um serviço. MainType, RequestType, ResponseType e ServiceCategory. Registries/FilterRegistry.cs: Registra e gerencia os filtros disponíveis no sistema. Registries/HandlerRegistry.cs: Registra e gerencia os handlers de requisições disponíveis no sistema. IRequestFilter.cs: Define a interface para filtros que podem ser aplicados às requisições. IRequestHandler.cs: Define a interface para os handlers de requisições. ISender.cs: Define a interface para enviar requisições e obter respostas. Dentro desse arquivo temos a implementação dessa mesma interface. A ideia é que o Sender consiga invocar os handlers utilizando o HandlerExecutor e enviar notificações utilizando um NotificationExecutor. Como pode notar nós não temos nada de notificação nessa versão. No próximo post nós vamos efetuar essa implementação. Usings.cs: Centraliza os using comuns para facilitar a manutenção e reduzir redundâncias no código. Seguindo essa estrutura, para adicionarmos um sistema de notificação, precisamos basicamente criar um Registry para registar todas as classes que enviam notificação, e um Executor para propagar todas as mensagens. Próximo post vai ficar show de bola, garanto! De qualquer forma, vamos passar classe a classe explicando tudinho. Os arquivos IRequestFilter.cs, IRequestHandler.cs e ISender.cs são praticamente iguais. Se você leu o post anterior já sabe o que eles fazem, inclusive o IRequestFilter que segue a mesma ideia do IRequestHandler. A principal diferença é que o IRequestFilter tem um argumento chamado Func next no método FilterAsync. Mais a frente eu explico o que ele faz. O Usings.cs já foi descrito acima... Arquivo DependencyInjection/DependencyInjection.cs public static class DependencyInjection { public static IServiceCollection AddTheMediator( this IServiceCollection services, Action configurator) { var configuration = new Configuration(services); configurator(configuration); services.AddSingleton(_ => configuration.Handlers); services.AddSingleton(_ => configuration.Filters); //services.AddSingleton(_ => configuration.Notifications); services.AddSingleton(); //services.AddSingleton(); services.AddSingleton(); services.AddSi

Nada do que foi será...
De novo do jeito que já foi um dia...
Confesso que não conheço muito a carreira do Lulu Santos, mas esse trecho define bem o que apresentaremos nesse post!
Eu recomendo muito que você leia o post anterior Reinventando a Roda: Criando seu próprio MediatR - Parte 1. Nele eu explico a ideia por trás dessa implementação. Eu não vou explicar coisas que foram explicadas lá justamente para não deixar o post gigante!!!!
Assumo aqui que você, de fato, leu o post anterior. Sendo assim, saiba que tudo aquilo que eu fiz foi refeito. Praticamente todo código que eu escrevi, foi reescrito. E o motivo talvez não seja forte o suficiente, mas é o que tem para hoje: Eu comecei com a ideia de criar um Mediator da forma mais simples possível - o que foi entregue no primeiro post - porém, ao avançar para a criação dos filtros de handlers percebi que precisava deixar o código mais organizado, limpo e acima de tudo testável. Pois bem, é o que será entregue nesse segundo post.
Mas não se preocupe, eu vou repassar por todas as classes novamente, explicando as definições e decisões, e no fim tenho certeza de que você meu caro leitor, minha cara leitora, vai concordar que ficou melhor. (Inclusive me atiça a ideia de empacotar essa lib e distribuir... não sei... se você gosta da ideia, poste aí nos comentários...)
Café tá caro... pega um Kisuco de Morango e vem comigo...
Primeiramente, temos a seguinte estrutura do projeto:
TheMediator.Core
├── DependencyInjection/
│ └── DependencyInjection.cs
├── Executors/
│ ├── FilterExecutor.cs
│ └── HandlerExecutor.cs
├── Inspectors/
│ └── ServiceInspector.cs
├── Models/
│ ├── ServiceCategory.cs
│ ├── ServiceDescriptor.cs
│ └── Void.cs
├── Registries/
│ ├── FilterRegistry.cs
│ └── HandlerRegistry.cs
├── IRequestFilter.cs
├── IRequestHandler.cs
├── ISender.cs
└── Usings.cs
Note que agora temos clareza de todos os componentes do nosso Mediator.
DependencyInjection/DependencyInjection.cs: Configura a injeção de dependências do nosso querido TheMediator.Core.
Executors/FilterExecutor.cs: Executa os filtros registrados para processar requisições antes ou depois de serem manipuladas.
Executors/HandlerExecutor.cs: Executa os handlers registrados para processar as requisições (com ou sem retorno
TRequest
). OHandlerExecutor
utiliza oFilterExecutor
no processo de execução da request.Inspectors/ServiceInspector.cs: Inspeciona os tipos obtidos pelos assemblies passados como parâmetro e cria os seus respectivos serviços.
Models/ServiceCategory.cs: Representa uma categoria de serviço, usada para organizar ou classificar serviços (Handler, Filter e no futuro Notification).
Models/ServiceDescriptor.cs: Descreve um serviço. MainType, RequestType, ResponseType e ServiceCategory.
Registries/FilterRegistry.cs: Registra e gerencia os filtros disponíveis no sistema.
Registries/HandlerRegistry.cs: Registra e gerencia os handlers de requisições disponíveis no sistema.
IRequestFilter.cs: Define a interface para filtros que podem ser aplicados às requisições.
IRequestHandler.cs: Define a interface para os handlers de requisições.
ISender.cs: Define a interface para enviar requisições e obter respostas. Dentro desse arquivo temos a implementação dessa mesma interface. A ideia é que o
Sender
consiga invocar os handlers utilizando oHandlerExecutor
e enviar notificações utilizando umNotificationExecutor
. Como pode notar nós não temos nada de notificação nessa versão. No próximo post nós vamos efetuar essa implementação.Usings.cs: Centraliza os
using
comuns para facilitar a manutenção e reduzir redundâncias no código.
Seguindo essa estrutura, para adicionarmos um sistema de notificação, precisamos basicamente criar um Registry para registar todas as classes que enviam notificação, e um Executor para propagar todas as mensagens. Próximo post vai ficar show de bola, garanto!
De qualquer forma, vamos passar classe a classe explicando tudinho.
Os arquivos IRequestFilter.cs, IRequestHandler.cs e ISender.cs são praticamente iguais. Se você leu o post anterior já sabe o que eles fazem, inclusive o IRequestFilter que segue a mesma ideia do IRequestHandler.
A principal diferença é que o IRequestFilter
tem um argumento chamado Func
no método FilterAsync
. Mais a frente eu explico o que ele faz.
O Usings.cs já foi descrito acima...
Arquivo DependencyInjection/DependencyInjection.cs
public static class DependencyInjection
{
public static IServiceCollection AddTheMediator(
this IServiceCollection services,
Action<Configuration> configurator)
{
var configuration = new Configuration(services);
configurator(configuration);
services.AddSingleton<HandlerRegistry>(_ => configuration.Handlers);
services.AddSingleton<FilterRegistry>(_ => configuration.Filters);
//services.AddSingleton(_ => configuration.Notifications);
services.AddSingleton<FilterExecutor>();
//services.AddSingleton();
services.AddSingleton<HandlerExecutor>();
services.AddSingleton<ISender, Sender>();
return services;
}
public class Configuration(IServiceCollection services)
{
internal HandlerRegistry Handlers { get; } = new(services);
internal FilterRegistry Filters { get; } = new(services);
//internal NotificationRegistry Notifications { get; } = new(services);
public Configuration AddServicesFromAssemblies(params Assembly[] assemblies)
{
if (assemblies is null || assemblies.Length == 0)
throw new ArgumentNullException(nameof(assemblies), "Assemblies cannot be null or empty.");
assemblies
.Select(ServiceInspector.GetServicesByAssembly)
.SelectMany(serviceType => serviceType)
.ToList()
.ForEach(serviceDescriptor =>
{
if (serviceDescriptor.Category == ServiceCategory.Handler)
Handlers.Add(serviceDescriptor);
//TODO: Incluir notificações...
// if (serviceDescriptor.Category == ServiceCategory.Notification)
// Notifications.Add(serviceDescriptor);
});
return this;
}
public Configuration AddFilter<TFilter>()
where TFilter : class, IRequestFilter
{
Filters.Add<TFilter>();
return this;
}
}
}
A classe DependencyInjection adiciona o método de extensão AddTheMediator
ao ServiceCollection. Isso é bem comum de se ver em bibliotecas. Dentro dessa classe temos uma outra classe chamada Configuration
que como o nome sugere, ajuda na configuração do nosso Mediator. Gosto dessa abordagem de uma classe dentro da outra.
Logo, o uso da nossa biblioteca se inicia à partir do método AddTheMediator
. Ao invocá-lo devemos passar uma Action onde podemos acionar métodos da classe Configuration. É nesse momento que nós informamos ao nosso Mediator quais são os handlers e filters existentes no sistema. Esse processo mudou pouco em relação ao post anterior.
Note que podemos adicionar handlers e no futuro notifications de duas formas:
- Varrendo os assemblies, identificando e registrando os tipos respectivos;
- Ou informando manualmente através dos métodos
Handlers.Add
e futuramenteNotifications.Add
;
Na teoria, é mais simples usar o
AddServicesFromAssemblies
, mas obviamente informar tipo por tipo acaba sendo mais performático. Não que carregar a partir de assemblies seja tããão ruim, mas o processo abre o objeto a procura de tipos de dados específicos e osso tem um custo. Eu particularmente não vejo problema nisso...
O método AddServicesFromAssemblies
percorre a lista de assemblies e invoca o ServiceInspector.GetServicesByAssembly
criando uma lista de Services com os tipos pré-estabelecidos. Abaixo veremos como isso é feito.
Com o Service criado, enviamos ele ou ao HandlerRegistry ou futuramente ao NotificationRegistry onde serão devidamente registrados no sistema de injeção de dependência.
Além dos Handlers também podemos informar os Filters. Só que manualmente, um por um!
E talvez aqui surja uma pergunta: Eu não poderia carregar os filtros da mesma forma que eu faço com os Handlers?
A resposta é não!
Os filtros precisam manter uma ordem sequencial de execução. Logo, eu preciso registrá-los um a um na ordem que eu quero que eles sejam executados.
Handlers e Notifications podem ser registrados em qualquer ordem, já que eles são invocados diretamente.
Arquivo Inspectors/ServiceInspector.cs
internal class Void;
internal static class ServiceInspector
{
private static readonly List<Type> HandlerTypes =
[
typeof(IRequestHandler<,>),
typeof(IRequestHandler<>)
];
private static readonly List<Type> FilterTypes =
[
typeof(IRequestFilter)
];
public static IEnumerable<Models.ServiceDescriptor> GetServicesByAssembly(
Assembly assembly)
{
var types = assembly.GetTypes()
.Where(t => t.IsClass && t is { IsAbstract: false, IsGenericType: false });
foreach (var type in types)
{
var interfaces = type.GetInterfaces().Where(i => i.IsGenericType);
foreach (var @interface in interfaces)
{
var genericType = @interface.GetGenericTypeDefinition();
if (HandlerTypes.Contains(genericType))
yield return CreateService(type, ServiceCategory.Handler);
if (FilterTypes.Contains(genericType))
yield return CreateService(type, ServiceCategory.Filter);
//TODO: Incluir Notifications
// if (NotificationTypes.Contains(genericType))
// yield return CreateService(type, ServiceCategory.Notification);
}
}
}
public static Models.ServiceDescriptor CreateService(
Type mainType,
ServiceCategory serviceCategory)
{
var types = serviceCategory == ServiceCategory.Filter ? FilterTypes : HandlerTypes;
foreach (var type in types)
{
var interfaceType = GetMatchingInterface(mainType, type);
if (interfaceType is null)
continue;
return CreateServiceFromInterface(mainType, interfaceType, serviceCategory);
}
throw new InvalidOperationException(
$"The type {mainType} does not implement a valid handler or filter handler interface.");
}
internal static Models.ServiceDescriptor CreateServiceFromInterface(
Type mainType,
Type interfaceType,
ServiceCategory serviceCategory)
{
var @void = typeof(Void);
var genericArguments = interfaceType.GetGenericArguments();
return genericArguments.Length switch
{
0 => new Models.ServiceDescriptor(mainType, @void, @void, serviceCategory),
1 => new Models.ServiceDescriptor(mainType, genericArguments[0], @void, serviceCategory),
_ => new Models.ServiceDescriptor(mainType, genericArguments[0], genericArguments[1], serviceCategory)
};
}
}
internal static Type? GetMatchingInterface(Type mainType, Type type)
{
var allInterfaces = mainType.GetInterfaces();
return allInterfaces.FirstOrDefault(i =>
i.IsGenericType && i.GetGenericTypeDefinition() == type) ??
allInterfaces.FirstOrDefault(i => i == type);
}
Esse arquivo segue a mesma ideia do famigerado TypeInspector.cs apresentado no post anterior. Aliás, eu apenas renomeei.
O método GetServicesByAssembly
tem como finalidade abrir um Assembly e procurar por uma lista específica de tipos, nesse momento IRequestHandler
e IRequestFilter
.
Ao obter um tipo que implemente alguma dessas interfaces, ele cria um ServiceDescriptor (antigo ServiceTypes) informando qual é a categoria desse serviço:
internal enum ServiceCategory
{
Handler,
Filter,
//Notification
}
Dessa maneira, numa passada só pelos assemblies, eu tenho uma lista de handlers e futuramente uma lista de Notifications.
CreateService
é um método utilizado no momento que se registra um Service manualmente, já que eu preciso informar o mainType e o serviceCategory. A partir do mainType sabemos qual interface de serviço ele implementa (IRequestHandler<,>, IRequestHandler<>, IRequestFilter e no futuro INotificationHandler) e com isso temos nosso ServiceDescriptor preenchido bonitinho com tudo que precisamos!
Inclusive temos o tipo Void! Olha ele aí novamente!!! Se você leu o post anterior sabe para que ele serve :)
Já o método GetMatchingInterface
verifica se o tipo principal (mainType
) implementa uma interface que corresponde ao tipo genérico ou não genérico especificado (type
). Ele retorna a primeira interface correspondente ou null caso nenhuma seja encontrada.
Por fim, temos o método CreateServiceFromInterface
que serve para criar de fato o objeto ServiceDescriptor
obtendo da interface de serviço quais tipos genéricos ela espera. A partir deles sabemos qual é o MainType
, RequestType
, ResponseType
e, claro, temos já em mãos o ServiceCategory
.
Lembrando que um Filter não tem nem RequestType
e nem ResponseType
, sendo assim, esses tipos vão receber o glorioso Void
!
Já os Handlers podem ter RequestType
e/ou ResponseType
e futuramente as Notifications terão apenas um MessageType.
O
ServiceDescriptor
tende a mudar no próximo post, mas não vai ser nada tão grande, é mais para poder abrigar um TMessage também.
Fechamos ServiceInspector
!
Registries/FilterRegistry.cs e Registries/HandlerRegistry.cs servem como repositórios de ServiceDecriptors.
Além de armazenar essas informações, esses Registries adicionam o tipo principal (MainType) ao serviço de injeção de dependência.
O HandlerRegistry
armazena os ServiceDecriptors num HashSet e no post anterior eu expliquei o motivo.
Já no FilterRegistry
eu uso uma Stack! Interessante, não? Quando eu for falar sobre FilterExecutor eu explico o motivo. Mas vai pensando ai...
Arquivo Executors/HandlerExecutor.cs
internal class HandlerExecutor(
IServiceProvider serviceProvider,
HandlerRegistry handlerRegistry,
FilterExecutor filterExecutor)
{
public Task<TResponse> SendAsync<TRequest, TResponse>(TRequest request,
CancellationToken cancellationToken)
where TRequest : notnull
{
cancellationToken.ThrowIfCancellationRequested();
var handlerType = handlerRegistry.GetHandler<TRequest, TResponse>(ServiceCategory.Handler);
var handlerService = (IRequestHandler<TRequest, TResponse>)serviceProvider.GetRequiredService(handlerType.MainType);
return filterExecutor.Execute(
request,
() => handlerService.HandleAsync(request, cancellationToken),
cancellationToken);
}
public Task SendAsync<TRequest>(TRequest request, CancellationToken cancellationToken)
where TRequest : notnull
{
cancellationToken.ThrowIfCancellationRequested();
var handlerType = handlerRegistry.GetHandler<TRequest, Void>(ServiceCategory.Handler);
var handlerService = (IRequestHandler<TRequest>)serviceProvider.GetRequiredService(handlerType.MainType);
return filterExecutor.Execute(
request,
() => handlerService.HandleAsync(request, cancellationToken),
cancellationToken);
}
}
Essa classe espera por injeção de dependência um IServiceProvider
, HandlerRegistry
e umFilterExecutor
que são injetados lá no método DependencyInjection.AddMediator
.
Os métodos SendAsync
e SendAsync
fazem a mesma coisa. A diferença é que um espera um retorno e o outro não. E o que eles fazem é bem simples!!!
Começamos o método verificando se um cancelamento de token foi solicitado: cancellationToken.ThrowIfCancellationRequested();
.
Se você não sabe para que serve um CancellationToken
fiz um post exclusivamente para você: # Async/Await: Para que serve o CancellationToken?.
Em seguida eu utilizo o handlerRegistry
para obter o handlerType correspondente ao TRequest
e/ou TResponse
.
Com o handlerType consigo pedir para o sistema de injeção de dependências que obtenha o Handler injetado. Simples assim!
Aliás, se a gente parar para analisar com calma, a mágica toda só acontece porque temos um sistema de injeção de dependências sensacional no dotnet. Se a gente precisasse criar na mão, já sabem, né?
O próximo passo é invocar o filterExecutor.Execute passando a função handlerService.HandleAsync
como parâmetro (Func
ou Func
dependendo se tem ou não retorno).
Essa função será executada após todos os filtros registrados serem invocados.
Viu que simples?
Arquivo Executors/FilterExecutor.cs
internal class FilterExecutor(
IServiceProvider serviceProvider,
FilterRegistry filterRegistry)
{
public Task Execute<TRequest>(
TRequest request,
Func<Task> function,
CancellationToken cancellationToken)
where TRequest : notnull
{
var task = filterRegistry.ListFilters()
.Select(f => (IRequestFilter)serviceProvider.GetRequiredService(f.MainType))
.Aggregate(
function,
(acc, source) => () =>
{
cancellationToken.ThrowIfCancellationRequested();
return source.FilterAsync(
request,
async () =>
{
await acc();
return Models.Void.Null;
}, cancellationToken);
});
return task();
}
public Task<TResponse> Execute<TRequest, TResponse>(
TRequest request,
Func<Task<TResponse>> function,
CancellationToken cancellationToken)
where TRequest : notnull
{
var task = filterRegistry.ListFilters()
.Select(f => (IRequestFilter)serviceProvider.GetRequiredService(f.MainType))
.Aggregate(
function,
(acc, source) => () =>
{
cancellationToken.ThrowIfCancellationRequested();
return source.FilterAsync(request, async () => response = await acc(), cancellationToken);
});
return task();
}
}
Essa classe tem uma estrutura parecida com a HandlerExecutor
. Logo, os métodos Execute
e Execute
fazem a mesma coisa, a diferença está no fato de ter ou não retorno da execução. Acho que nessa altura do campeonato você já deve ter entendido isso :)
Quando o handler não tem o retorno de uma execução, o valor padrão devolvido é Void.Null
. Como os filtros atendem qualquer tipo de handler (com ou sem retorno), essa foi a forma encontrada para ter um tipo de retorno específico. Para você que utliza o filtro, isso é completamente transparente já que nesse caso o retorno passa a ser apenas uma Task
.
A gente começa obtendo todos os filtros registrados no nosso sistema. Em seguida nós usamos o serviceProvider
para obter o objeto construído pelo sistema de injeção de dependência.
Depois disso, Aggregate
! Eu particularmente acho que essa implementação ficou realmente elegante. Ela conseguiu ficar mais elegante do que o Zidane jogando bola!
O método Aggregate
é usado para transformar uma lista de filtros em uma cadeia de execução.
Lembra que eu disse que o FilterRegistry
armazenava os services numa Stack
?
Eu usei uma Stack
para garantir que os Filters sejam invocados na ordem inversa à qual foram adicionados. Isso é importante porque, ao usar uma pilha (estrutura LIFO - Last In, First Out), o último filtro registrado será o primeiro a ser executado pelo FilterExecutor
.
Isso influencia diretamente na ordem de execução dos filtros. Eu não gostaria de usar um Reverse
toda vez que um Handler fosse executado. Isso seria custoso.
Pense que o último filtro incluído vai invocar o penúltimo que por sua vez vai invocar o antepenúltimo e por aí vai.
Sendo assim, ao registrar filtros dessa forma:
configuration.AddFilter<Filter1>();
configuration.AddFilter<Filter2>();
configuration.AddFilter<Filter3>();
Eles serão representados da seguinte forma na Stack
:
Filter3,
Filter2,
Filter1
E a invocação segue o seguinte fluxo:
Filter3
└── Filter2
└── Filter1
Em resumo, o Filter3
só vai ser executado após o Filter2
ser executado que só será executado após o Filter1
ser executado. Cascata... melhor que Agile...
Seguimos...
Lembram do Func
da interface IRequestFilter
que eu citei no começo do post?
Então... Ele é essencial no processo de execução dos filtros pois representa a próxima etapa na cadeia de execução.
Cada filtro pode decidir se vai ou não invocá-lo - await next()
- determinando se o fluxo é interrompido ou vai continuar executando...
Imagine que podemos ter um Filter que valide se o request contém valores válidos. Se for válido, podemos simplesmente dizer ao fluxo: vá em frente - await next()
- ou podemos interromper lançando uma exceção ou algo do tipo.
Eu acho que essa é a parte mais chatinha do código. Mas não é algo tããão complexo... Se analisar com atenção o código vai perceber isso...
Com isso terminamos a nossa implementação. Já temos nossa biblioteca Mediator linda e maravilhosa. Funcionando Like a charm! Handlers e Filters. Que maravilha. Falta pouco para termos a v1 com todas as funcionalidades propostas implementadas.
É claro que eu vou mostrar como ficou a utilização dessa nossa belezura.
A gente vai criar dois filters, um para medir o tempo de execução e o outro para logar a execução. E também teremos um handler básico. Saca só...
No projeto de Playgorund, vamos alterar nossa Program
:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddTheMediator(configuration =>
{
configuration.AddServicesFromAssemblies(typeof(Program).Assembly);
configuration.AddFilter<MeasureTimeRequestFilter>();
configuration.AddFilter<LoggerRequestFilter>();
});
builder.Services.AddOpenApi();
var app = builder. Build();
app.MapOpenApi();
app.MapScalarApiReference();
app.MapGet("/products/{id:guid}",
async (
[FromServices] ISender sender,
[FromRoute] Guid id,
CancellationToken cancellationToken) =>
{
var response = await sender.SendAsync<Guid, ProductResponse?>(id, cancellationToken);
return Results.Ok(response);
})
.WithName("Products.Get.ProductByIdQuery");
app.Run();
public class GetByIdProductRequestHandler
: IRequestHandler<Guid, ProductResponse?>
{
public Task<ProductResponse?> HandleAsync(Guid request, CancellationToken cancellationToken)
{
return request == Guid.Empty
? Task.FromResult<ProductResponse?>(null)
: Task.FromResult<ProductResponse?>(new ProductResponse(request, "Playstation 5", 4500));
}
}
public class MeasureTimeRequestFilter(ILogger<MeasureTimeRequestFilter> logger)
: IRequestFilter
{
public Task FilterAsync<TRequest>(TRequest request, Func<Task> next, CancellationToken cancellationToken)
where TRequest : notnull
{
logger.LogInformation("Started measuring time for Request type {Type} at {Time}", request.GetType().Name, DateTime.Now);
var stopwatch = new Stopwatch();
stopwatch.Start();
var response = next();
stopwatch.Stop();
logger.LogInformation("Request type {Type} execution time: {Time} ms", request.GetType().Name, stopwatch.ElapsedMilliseconds);
return response;
}
}
public class LoggerRequestFilter(ILogger<LoggerRequestFilter> logger)
: IRequestFilter
{
public Task FilterAsync<TRequest>(TRequest request, Func<Task> next,
CancellationToken cancellationToken)
where TRequest : notnull
{
logger.LogInformation("Request type {Type} execution started: {Time}", request.GetType().Name, DateTime.Now);
var response = next();
logger.LogInformation("Request type {Type} execution finished: {Time}", request.GetType().Name, DateTime.Now);
return response;
}
}
Se tudo der certo e nada der errado, eu executar a aplicação e acessar o endpoint http://localhost:<
, vamos receber as seguintes informações no console:
info: MeasureTimeRequestFilter[0]
Started measuring time for Request type Guid at 04/18/2025 16:47:53
info: LoggerRequestFilter[0]
Request type Guid execution started: 04/18/2025 16:47:53
info: LoggerRequestFilter[0]
Request type Guid execution finished: 04/18/2025 16:47:53
info: MeasureTimeRequestFilter[0]
Request type Guid execution time: 3 ms
Chegamos ao final da parte 2 do post Reinventando a Roda: Criando seu próprio MediatR!
Você pode acessar o código fonte dessa implementação nesse link: https://github.com/angelobelchior/TheMediator
Espero que vocês tenham curtido esses posts. Eu me diverti muito implementando o código e escrevendo!!!
Em breve a gente passa a régua no assunto!!
Forte abraço e até mais!!!