Como o MediatR pode ajudar no meu projeto?

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.

Sequência de execução dos behaviours e handlers

Sequência de execução dos Behaviours e Handlers

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.

Endpoints REST, gRPC e consumidores de evento, todos usando o MediatR para delegar o tratamento para a camada de application

Múltiplas fontes, tratamento padronizado dos requests

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.

Outras opções para compartilhar:
comments powered by Disqus