Handling ASP.NET Exceptions Using IRO.Mvc.MvcExceptionHandler





If you are a c # backend developer, you probably sooner or later need to find a unified way to handle exceptional situations. Although, even if you are content with the 500 code in the answer, this article will still help improve your method, while not forcing anything to rewrite.



We will talk about the ASP.NET library, which allows you to solve this problem as elegantly as possible. For those who are too lazy to read a long article - the readme and the library itself are here , an example is here . Available on nuget.org and I will only be glad if it will benefit anyone. And so, let's move on to the code. First, let's take a look at the alternatives.



One of the first things that may come to mind is to create a DTO (Data transfer object) for handling exceptions, catch an exception in the controller (although it is not necessary that it will be an exception, maybe it's just checking for null or something like that), Fill in the data in the DTO and send it to the client. The code for this method might look something like this:



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





Another option is to use HTTP status codes for this.



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





A fairly common practice, but having its drawbacks: it can be difficult to describe the essence of your situation with one of the standard codes, which is why the same code can be interpreted differently even on the same system, and it also provides too little information for debugging to the developer.



And here some may even begin to combine both of these methods, and in different proportions. Somewhere they will forget to send the DTO, somewhere the code will not be sent or the wrong one will be sent, but somewhere in general it will be serialized with the wrong json settings and will return not what is needed.



Faced with the above, many are trying to solve this problem using app.UseExceptionHandler (); by handling exceptions through it. This is a good attempt, but it will not let you easily forget about the problem. First, you will still face the problem of choosing a DTO for exceptions. Secondly, such a handler will not allow to process http error codes that were returned from controllers, because An exception did not occur. Thirdly, in this way it is inconvenient to solve the problem of error classification; you will have to write a lot of code to attach a message, http code or something to each exception. And fourthly, you lose the opportunity to use AspA DeveloperExceptionPage, which is very inconvenient for debugging. Even if you somehow solve this problem, then all the developers on this project will have to strictly follow the specifications, build error handling specifically on exceptions, do not return their DTOs, otherwise the errors in your api may look different from method to method.



Selected exception handling option



Before I show how IRO.Mvc.MvcExceptionHandler allows you to handle exceptions, I will first describe how I see the ideal exception handling. To do this, we establish a number of requirements:



  1. It should be a DTO, but we also do not refuse http codes, because for many errors, they are still well suited, can be used everywhere and in an old project that you have to support too, and they are simply universal. The standard DTO will include the IsError field (which allows writing universally error handling on the client), it should also contain the ErrorKey string error code, which the developer can immediately recognize only by looking at it and which provides more information. Additionally, you can add a link to a page with a description of this error, if necessary.
  2. This is all in the prod. In development mode, this DTO should return a stack trace, request data: cookies, headers, parameters. Looking ahead, the middleware described in the article even returns a link to the generated DeveloperExceptionPage, which allows you to watch the exception trace in a convenient form, but more on that later.
  3. The developer can bind together the exception, http error code and ErrorKey. This means that if he sends the code 403 from the controller, then if the developer attached a specific ErrorKey to it, the DTO will be returned with it. And vice versa, if a UnauthorizedAccessException occurs, it will be bound to the http code and ErrorKey.


This is the default format used in the library:



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





I must say right away that the type in which the data will be transmitted to the client can be set absolutely anyone, this is just one of the options.



IRO.Mvc.MvcExceptionHandler



Now I will show how I solved this problem for myself by writing the IRO.Mvc.MvcExceptionHandler library.



We connect an exception handler just like any other middleware - in the Startup class.



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





Inside the delegate being delegated, we need to configure our middleware. It is necessary to carry out mapping (binding) of exceptions to http codes and ErrorKey. Below is the easiest setup option.



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





As I promised to the most lazy hardcore developers who are not used to handling exceptions - nothing else needs to be done. This code will bind all exceptions in the ASP.NET pipeline to the common DTO with code 500, and the name of the exception will be written to ErrorKey.



It is worthwhile to understand that the RegisterAllAssignable method not only registers an exception of the specified type, but also all its descendants. If you want to send only information on specific exceptions to the client, it is quite a reasonable decision to create your ClientException and map only it. At the same time, if you set one http code for ClientException, and set another one for its successor SpecialClientException, then the code SpecialClientException will be used for all its descendants, ignoring the ClientException setting. All of this is cached, so there will be no performance issues.



You can fine-tune and register your ErrorKey and http code for a specific exception:



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





In addition to mapping, it is worthwhile to configure middleweights. You can specify the json serialization settings, the address of your site, a link to the error description page, the mode of operation of the middleware through IsDebug, the standard http code for unhandled exceptions.



  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;
      
      





The last property indicates whether it is possible to bind our DTO by http code.

You can also specify how to handle situations with internal exceptions, for example, TaskCanceledException with an internal logged error due to .Wait (). For example, here is a standard resolver that takes out internal exceptions from such exceptions and works with them already:



  s.InnerExceptionsResolver = InnerExceptionsResolvers.InspectAggregateException;
      
      





If you need to fine-tune serialization, you can set the FilterAfterDTO method. Return true to disable standard processing and serialize errorContext.ErrorDTO as you like. There is access to the HttpContext and the error itself.



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





DeveloperExceptionPage and other advantages of debug mode



We’ve figured out the settings, now let's figure out how to debug it all. In the DTO prod, the answer in the answer is simple and I already showed it above, now I will show how the same DTO looks in debug mode:







As you can see, there is a lot more information here, there is stackrace and request data. But it’s even more convenient to simply follow the link in the DebugUrl field and review the error data without straining:







It was rather difficult to implement this function, as DeveloperExceptionPage is simply not intended to be used by third-party developers. Initially, it was impossible to open the link in a browser with a different session, the content ceased to be displayed after a reboot. All this could be solved only by caching the html response of this middlever. Now you can at least pass the exception link to your teammate if you use a shared dedicated server.



Conclusion



I hope the developers who read this article have found an interesting and useful tool for themselves. For me, this article is partly a test of the usefulness of its developments and articles on them. I have some more ready-made more cool projects that I would like to tell the Habr community about.



All Articles