Ir para o conteúdo

É muito comum no processo de desenvolvimento de APIs a criação de abstrações globais para tratamento de mensagens genéricas para validação de dados. Entretanto, muitas vezes essas abstrações podem degradar o tempo de resposta do sistema, sendo um exemplo dessa abstração a utilização de filtros para o tratamento global de exceptions lançadas para tratar validações de negócio.

Esse artigo mostrará três abordagens para o retorno de erros para que seja possível comparar a performance entre usar e não usar os filtros globais para tratamento de exceptions. Para isso será utilizado a biblioteca BenchmarkDotNet para criar testes de performance dos endpoints das APIs.

Introdução

É comum encontrar projetos que utilizam exceptions para a validação de dados. Vindo de encontro a essa prática, também é comum a criação de filtros globais para padronizar as respostas da API para as exceptions de validação de dados. Entretanto, nem sempre é possível ver exemplos que demonstrem o impacto de performance gerado por essa prática.

Dessa forma, através da criação de três APIs será possível comparar e exemplificar a diferença de performance entre projetos que usam o tratamento global de exceptions criadas para validação de dados e projetos que não utilizam essa abordagem.

Todos os projetos terão a finalidade de simplesmente expor um endpoint com verbo POST onde retornaram um status code 400 quando não informado o CPF, sendo todos também criado na versão 6.0 do dotnet, sendo essa a última versão LTS lançada a até a data de publicação desse artigo.

No primeiro exemplo será simplesmente disparada uma exception de negócio e o retorno 400 será tratado através de um filtro global. No segundo exemplo teremos a mesma exception sendo lançada, só que dessa vez o retorno 400 estará no catch do bloco try. Já no terceiro exemplo, será criado um endpoint que simplesmente retornará um status code 400 sem o uso de exception, sendo esse último exemplo usado como baseline para comparação de performance entre as três abordagens para tratamento de validações de negócio.

Para a realização dos testes de performance será utilizado a biblioteca BenchmarkDotNet, sendo uma biblioteca de código aberto, desenvolvida e mantida pela equipe de desenvolvimento da Microsoft. De acordo com a documentação, ela também é utilizada para testes de performance por outros projetos da Microsoft como: ASP.NET Core, Entity Framework Core, além também de ser utilizada pela equipe do Dapper, Serilog, Autofac e MediatR.

Criação dos projetos

A criação do projeto é bastante simples, utilizando o Visual Studio 2022, através do menu “File -> New Project” é possível ter acesso aos “templates” oferecidos pela IDE, nesse caso será utilizado o template “Console App” e tendo o C# como linguagem de programação, conforme pode-se ver na Figura 1.

Figura 1 – Selecionando o template do projeto para Benchmark

Conforme dito, o processo de criação é simples, basta ir seguindo o Wizard de criação de projeto, preenchendo o nome do projeto com “TestePerformance” e o nome da solução de “TestePerformance”, como apresentado pela Figura 2.

Figura 2 – Nomeando o projeto e a solução

Para realizar o Benchmark dos endpoints será necessário a instalação de 2 pacotes BenchmarkDotNet e BenchmarkDotNet.Annotations no projeto TestePerformance. Também será necessário a instalação do Microsoft.AspNetCore.Mvc.Testing para que seja possível rodar o benchmark dos endpoins sem ter que executar os webapis.

Mas antes de continuar com a implementação dos métodos de Benchmark vamos criar os três projetos de API, ValidacaoComExceptionFilter, ValidacaoComTryCatch e ValidacaoSemException. No Solution Explorer selecione a Solution e com o direito do mouse siga os seguintes passos para criar os três projetos:

  1. Add
  2. New Project
  3. Selecione ASP.NET Core Web API
  4. Next
  5. Preencha o Project Name
  6. Salve o novo projeto na mesma pasta da solução
  7. Selecione o .net 6 como Framework
  8. Clique em Create

Altere a classe Program nos três novos projetos para:

internal class Program
{
    private static void Main(string[] args)
    {
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder => webBuilder.UseStartup<Startup>())
            .Build()
            .Run();
    }
}

Crie a Startup nos três projetos com o seguinte código:

public sealed class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers();
        services.AddEndpointsApiExplorer();
        services.AddSwaggerGen();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseSwagger();
            app.UseSwaggerUI();
        }
        app.UseRouting();
        app.UseHttpsRedirection();

        app.UseAuthorization();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
    }
}

Agora crie o controller produtos no projeto ValidacaoComExceptionFilter com o seguinte código:

[Route("api/[controller]")]
[ApiController]
public class ProdutosController : ControllerBase
{
    [HttpPost]
    public IActionResult Post(string? cpf)
    {
        if (string.IsNullOrEmpty(cpf))
        {
            throw new BusinessException("Validação por exception");
        }
        return Ok();
    }
}

Crie também a BusinessException com o seguinte código:

namespace ValidacaoComExceptionFilter
{
    [Serializable]
    internal class BusinessException : Exception
    {
        public BusinessException()
        {
        }

        public BusinessException(string? message) : base(message)
        {
        }

        public BusinessException(string? message, Exception? innerException) : base(message, innerException)
        {
        }

        protected BusinessException(SerializationInfo info, StreamingContext context) : base(info, context)
        {
        }
    }
}

O próximo passo é criar o filtro global que irá retornar o status code 400. Para nível de exemplo, o filtro será bastante simples, pois o foco é só avaliar a performance do uso do filtro para tratamento de exceptions. Para isso crie a classe ExceptionFilter com o seguinte código:

public class ExceptionFilter : IExceptionFilter
{
    public void OnException(ExceptionContext context)
    {
        if (context.Exception is BusinessException)
        {
            context.Result = new BadRequestObjectResult(

quot;ExceptionFilter: {context.Exception.Message}");
context.ExceptionHandled = true;
}
}
}

E registre o filtro na startup dentro nas options do AddControllers, ficando da seguinte forma o ConfigureService:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers(options=> {
        options.Filters.Add<ExceptionFilter>();
    });
    services.AddEndpointsApiExplorer();
    services.AddSwaggerGen();
}

Agora vamos criar o endpoint que irá retornar o erro 400 usando o bloco try/catch, para isso crie o controller produtos no projeto ValidacaoComTryCatch com o seguinte código:

[Route("api/[controller]")]
[ApiController]
public class ProdutosController : ControllerBase
{
    [HttpPost]
    public IActionResult Post(string? cpf)
    {
        try
        {
            if (string.IsNullOrEmpty(cpf))
            {
                throw new BusinessException("Validação por exception");
            }
        }
        catch (BusinessException ex)
        {
            return BadRequest(

quot;TryCatch {ex.Message}");
}

return Ok();
}
}

Criei também o BusinessException dentro do projeto ValidacaoComTryCatch.

E por último vamos criar o endpoint que irá retornar direto o erro 400 no projeto ValidacaoSemException com o seguinte código:

[Route("api/[controller]")]
[ApiController]
public class ProdutosController : ControllerBase
{
    [HttpPost]
    public IActionResult Post(string? cpf)
    {
        if (string.IsNullOrEmpty(cpf))
        {
            return BadRequest(

quot;SemException validação de exemplo");
}
return Ok();
}
}

Agora podemos voltar para o projeto TestePerformance e implementar os métodos de Benchmark que irá comparar a performance dos três endpoints.

Crie a classe BenchmarkApi, ela será responsável por criar um HttpClient usando o WebApplicationFactory para cada um dos endpoints que iremos comparar suas performances, ficando da seguinte forma sua implementação:

[MemoryDiagnoser]
public class BenchmarkApi
{
    private HttpClient _httpClientExceptionFilter;
    private HttpClient _httpClientComTryCatch;
    private HttpClient _httpClientSemException;

    [GlobalSetup]
    public void GlobalSetup()
    {
        _httpClientExceptionFilter = new WebApplicationFactory<ValidacaoComExceptionFilter.Startup>()
            .WithWebHostBuilder(configuration =>
            {
                configuration.ConfigureLogging(logging => { });
            }).CreateClient();

        _httpClientComTryCatch = new WebApplicationFactory<ValidacaoComTryCatch.Startup>()
            .WithWebHostBuilder(configuration =>
            {
                configuration.ConfigureLogging(logging => { });
            }).CreateClient();

        _httpClientSemException = new WebApplicationFactory<ValidacaoSemException.Startup>()
            .WithWebHostBuilder(configuration =>
            {
                configuration.ConfigureLogging(logging => { });
            }).CreateClient();
    }

    [Benchmark]
    public Task ValidacaoComExceptionFilter()
    {
        return _httpClientExceptionFilter.PostAsync("/api/produtos", null);
    }

    [Benchmark]
    public Task ValidacaoComTryCatch()
    {
        return _httpClientComTryCatch.PostAsync("/api/produtos", null);
    }

    [Benchmark(Baseline = true)]
    public Task ValidacaoSemException()
    {
        return _httpClientSemException.PostAsync("/api/produtos", null);
    }
}

O próximo passo e realizar a chamada no método Main da classe Program do projeto TestePerformance.

internal class Program
{
    private static void Main(string[] args)
    {
        BenchmarkRunner.Run<BenchmarkApi>();
    }
}

E não esqueça de adicionar as referências dos três projetos de API ao projeto TestePerformance.

Com tudo concluído, o próximo passo é executar o TestePerformance para ver o resultado gerado pela comparação da biblioteca BenchmarkDotNet. Porém, o recomendado é executar em modo release, para ser mais simples, vamos rodar pelo terminal integrado do Visual Studio com o seguinte comando na pasta do TestePerformance.

dotnet run -c Release

E após a conclusão teremos o resultado onde podemos ver que o endpoint que retornou direto o status code 400 tem um tempo médio de execução menor que os endpoints que usaram Exceptions para o tratamento de erros.

Figura 3 – Resultados obtidos

Conclusão

Com esse artigo podemos comparar a performance do fluxo de erro 400 de três aplicações bem simples, onde na primeira disparamos uma excpetion e tratamos o fluxo 400 através de um filtro global de exceptions. No segundo trouxemos uma abordagem com um bloco try/catch e o último com um retorno direto do fluxo de erro 400. Sendo que o último exemplo teve um tempo médio de execução melhor no Benchmark. Já o com tratamento por filtro global, a média chegou a ser o dobro comparado ao exemplo de retorno direto do BadRequest.

O propósito desse artigo não é dizer para não usar o filtro global para tratamento de exceptions, mas sim mostrar o quanto de performance dessa abordagem pode impactar na performance quando comparado com abordagens mais diretas no que tange os fluxos de tratamento de erros das aplicações que construímos.

O código fonte do bench pode ser baixado no github: https://github.com/pablotdv/teste-performance-exception-filter.

Outras publicações