Tecnologia
Teste de performance de filtros globais para tratamento de exception em projetos feitos em NET Core
6 minutos de leitura
É 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.
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.
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:
- Add
- New Project
- Selecione ASP.NET Core Web API
- Next
- Preencha o Project Name
- Salve o novo projeto na mesma pasta da solução
- Selecione o .net 6 como Framework
- 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.
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.