O MediatR é uma biblioteca pouco ambiciosa, feita para resolver um problema bem específico: como, dentro de um processo, desacoplar o envio de mensagens do manuseio das mesmas? E o MediatR faz isso de forma simples e elegante.
MediatR
A biblioteca foi criada por Jimmy Bogard, o mesmo autor do AutoMapper, e está atualmente na sua quinta versão. Um item bacana é que a biblioteca não tem dependências.
Requests e Handlers
O que eu mais gosto do MediatR é a forma como ele facilita mover toda lógica de negócio da camada de apresentação para a camada de aplicação.
Por exemplo, imagine que um microserviço tenha uma funcionalidade de adicionar um item ao carrinho. Neste caso podemos imaginar um endpoint parecido com este:
public class CarrinhoComprasController : ControllerBase
{
private readonly IMediator _mediator;
// NOTE QUE O MEDIATOR É INJETADO NO CONSTRUTOR.
// BEM PADRÃO NO ASP.NET Core.
public CarrinhoComprasController(IMediator mediator)
{
_mediator = mediator;
}
[HttpGet]
public async Task<ProdutosViewModel> AdicionarItemNoCarrinho(
[FromBody]AdicionarItemCarrinhoRequest request)
{
return await _mediator.Send(request); // AQUI, DESPACHAMOS UM REQUEST PARA O MEDIATOR
}
}
Como o nome implica, o MediatR é um mediador, ou seja, ele vai encaminhar o Request para quaisquer Handlers interessados. Vamos agora dar uma olhada na definição de um Request do MediatR:
public class AdicionarItemCarrinhoRequest : IRequest<ItemAdicionadoCarrinhoViewModel>
{
public string IdProduto { get; set; }
public string Referrer { get; set; }
}
public class ItemAdicionadoCarrinhoViewModel
{
public Guid IdItemCarrinho { get; set; }
}
No snippet acima, podemos ver que AdicionarItemCarrinhoRequest
é um IRequest
. Esta é uma interface do MediatR que simplesmente associada entrada e saída num contrato. Ou seja, AdicionarItemCarrinhoRequest
é um request que deve retornar um ItemAdicionadoCarrinhoViewModel
.
Já um handler terá essa cara:
public class AdicionarItemCarrinhoRequestHandler
: IRequestHandler<AdicionarItemCarrinhoRequest, ItemAdicionadoCarrinhoViewModel>
{
public async Task<ItemAdicionadoCarrinhoViewModel> Handle(AdicionarItemCarrinhoRequest request, CancellationToken cancellationToken)
{
// aqui vai lógica de negócio do caso de uso para listar os produtos
// o retorno abaixo é só para simplificar o exemplo
return await Task.FromResult(new ItemAdicionadoCarrinhoViewModel
{
IdItemCarrinho = Guid.NewGuid()
});
}
}
Por fim, para habilitar o MediatR num projeto ASP.NET Core usando o container de injeção de dependência padrão, basta fazer isso no Startup.cs
:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
// o tipo passado aqui é do assembly onde estiverem os Requests e Handlers
services.AddMediatR(typeof(Startup));
}
A wiki do MediatR tem exemplos de configuração para todos os containers mais populares.
Behaviours
Quando nossas aplicações vão crescendo, também cresce a necessidade de maior instrumentação. Logs padronizados entre todos os request, um tratamento de exceções mais uniforme, medição de performance e geração de métricas são apenas alguns exemplos de interesses transversais (cross-cutting concerns) que se mostram cada vez mais necessários.
Os Behaviours permitem abstrair justamente essa parte. Eles funcionam de forma semelhante aos Middlewares do ASP.NET Core. A seguir podemos ver um exemplo que loga o payload de qualquer Request se o log estiver em nível Debug:
public class RequestPayloadLoggingBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
{
private readonly ILogger _logger;
public RequestPayloadLoggingBehaviour(
ILogger<RequestPayloadLoggingBehaviour<TRequest, TResponse>> logger)
{
_logger = logger;
}
public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken,
RequestHandlerDelegate<TResponse> next)
{
if (_logger.IsEnabled(LogLevel.Debug))
{
var requestName = typeof(TRequest).Name;
var stringPayload = JsonConvert.SerializeObject(request);
_logger.LogDebug($"New request with payload of {requestName} as {stringPayload}");
}
return await next(); // EXECUTA O PRÓXIMO BEHAVIOUR OU HANDLER
}
}
Outro exemplo muito útil, e que costumo usar nos meus projetos, é um Behaviour para validar o Request usando o FluentValidation antes de mandar ele para os Handlers:
public class RequestValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
// Validators registrados para um determinado request
private readonly IEnumerable<IValidator<TRequest>> _validators;
public RequestValidationBehavior(IEnumerable<IValidator<TRequest>> validators)
{
_validators = validators;
}
public Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken,
RequestHandlerDelegate<TResponse> next)
{
if (_validators.Any())
{
var context = new ValidationContext(request);
var failures = _validators
.Select(v => v.Validate(context))
.SelectMany(result => result.Errors)
.Where(f => f != null)
.ToList();
if (failures.Count != 0)
{
// ABORTA O PROCESSO SE O REQUEST NÃO FOR VÁLIDO
throw new ValidationException(failures);
}
}
// EXECUTA O PRÓXIMO BEHAVIOUR OU HANDLER COM UM REQUEST VÁLIDO
return next();
}
}
Este Behaviour vai garantir que apenas Requests válidos seguem para processamento. Por fim, vejamos um exemplo básico de benchmark:
public class BenchmarkBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
private readonly ILogger _logger;
public BenchmarkBehavior(ILogger<BenchmarkBehavior<TRequest, TResponse>> logger)
{
_logger = logger;
}
public Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken,
RequestHandlerDelegate<TResponse> next)
{
var stopwatch = new Stopwatch();
stopwatch.Start();
// NOTE QUE A EXECUÇÃO DO HANDLER ACONTECE AQUI NO MEIO
var response = next();
stopwatch.Stop();
_logger.LogInformation($"Request {request.GetType().Name} took {stopwatch.Elapsed}");
return response;
}
}
A figura a seguir representa a sequência de execução de processamento dos behaviours. Cada Behaviour tem a oportunidade de implementar lógica antes e depois da execução do próximo.
A ordem de execução dos Behaviours é determinada durante o registro dos mesmos no container de injeção de dependência:
cfg.For(typeof(IPipelineBehavior<,>)).Add(typeof(OuterBehavior<,>));
cfg.For(typeof(IPipelineBehavior<,>)).Add(typeof(InnerBehavior<,>));
cfg.For(typeof(IPipelineBehavior<,>)).Add(typeof(ConstrainedBehavior<,>));
Testes
Outro ponto que é bastante beneficiado pelo uso do MediatR são os testes. Um teste de unidade ou integração pode ser tão simples quanto este a seguir:
[Fact]
public async Task Deve_adicionar_o_produto_123_no_carrinho()
{
// ARRANGE
var request = new AdicionarItemCarrinhoRequest
{
IdProduto = "123",
Referrer = "062d516d-356f-45be-a63c-d723e752d6f0"
};
// ACT
var viewModel = await new AdicionarItemCarrinhoRequestHandler(logger)
.Handle(request, CancellationToken.None);
// ASSERT
viewModel.IdItemCarrinho.Should().NotBe(Guid.Empty);
// outros asserts
}
Questões estilísticas
Uma forma de facilitar a navegação e a legibilidade do código dos Requests, é juntar Request e Handler no mesmo arquivo. Uma das opções é fazer assim:
public class AdicionarItemCarrinhoRequest : IRequest<ItemAdicionadoCarrinhoViewModel>
{
public string IdProduto { get; set; }
public string Referrer { get; set; }
}
public class AdicionarItemCarrinhoRequestHandler
: IRequestHandler<AdicionarItemCarrinhoRequest, ItemAdicionadoCarrinhoViewModel>
{
public async Task<ItemAdicionadoCarrinhoViewModel> Handle(AdicionarItemCarrinhoRequest request, CancellationToken cancellationToken)
{
// ...
}
}
Mas me parece melhor ainda aninhar o Handler dentro da classe Request. Essa foi uma dica do próprio Jimmy Bogard numa das palestras dele:
public class AdicionarItemCarrinhoRequest : IRequest<ItemAdicionadoCarrinhoViewModel>
{
public string IdProduto { get; set; }
public string Referrer { get; set; }
public class AdicionarItemCarrinhoRequestHandler
: IRequestHandler<AdicionarItemCarrinhoRequest, ItemAdicionadoCarrinhoViewModel>
{
public async Task<ItemAdicionadoCarrinhoViewModel> Handle(AdicionarItemCarrinhoRequest request, CancellationToken cancellationToken)
{
// ...
}
}
}
Desta forma, podemos ver rapidamente no que consiste o Request (seus parâmetros), e como será processado (o handler).
Alguns pensamentos
Microserviços podem variar muito em termos de complexidade e sofisticação. Por definição, a ideia é que sejam pequenos; mas o tamanho reduzido não os livra de ter que lidar com vários aspectos da vida em meio a um ecossistema de microserviços.
E é justamente por isso que um microserviço muitas vezes precisa conversar com outros para cumprir sua tarefa. Essas comunicações ocorrem majoritariamente em duas categorias: chamadas síncronas com alguma tecnologia de RPC (Remote Procedure Call) como REST, Thrift ou gRPC, ou assíncronas, usando algum broker como RabbitMQ ou Amazon SNS.
E nada impede que um mesmo serviço disponibilize endpoints REST, gRPC e ainda consuma mensagens de algum broker. E neste caso, é sempre interessante ser capaz de processar as regras de negócio de forma padronizada, independente do gatilho. E para isso, o MediatR cai como uma luva.
O diagrama acima mostra um cenário em que os Behaviours do MediatR acabam apresentando uma grande vantagem em relação aos middlewares do ASP.NET Core, pois permitem qualquer tipo de entrada do serviço, desde as mais comuns, suportadas nativamente pelo ASP.NET Core, até as mais exóticas, feitas in-house.