Gestion des exceptions ASP.NET à l'aide de IRO.Mvc.MvcExceptionHandler



Si vous êtes un développeur backend c #, vous devrez probablement tôt ou tard trouver un moyen unifié de gérer des situations exceptionnelles. Bien que, même si vous êtes satisfait du code 500 dans la réponse, cet article vous aidera à améliorer votre méthode, sans forcer quoi que ce soit à réécrire.

Nous parlerons de la bibliothèque ASP.NET, qui vous permet de résoudre ce problème le plus élégamment possible. Pour ceux qui sont trop paresseux pour lire un long article - le readme et la bibliothèque elle-même sont ici , un exemple est ici . Disponible sur nuget.org et je ne serai heureux que si cela profite à quiconque. Et donc, passons au code. Voyons d'abord les alternatives.

L'une des premières choses qui peut vous venir à l'esprit est de créer un DTO (Data transfer object) pour gérer les exceptions, intercepter une exception dans le contrôleur (bien qu'il ne soit pas nécessaire que ce soit une exception, il est possible de vérifier simplement null ou quelque chose comme ça), Remplissez les données dans le DTO et envoyez-les au client. Le code de cette méthode pourrait ressembler à ceci:

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

Une autre option consiste à utiliser des codes d'état HTTP pour cela.

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

Une pratique assez courante, mais ayant ses inconvénients: il peut être difficile de décrire l'essence de votre situation avec l'un des codes standard, c'est pourquoi le même code peut être interprété différemment même sur le même système, et il fournit également trop peu d'informations pour le débogage au développeur.

Et ici, certains peuvent même commencer à combiner ces deux méthodes, et dans des proportions différentes. Quelque part, ils oublieront d'envoyer le DTO, quelque part le code ne sera pas envoyé ou le mauvais sera envoyé, mais quelque part en général, il sera sérialisé avec les mauvais paramètres json et ne retournera pas ce qui est nécessaire.

Face à ce qui précède, beaucoup essaient de résoudre ce problème en utilisant app.UseExceptionHandler (); en gérant les exceptions à travers elle. C'est une bonne tentative, mais elle ne vous permettra pas d'oublier facilement le problème. Tout d'abord, vous serez toujours confronté au problème du choix d'un DTO pour les exceptions. Deuxièmement, un tel gestionnaire ne permettra pas de traiter les codes d'erreur http renvoyés par les contrôleurs, car Aucune exception ne s'est produite. Troisièmement, de cette façon, il n'est pas pratique de résoudre le problème de classification des erreurs, vous devrez écrire beaucoup de code pour joindre un message, du code http ou quelque chose à chaque exception. Et quatrièmement, vous perdez la possibilité d'utiliser AspA DeveloperExceptionPage, ce qui est très gênant pour le débogage. Même si vous résolvez ce problème d'une manière ou d'une autre, tous les développeurs de ce projet devront suivre strictement les spécifications, créer une gestion des erreurs spécifiquement sur les exceptions, ne pas renvoyer leurs DTO, sinon les erreurs dans votre API peuvent différer d'une méthode à l'autre.

Option de gestion des exceptions sélectionnée


Avant de montrer comment IRO.Mvc.MvcExceptionHandler vous permet de gérer les exceptions, je vais d'abord décrire comment je vois la gestion des exceptions idéale. Pour ce faire, nous établissons un certain nombre d'exigences:

  1. Ce devrait être un DTO, mais nous ne refusons pas non plus les codes http, car pour de nombreuses erreurs, elles sont toujours bien adaptées, peuvent être utilisées partout et dans un ancien projet que vous devez également soutenir, et elles sont tout simplement universelles. Le DTO standard inclura le champ IsError (qui permet d'écrire universellement la gestion des erreurs sur le client), il devrait également contenir le code d'erreur de la chaîne ErrorKey, que le développeur ne peut reconnaître immédiatement qu'en le regardant et qui fournit plus d'informations. En outre, vous pouvez ajouter un lien vers une page avec une description de cette erreur, si nécessaire.
  2. Tout est dans la prod. En mode développement, ce DTO doit retourner une trace de pile, demander des données: cookies, en-têtes, paramètres. À l'avenir, le middleware décrit dans l'article renvoie même un lien vers la DeveloperExceptionPage générée, qui vous permet de regarder la trace d'exception sous une forme pratique, mais plus à ce sujet plus tard.
  3. Le développeur peut lier ensemble l'exception, le code d'erreur http et ErrorKey. Cela signifie que s'il envoie du code 403 depuis le contrôleur, alors si le développeur lui a attaché une ErrorKey spécifique, le DTO avec lui sera retourné. Et vice versa, si une exception UnauthorizedAccessException se produit, elle sera liée au code http et à ErrorKey.

Il s'agit du format par défaut utilisé dans la bibliothèque:

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

Je dois dire tout de suite que la forme sous laquelle les données seront transmises au client peut être absolument quelconque, ce n'est là qu'une des options.

IRO.Mvc.MvcExceptionHandler


Je vais maintenant montrer comment j'ai résolu ce problème par moi-même en écrivant la bibliothèque IRO.Mvc.MvcExceptionHandler.

Nous connectons un gestionnaire d'exceptions comme tout autre middleware - dans la classe Startup.

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

À l'intérieur du délégué délégué, nous devons configurer notre middleware. Il est nécessaire de mapper (lier) les exceptions aux codes http et ErrorKey. Voici l'option de configuration la plus simple.

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

Comme je l'ai promis aux développeurs hardcore les plus paresseux qui ne sont pas habitués à gérer les exceptions - rien d'autre ne doit être fait. Ce code liera toutes les exceptions du pipeline ASP.NET au DTO commun avec le code 500 et le nom de l'exception sera écrit dans ErrorKey.

Il convient de comprendre que la méthode RegisterAllAssignable enregistre non seulement une exception du type spécifié, mais également tous ses descendants. Si vous souhaitez envoyer uniquement des informations sur des exceptions spécifiques au client, il est tout à fait raisonnable de créer votre exception ClientException et de la mapper uniquement. Dans le même temps, si vous définissez un code http pour ClientException et en définissez un autre pour son successeur SpecialClientException, le code SpecialClientException sera utilisé pour tous ses descendants, en ignorant le paramètre ClientException. Tout cela est mis en cache, il n'y aura donc aucun problème de performances.

Vous pouvez affiner et enregistrer votre code ErrorKey et http pour une exception spécifique:

  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 }); }); 

En plus du mappage, il vaut la peine de configurer les poids moyens. Vous pouvez spécifier les paramètres de sérialisation json, l'adresse de votre site, un lien vers la page de description des erreurs, le mode de fonctionnement du middleware via IsDebug, le code http standard pour les exceptions non gérées.

  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; 

La dernière propriété indique s'il est possible de lier notre DTO par le code http.
Vous pouvez également spécifier comment gérer les situations avec des exceptions internes, par exemple, TaskCanceledException avec une erreur interne enregistrée en raison de .Wait (). Par exemple, voici un résolveur standard qui supprime les exceptions internes de ces exceptions et fonctionne déjà avec elles:

  s.InnerExceptionsResolver = InnerExceptionsResolvers.InspectAggregateException; 

Si vous devez affiner la sérialisation, vous pouvez définir la méthode FilterAfterDTO. Renvoyez true pour désactiver le traitement standard et sérialiser errorContext.ErrorDTO comme vous le souhaitez. Il y a accès au HttpContext et à l'erreur elle-même.

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

DeveloperExceptionPage et autres avantages du mode débogage


Nous avons déterminé les paramètres. Voyons maintenant comment déboguer le tout. Dans le prod DTO, la réponse dans la réponse est simple et je l'ai déjà montré ci-dessus, maintenant je vais montrer à quoi ressemble le même DTO en mode débogage:



Comme vous pouvez le voir, il y a beaucoup plus d'informations ici, il y a stackrace et demander des données. Mais il est encore plus pratique de simplement suivre le lien dans le champ DebugUrl et d'afficher les données d'erreur sans forcer:



Il était assez difficile de mettre en œuvre cette fonction, DeveloperExceptionPage n'est tout simplement pas destiné à être utilisé par des développeurs tiers. Initialement, il était impossible d'ouvrir le lien dans un navigateur avec une session différente, le contenu a cessé d'être affiché après un redémarrage. Tout cela ne pourrait être résolu qu'en mettant en cache la réponse html de ce middlever. Vous pouvez maintenant au moins transmettre le lien d'exception à votre coéquipier si vous utilisez un serveur dédié partagé.

Conclusion


J'espère que les développeurs qui ont lu cet article ont trouvé un outil intéressant et utile pour eux-mêmes. Pour moi, cet article est en partie un test de l'utilité de ses développements et articles sur eux. J'ai des projets plus cool et prêts à l'emploi dont je voudrais parler à la communauté Habr.

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


All Articles