.NET

7 jan, 2019

Tratamento global de Exceptions no ASP.Net Core

Publicidade

Desde as primeiras versões do C# temos à nossa disposição o famoso bloco try…catch, onde podemos capturar exceções e tratá-las da melhor forma possível, seja gravando um log, adicionando uma mensagem amigável para o usuário, etc.

Porém, muitas vezes não conseguimos prever todos os possíveis erros que possam acontecer e muitas exceções acabam “explodindo” na tela para o usuário. Ou pior, causam a queda de nossos sistemas e até mesmo prejuízos financeiros para nossos clientes.

Uma forma de prevenir que ocorram exceptions não tratadas é fazendo o seu gerenciamento de forma global, assim podemos ter um local centralizado onde todas as exceções são capturadas, facilitando a manutenção e deixando nossas classes mais limpas e legíveis, sem a necessidade de usar o bloco try…catch em cada método de seu sistema.

O uso exagerado do bloco try…catch pode tornar o código mais verboso e sua leitura difícil. Tenha bom senso, você pode usá-lo onde realmente irá tratar a exception, gerar um log específico, tomar uma ação efetiva, etc. Caso contrário, deixe a exception subir a stack e ser capturada pelo filtro global.

Existem diversas formas de se tratar as exceptions de forma global, seja usando Middlewares, Action Filters, ferramentas específicas de AOP (Aspect-Oriented Programming), entre outros. Hoje veremos como fazer isso com Middlewares.

Lembrando que o código fonte de demonstração está em meu GitHub.

O que são Middlewares?

Antes de mais nada, é preciso entender o que são os famosos Middlewares do ASP.Net Core.

Os Middlewares basicamente são componentes que definem o pipeline de execução dos requests HTTP. Ou seja, todos os passos que seu request faz dentro da aplicação desde a sua recepção até a resposta.

Eles são encadeados, então um middleware pode executar outro através de delegates repassando o mesmo contexto da requisição. Quando uma resposta é gerada em algum passo dentro do pipeline, a execução volta para o passo anterior e assim por diante.

Você pode criar trechos de código que executam antes ou depois do próximo passo do pipeline. Eles são configurados em ordem no método Configure da classe Startup, através da interface IApplicationBuilder.

Agora que você já sabe o que são Middlewares, podemos criar nosso componente de tratamento centralizado de exceptions.

Para mais informações sobre a criação de Middlewares no ASP.Net Core, recomendo a leitura da documentação oficial.

Middleware Nativo do ASP.Net Core

O ASP.Net Core possui um Middleware nativo para tratamento de exceptions, que pode ser configurado através do método UseExceptionHandler.

public static class ExceptionHandlerExtensions
{
	public static void UseGlobalExceptionHandler(this IApplicationBuilder app, ILoggerFactory loggerFactory)
	{
		app.UseExceptionHandler(builder =>
		{
			builder.Run(async context =>
			{
				var exceptionHandlerFeature = context.Features.Get<IExceptionHandlerFeature>();

				if (exceptionHandlerFeature != null)
				{
					var logger = loggerFactory.CreateLogger("GlobalExceptionHandler");
					logger.LogError($"Unexpected error: {exceptionHandlerFeature.Error}");

					context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
					context.Response.ContentType = "application/json";

					var json = new
					{
						context.Response.StatusCode,
						Message = "An error occurred whilst processing your request",
						Detailed = exceptionHandlerFeature.Error
					};

					await context.Response.WriteAsync(JsonConvert.SerializeObject(json));
				}
			});
		});
	}
}
public class Startup
{
	public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
	{
		// ... código omitido
		
		app.UseGlobalExceptionHandler(loggerFactory);
	}
}

Neste exemplo, o que fizemos foi extender o comportamento padrão do método capturando a exception gerada, então gravamos um log de erro e retornamos uma mensagem para o usuário com HTTP Status Code 500 (Internal Server Error) em formato JSON. Veja que recebemos uma instância de ILoggerFactory via parâmetro do método como dependência para geração do log de erros.

Parte da mensagem que será retornada para o client é a própria exception através do campo Detailed. Eu recomendo não retornar os detalhes da exception em uma aplicação real de produção, pois um usuário mal intencionado pode ver esses detalhes e descobrir brechas para atacar seu sistema.

Em ambiente de desenvolvimento não vejo nada de errado em mostrar os detalhes das exceptions, até mesmo para ajudar a resolver os possíveis problemas mais rapidamente. Você pode facilmente verificar em qual ambiente sua aplicação está usando a interface IHostingEnvironment.

Para que seu método de extensão tenha efeito, será necessário chamá-lo na classe Startup dentro do método Configure.

Middleware customizado

Com a interface IMiddleware, você pode facilmente criar seu próprio Middleware customizado. O uso dessa interface implica que seu middleware será do tipo Factory-based, sendo necessário também fazer sua configuração no container de DI, conforme veremos mais adiante.

public class GlobalExceptionHandlerMiddleware : IMiddleware
    {
        private readonly ILogger<GlobalExceptionHandlerMiddleware> _logger;

        public GlobalExceptionHandlerMiddleware(ILogger<GlobalExceptionHandlerMiddleware> logger)
        {
            _logger = logger;
        }

        public async Task InvokeAsync(HttpContext context, RequestDelegate next)
        {
            try
            {
                await next(context);
            }
            catch (Exception ex)
            {
                _logger.LogError($"Unexpected error: {ex}");
                await HandleExceptionAsync(context, ex);
            }
        }

        private static Task HandleExceptionAsync(HttpContext context, Exception exception)
        {
            context.Response.ContentType = "application/json";
            context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;

            var json = new
            {
                context.Response.StatusCode,
                Message = "An error occurred whilst processing your request",
                Detailed = exception
            };
            
            return context.Response.WriteAsync(JsonConvert.SerializeObject(json));
        }
    }
public static class GlobalExceptionHandlerMiddlewareExtensions
{
	public static IServiceCollection AddGlobalExceptionHandlerMiddleware(this IServiceCollection services)
	{
		return services.AddTransient<GlobalExceptionHandlerMiddleware>();
	}

	public static void UseGlobalExceptionHandlerMiddleware(this IApplicationBuilder app)
	{
		app.UseMiddleware<GlobalExceptionHandlerMiddleware>();
	}
}
public class Startup
{
	public void ConfigureServices(IServiceCollection services)
	{
		// ... código omitido
		
		services.AddGlobalExceptionHandlerMiddleware();	
	}

	public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
	{
		// ... código omitido
		
		app.UseGlobalExceptionHandlerMiddleware();
	}
}

Nosso middleware recebe em seu construtor uma dependência de ILogger para a geração de logs de erro, diferentemente do exemplo anterior, que recebia a dependência de log via parâmetro no método UseGlobalExceptionHandler.

O método InvokeAsync será chamado automaticamente. Nele, existe uma chamada ao método next(context) que executará o próximo passo do pipeline. Ele está contido em um bloco try…catch.

Caso alguma exception seja lançada, ela será capturada e enviada para o método HandleExceptionAsync, onde retornamos uma mensagem JSON para o client, exatamente como no exemplo anterior.

Algumas alterações também devem ser feitas na classe Startup. Para simplificar, criei alguns métodos de extensão na classe GlobalExceptionHandlerMiddlewareExtensions.

Como nosso middleware é ativado através da classe MiddlewareFactory, ele deve ser registrado no container de DI através do método AddGlobalExceptionHandlerMiddleware. Em seguida, basta fazer a chamada ao método UseGlobalExceptionHandlerMiddleware para adicioná-lo ao pipeline do ASP.Net Core.

Lembre-se de que o pipeline é executado na ordem em que foi definido no método Configure, então ele deve ficar antes da definição de uso do MVC.

Controllers

Para ambos os casos, as Controllers de nossa API ficam limpas, não sendo necessário fazer uso de blocos try…catch, conforme mencionei anteriormente. Todas as exceções não tratadas serão capturadas pelo mecanismo de tratamento global.

[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
	private readonly FakeData _data;

	public ValuesController(FakeData data)
	{
		_data = data;
	}

	[HttpGet]
	public ActionResult<int> Get()
	{
		return _data.GetRandomNumber();
	}
}

Testes

Nossa API de testes contém um único endpoint chamado “api/values” que retornará um valor numérico aleatório. Para simulação, eu criei uma classe auxiliar que irá gerar exceptions também de forma aleatória, então algumas chamadas à API irão funcionar e outras irão disparar uma exception.

A chamada a seguir retorna um valor numérico, com HTTP Status Code 200 indicando o sucesso da requisição.

O próximo exemplo mostra um objeto JSON que contém o erro ocorrido, bem como os detalhes da exception, com HTTP Status Code 500 indicando a falha na requisição.

Menção honrosa

Eu gostaria de indicar aqui o pacote nuget GlobalExceptionHandler, criado por Joseph Woodward, que também resolve o problema de tratamento global de exceções usando métodos de extensão ao middleware nativo (convention-based).

A diferença é que ele possui alguns recursos bem legais de customização como, por exemplo, mapeamento de exceptions para determinado HTTP Status Code, mensagens diferentes para cada exception, negociação de conteúdo, entre outros.

Conclusão

A captura de exceções é uma parte crucial em todas as aplicações e o correto tratamento delas pode nos ajudar no rápido troubleshooting dos problemas e suas respectivas correções.

Ignorar que elas existem ou não fazer um tratamento adequado delas é um problema sério que existe em muitas aplicações corporativas hoje em dia.

Não pense que sua aplicação estará livre delas, pois como seu nome sugere, exceções ao comportamento esperado do seu sistema podem e irão ocorrer em algum momento, esteja preparado para isso e seja feliz.

Existe uma especificação formal que basicamente define um formato padrão de mensagens de erro nas respostas de APIs HTTP. Esse padrão é conhecido como Problem Details e foi implementado no ASP.Net Core 2.1.

Pretendo cobri-lo em um artigo posterior como um complemento à este, já que a implementação é bem parecida com o que fizemos aqui.

Referências

Artigo do autor pode ser lido também aqui:
https://static.imasters.com.br/wp-content/uploads/2019/01/10105324/Tratamento-global-de-Exceptions-no-ASP.Net-Core.jpg/