Manipulando exceções do ASP.NET usando IRO.Mvc.MvcExceptionHandler



Se você é um desenvolvedor de back-end em c #, provavelmente mais cedo ou mais tarde precisará encontrar uma maneira unificada de lidar com situações excepcionais. Embora, mesmo se você estiver satisfeito com o código 500 na resposta, este artigo ainda ajudará a melhorar seu método, sem forçar a reescrita.

Falaremos sobre a biblioteca ASP.NET, que permite resolver esse problema da maneira mais elegante possível. Para quem tem preguiça de ler um longo artigo - o leia-me e a própria biblioteca estão aqui , um exemplo está aqui . Disponível no nuget.org e ficarei feliz em beneficiar alguém. E então, vamos ao código. Primeiro, vamos dar uma olhada nas alternativas.

Uma das primeiras coisas que pode vir à mente é criar um DTO (objeto de transferência de dados) para lidar com exceções, capturar uma exceção no controlador (embora não seja necessário que seja uma exceção, é possível apenas verificar nulo ou algo assim), Preencha os dados no DTO e envie-os para o cliente. O código para este método pode ser algo como isto:

public IActionResult Get() { try { //Code with exception. } catch (Exception ex) { return new JsonResult( new ErrorDto { IsError = true, Message = ex.Message }); } } 

Outra opção é usar códigos de status HTTP para isso.

 public IActionResult Get() { try { //Code with exception. } catch (Exception ex) { return BadRequest(); } } 

Uma prática bastante comum, mas com suas desvantagens: pode ser difícil descrever a essência da sua situação com um dos códigos padrão, e é por isso que o mesmo código pode ser interpretado de maneira diferente, mesmo no mesmo sistema, e também fornece poucas informações para depuração do desenvolvedor.

E aqui alguns podem até começar a combinar esses dois métodos, e em diferentes proporções. Em algum lugar eles esquecerão de enviar o DTO, em algum lugar o código não será enviado ou o errado será enviado, mas em geral ele será serializado com as configurações erradas do json e não retornará o que é necessário.

Diante do exposto, muitos estão tentando resolver esse problema usando app.UseExceptionHandler (); manipulando exceções por meio dele. É uma boa tentativa, mas não permitirá que você esqueça facilmente o problema. Primeiro, você ainda enfrentará o problema de escolher um DTO para exceções. Em segundo lugar, esse manipulador não permitirá processar códigos de erro http retornados dos controladores, porque Uma exceção não ocorreu. Em terceiro lugar, dessa maneira, é inconveniente resolver o problema de classificação de erros, você precisará escrever muito código para anexar uma mensagem, código http ou algo a cada exceção. Em quarto lugar, você perde a oportunidade de usar o AspA DeveloperExceptionPage, o que é muito inconveniente para a depuração. Mesmo que você resolva esse problema de alguma forma, todos os desenvolvedores deste projeto terão que seguir rigorosamente as especificações, criar tratamento de erros especificamente em exceções, não retornar seus DTOs, caso contrário, os erros na sua API poderão parecer diferentes de método para método.

Opção de tratamento de exceção selecionada


Antes de mostrar como IRO.Mvc.MvcExceptionHandler permite que você lide com exceções, primeiro descreverei como vejo o tratamento ideal de exceções. Para fazer isso, estabelecemos vários requisitos:

  1. Deve ser um DTO, mas também não recusamos códigos http, porque para muitos erros, eles ainda são adequados, podem ser usados ​​em qualquer lugar e em um projeto antigo que você também precisa apoiar, e eles são simplesmente universais. O DTO padrão incluirá o campo IsError (que permite gravar universalmente o tratamento de erros no cliente), também deve conter o código de erro da string ErrorKey, que o desenvolvedor pode reconhecer imediatamente apenas olhando para ele e fornecendo mais informações. Além disso, você pode adicionar um link para uma página com uma descrição desse erro, se necessário.
  2. Isso é tudo no prod. No modo de desenvolvimento, esse DTO deve retornar um rastreamento de pilha, solicitar dados: cookies, cabeçalhos, parâmetros. No futuro, o middleware descrito no artigo retorna um link para o DeveloperExceptionPage gerado, que permite assistir a exceção de exceção de uma forma conveniente, mas mais sobre isso mais tarde.
  3. O desenvolvedor pode vincular a exceção, código de erro http e ErrorKey. Isso significa que, se ele enviar código 403 do controlador, se o desenvolvedor anexar uma ErrorKey específica a ele, o DTO com ele será retornado. E vice-versa, se ocorrer uma UnauthorizedAccessException, ela será vinculada ao código http e à ErrorKey.

Este é o formato padrão usado na biblioteca:

 { "__IsError": true, "ErrorKey": "ClientException", "InfoUrl": "https://iro.com/errors/ClientException" } 

Devo dizer imediatamente que a forma em que os dados serão transmitidos ao cliente pode ser absolutamente qualquer, essa é apenas uma das opções.

IRO.Mvc.MvcExceptionHandler


Agora vou mostrar como resolvi esse problema escrevendo a biblioteca IRO.Mvc.MvcExceptionHandler.

Conectamos um manipulador de exceção como qualquer outro middleware - na classe Startup.

 app.UseMvcExceptionHandler((s) => { //Settings... }); 

Dentro do delegado que está sendo delegado, precisamos configurar nosso middleware. É necessário mapear (vincular) exceções aos códigos http e ErrorKey. Abaixo está a opção de configuração mais fácil.

  s.Mapping((builder) => { builder.RegisterAllAssignable<Exception>( httpCode: 500, errorKeyPrefix: "Ex_" ); }); 

Como prometi aos desenvolvedores hardcore mais preguiçosos que não estão acostumados a lidar com exceções - nada mais precisa ser feito. Esse código vinculará todas as exceções no pipeline do ASP.NET ao DTO geral com o código 500 e o nome da exceção será gravado no ErrorKey.

Vale a pena entender que o método RegisterAllAssignable não apenas registra uma exceção do tipo especificado, mas também todos os seus descendentes. Se você deseja enviar apenas informações sobre exceções específicas ao cliente, é uma decisão completamente razoável criar sua ClientException e mapeá-la apenas. Ao mesmo tempo, se você definir um código http para ClientException e outro para seu sucessor SpecialClientException, o código SpecialClientException será usado para todos os seus descendentes, ignorando a configuração ClientException. Tudo isso é armazenado em cache, portanto, não haverá problemas de desempenho.

Você pode ajustar e registrar o ErrorKey e o código http para uma exceção específica:

  s.Mapping((builder) => { //By exception, custom error key. builder.Register<ArgumentNullException>( httpCode: 555, errorKey: "CustomErrorKey" ); //By http code. builder.Register( httpCode: 403, errorKey: "Forbidden" ); //By exception, default ErrorKey and http code. builder.Register<NullReferenceException>(); //Alternative registration method. builder.Register((ErrorInfo) new ErrorInfo() { ErrorKey = "MyError", ExceptionType = typeof(NotImplementedException), HttpCode = 556 }); }); 

Além do mapeamento, vale a pena configurar pesos médios. Você pode especificar as configurações de serialização do json, o endereço do seu site, um link para a página de descrição do erro, o modo de operação do middleware através do IsDebug, o código http padrão para exceções não tratadas.

  s.ErrorDescriptionUrlHandler = new FormattedErrorDescriptionUrlHandler("https://iro.com/errors/{0}"); s.IsDebug = isDebug; s.DefaultHttpCode = 500; s.JsonSerializerSettings.Formatting = Formatting.Indented; s.Host="https://iro.com"; s.CanBindByHttpCode = true; 

A última propriedade indica se é possível vincular nosso DTO pelo código http.
Você também pode especificar como lidar com situações com exceções internas, por exemplo TaskCanceledException com um erro interno registrado devido a .Wait (). Por exemplo, aqui está um resolvedor padrão que remove exceções internas de tais exceções e já trabalha com elas:

  s.InnerExceptionsResolver = InnerExceptionsResolvers.InspectAggregateException; 

Se você precisar ajustar a serialização, poderá definir o método FilterAfterDTO. Retorne true para desativar o processamento padrão e serialize errorContext.ErrorDTO como desejar. Há acesso ao HttpContext e ao próprio erro.

  s.FilterAfterDTO = async (errorContext) => { //Custom error handling. Return true if MvcExceptionHandler must ignore current error, //because it was handled. return false; }; 

DeveloperExceptionPage e outras vantagens do modo de depuração


Nós descobrimos as configurações, agora vamos descobrir como depurar tudo. No DTO prod, a resposta na resposta é simples e eu já mostrei acima, agora mostrarei como o mesmo DTO parece no modo de depuração:



Como você pode ver, há muito mais informações aqui, há stackrace e solicita dados. Mas é ainda mais conveniente simplesmente seguir o link no campo DebugUrl e exibir os dados do erro sem sobrecarregar:



Foi bastante difícil implementar essa função, pois DeveloperExceptionPage simplesmente não se destina a ser usado por desenvolvedores de terceiros. Inicialmente, era impossível abrir o link em um navegador com uma sessão diferente, o conteúdo deixou de ser exibido após uma reinicialização. Tudo isso poderia ser resolvido apenas armazenando em cache a resposta html desse intermediário. Agora você pode pelo menos transmitir o link de exceção ao seu companheiro de equipe se você usar um servidor dedicado compartilhado.

Conclusão


Espero que os desenvolvedores que leem este artigo tenham encontrado uma ferramenta interessante e útil para eles. Para mim, este artigo é parcialmente um teste da utilidade de seus desenvolvimentos e artigos sobre eles. Tenho alguns projetos mais legais já prontos que gostaria de contar à comunidade Habr.

Source: https://habr.com/ru/post/pt469731/


All Articles