Back-End

16 mai, 2018

Fail-fast Validations com Pipeline Behavior no MediatR e ASP.Net Core

Publicidade

Sabemos que o MediatR simplifica muito o design de nossas aplicações tornando nosso código mais simples e com baixo acoplamento, conforme eu mostrei no artigo anterior. Se você não viu, corre lá e depois volte aqui.

Um dos recursos mais legais dele é a possibilidade de executarmos determinadas ações antes ou depois de um Handler ser disparado. Esse recurso leva o nome de Pipeline Behavior.

Você pode usar o Pipeline Behavior para, por exemplo, validar os parâmetros de entrada dos Requests, autorizar o acesso à determinado Handler, gravar log dos Requests, etc.

Lembre-se do artigo anterior, quando eu me refiro a Request no MediatR não estou falando de uma requisição HTTP, mas sim da “mensagem” que é enviada para o MediatR, e que está associada a determinado Handler.

Como funciona?

O Pipeline Behavior é bem parecido com o pipeline de execução do ASP.Net Core, onde temos middlewares sendo executados sequencialmente.

Para usar esse recurso do MediatR, você deve criar uma classe que implementa a interface IPipelineBehavior. Nessa classe iremos definir o comportamento que desejamos.

public class LoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> 
{
    public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next) 
    {
        await Logger.InfoAsync($"Handling {typeof(TRequest).Name}");
        
        var response = await next();
        
        await Logger.InfoAsync($"Handled {typeof(TResponse).Name}");
        
        return response;
    }
}

Veja que um dos parâmetros do Pipeline Behavior é um Request. Além disso, ele usa o método next() para disparar o próximo passo do pipeline, que normalmente é a execução do respectivo Handler.

Após implementar a classe, você deve registrá-la no container de DI de sua preferência. No meu caso irei usar o container nativo do ASP.Net Core mais adiante.

Quando um Request for enviado para o MediatR, os Behaviors serão executados na sequência em que foram definidos no container de DI.

Fail-Fast Validations

Em nosso exemplo iremos implementar um Behavior para validar os parâmetros de entrada dos Requests antes de seus respectivos Handlers serem disparados, dessa forma antecipamos a falha no Request e poupamos nossa aplicação de processar um Handler para descobrir que os parâmetros não são válidos.

Normalmente a validação dos Requests é feita na entrada do Handler ou nas Actions dentro das Controllers da API, com chamadas para validar os inputs de dados. Isso torna o código um pouco sujo, na minha opinião. A implementação do Behavior torna essa tarefa automática.

Para implementar esse Behavior usaremos o MediatR em conjunto com a biblioteca FluentValidation, que acredito ser uma das mais conhecidas e utilizadas mundialmente quando falamos de validações. Caso você queira usar outra biblioteca de Notification Pattern nas validações, fique à vontade, as alterações seriam mínimas. Recomendo você dar uma olhada no Flunt.

Implementação

A classe a seguir representa nosso Behavior responsável por interceptar todos os Requests. Veja que injetamos em nosso Behavior todos os validadores do FluentValidation. No método Handle executamos os validadores, e caso existam falhas apontadas por eles, interrompemos o pipeline e então retornamos essas mensagens de erro.

public class FailFastRequestBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
        where TRequest : IRequest<TResponse> where TResponse : Response
    {
        private readonly IEnumerable<IValidator> _validators;

        public FailFastRequestBehavior(IEnumerable<IValidator<TRequest>> validators)
        {
            _validators = validators;
        }

        public Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
        {
            var failures = _validators
                .Select(v => v.Validate(request))
                .SelectMany(result => result.Errors)
                .Where(f => f != null)
                .ToList();

            return failures.Any()
                ? Errors(failures)
                : next();
        }

        private static Task<TResponse> Errors(IEnumerable<ValidationFailure> failures)
        {
            var response = new Response();

            foreach (var failure in failures)
            {
                response.AddError(failure.ErrorMessage);
            }

            return Task.FromResult(response as TResponse);
        }
    }

A seguir temos um Request e seu respectivo Validator que irá garantir que os parâmetros do Request estejam em conformidade.

public class CreateUser : IRequest<Response>
{
    public string Name { get; }
    public string Email { get; }
    public string Password { get; }
    public string ConfirmPassword { get; }

    public CreateUser(string name, string email, string password, string confirmPassword)
    {
        Name = name;
        Email = email;
        Password = password;
        ConfirmPassword = confirmPassword;
    }
}
public class CreateUserValidator : AbstractValidator<CreateUser>
{
    public CreateUserValidator()
    {
        RuleFor(a => a.Name)
            .NotEmpty()
            .WithMessage("O Nome é obrigatório");

        RuleFor(a => a.Email)
            .EmailAddress()
            .WithMessage("E-mail inválido");

        RuleFor(a => a.Password)
            .NotEmpty()
            .WithMessage("A Senha é obrigatória");

        RuleFor(a => a.ConfirmPassword)
            .NotEmpty()
            .WithMessage("A Confirmação de Senha é obrigatória");

        RuleFor(a => a.ConfirmPassword)
            .Equal(b=> b.Password)
            .WithMessage("As Senhas não conferem");
    }
}

Na classe Startup de nossa API temos o método AddMediatr, que é responsável por fazer a devida injeção de dependências do MediatR além dos validadores de Requests.

public class Startup
{
	// ... restante do arquivo
		
    public void ConfigureServices(IServiceCollection services)
    {
        AddApplicationServices(services);

        services.AddMvc();
    }
		
    private static void AddApplicationServices(IServiceCollection services)
    {
        services.AddScoped<IUserRepository, UserRepository>();
        AddMediatr(services);
    }

    private static void AddMediatr(IServiceCollection services)
    {
        const string applicationAssemblyName = "DemoMediatR.Application";
        var assembly = AppDomain.CurrentDomain.Load(applicationAssemblyName);

        AssemblyScanner
            .FindValidatorsInAssembly(assembly)
            .ForEach(result => services.AddScoped(result.InterfaceType, result.ValidatorType));

        services.AddScoped(typeof(IPipelineBehavior<,>), typeof(FailFastRequestBehavior<,>));

        services.AddMediatR();
    }
}

Na controller apenas enviamos o Request para o MediatR e então recuperamos seu retorno. Caso ele retorne mensagens de erro, elas são exibidas com o Http Status Code 400 (Bad Request).

[Route("api/[controller]")]
public class AccountsController : Controller
{
    private readonly IMediator _mediator;

    public AccountsController(IMediator mediator)
    {
        _mediator = mediator;
    }

    [HttpPost]
    public async Task<IActionResult> CreateUser([FromBody] CreateUser command)
    {
        var response = await _mediator.Send(command).ConfigureAwait(false);

        if (response.Errors.Any())
        {
            return BadRequest(response.Errors);
        }

        return Ok(response.Result);
    }
}

Podemos fazer os testes de execução da API com o Postman.

Conclusão

Você deve ter percebido que as mensagens são cumulativas, então podemos validar todos os parâmetros e retornar as falhas de uma única vez.

Muitas APIs por aí fazem a validação campo a campo, disparando uma Exception para cada propriedade inválida. Essa abordagem é muito ruim, na minha opinião, pois além de interromper o fluxo para cada propriedade inválida, ainda existe o lançamento de Exception que é muito oneroso para a aplicação.

Usando uma abordagem de Fail-fast com Notification Pattern, nosso código fica mais limpo e as validações dos Requests, caso existam, serão executadas automaticamente dentro do pipeline do MediatR antes que seus respectivos Handlers sejam disparados.

Quaisquer dúvidas ou sugestões, por favor entrem em contato. Não deixe de conferir o código fonte para essa demo que está no meu Github.

Abraços!

Referências