Monday, July 29, 2019

ASP.NET Core 3.0 Exception Handling

An aspect of the ASP.NET Core platform that I have loved since the first version is that the team attempted to engineer some of the common best practices of previous ASP.NET development right in the framework. This is particularly evident for exception handling.

When it comes to dealing with the things that could go wrong in your application, the big issue is not using try/catch blocks wherever it makes sense, but making sure you can effectively trap whatever exception that could be thrown but may go unnoticed and unhandled by the application. Since the early days of programming (remember Visual Basic’s OnError?), it’s always been a good practice to define a sort of a safety net around the application to intercept whatever exception bubbles up outside the control of the calling context.

In this article, I’ll explain the facts of exception handling as you would set it up in ASP.NET Core 3.0.

The ASP.NET Safety Net

Whether Web Forms or MVC, classic ASP.NET provides a global error handler function in the folds of the global.asax file—the Application_Error method. Any code you use to fill the method is invoked just before the system displays the nefarious yellow page of death or lets the web server deal with an error HTTP status code. In other words, the code you put in the body of the Application_Error method represents your last chance to fix things before the end.

What kind of code should you have, however, in a global error handler?

Intelligently, classic ASP.NET let us define error paths in the web.config file and uses those paths to re-route the user to a different page. The path you specify is ultimately a URL and, as such, can point to a static or even a dynamic endpoint. A static endpoint (e.g., an HTML page) can’t do much for the end-user but profoundly apologise for the (largely unknown) error. A dynamic endpoint (an ASPX file or a controller route) can, in theory, tailor an up-close-and-personal message but only if it can gain access to the specific situation that generated the error.

In years of ASP.NET programming, we have first learned how to retrieve the last occurred exception and redirect back to the most appropriate error page. This ultimately resulted in a flexible error router dispatching to a myriad of error pages. The sore point of global exception handling, in fact, is that you discover the error in one request but need to place another request to the system to have the error message rendered in a user-friendly and nice-looking error page. The Server.TransferRequest method was introduced for that purpose. The method works like a canonical redirect except that it takes place internally and no interaction ever takes place with the browser. In addition to that, in ASP.NET MVC you can even find a way to place an analogous internal URL request that the action invoker stack would process as if it were originated by an external request. As a result, you get an exception, and then some code of yours runs with full access to the details of the exception and the power to display some user interface at leisure.

In the end, building a safety net around the application was a problem solved in ASP.NET and ASP.NET MVC. In ASP.NET Core, the way you address it is different and passes through a new type of middleware.

The Exception Handling Middleware

In ASP.NET Core 3.0, you install the exception handling middleware and let it capture any unhandled exceptions and redirect to the most appropriate endpoint. As mentioned, the redirect isn’t physical—no HTTP 302 request is ever sent—but logical, and the browser is never involved with this.

There are two types of exception handling middleware: one is targeted to developers and is ideal for development scenarios, and one is suitable for staging and production environments and, as such, targeted to end-users.

In a development scenario, you might want to have the original error message displayed, and you even want the stack trace available and a snapshot of the HTTP context with details of the query string, session state, cookies and the like. This is just the kind of thing labelled as the “yellow page of death” in older versions of ASP.NET.

To have that, you append the following piece of middleware to the ASP.NET Core runtime pipeline. In startup.cs, you add the following:

app.UseDeveloperExceptionPage();

As you can see, the middleware is parameter-less and doesn’t allow routing to a custom page or view. The middleware, instead, prepares a system page on the fly that contains the details of the last intercepted exception and a snapshot of system status at the time of the exception. It’s a terminating middleware in the sense that it stops the request and represents the new “white page of death”—yes, they changed the background colour!

Hand in hand with this middleware, you can also use the UseExceptionHandler middleware that, instead, is ideal for production-ready exception handling.

The UseExceptionHandler Middleware

You add the UseExceptionHandler middleware to the pipeline using the Configure method of the startup class. Here’s an example:

public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        app.UseExceptionHandler("/system/error");
        ...
    }
}

The UseExceptionHandler extension method takes a URL and logically redirects the flow of the application to it. More in detail, the middleware places a new request for the specified URL right into the ASP.NET pipeline.

As the two middleware blocks address distinct scenarios—one for development and one for production—you might want to programmatically switch between middleware at runtime. You just use the services of the hosting environment API.

public void Configure(
      IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    } 
    else 
    {
        app.UseExceptionHandler("~/system/error");
    }
    ...
}

The methods of the IHostingEnvironment interface let you detect the current environment and intelligently decide which exception middleware to turn on. Based on the listing above, the Error method on the System controller is invoked to deal with any unhandled exceptions.

The next point to address is, what should you do when an exception is thrown and travels unhandled up to the safety net?

Handling the Exception

Looking at the code snippet above, the Error method is invoked without explicit parameters. In the past, the GetLastError method on the Server intrinsic object helped to retrieve the last exception occurred. No such object exists anymore in ASP.NET Core, but the new Features property on the HTTP context object allows to retrieve runtime information such as the last exception occurred and related information. Here’s the code you need to have in the global error handler.

public IActionResult Error()
{
    // Retrieve error information in case of internal errors
    var error = HttpContext
              .Features
              .Get<IExceptionHandlerFeature>();
    if (error == null)
         return View(model);

    // Use the information about the exception 
    var exception = error.Error;
    ...
}

You query the HTTP context of the current request for the latest exception and receive an object that implements the IExceptionHandlerFeature interface. The object exposes a single property named Error which points you right to the last tracked exception.

The interesting part is that once you hold in your hands a valid exception object, then you have all the available information about what could have gone wrong and can arrange the most appropriate response—whether some plain text or a full-blown, nice-looking error page. You can also decide to swallow most of the details contained in the exception object and route to a few of the predefined (and more generic) error message. In other words, you’re now in total control of what you display to your users.

You are so much in control that you could even throw an exception any time that a business operation goes wrong or can’t be performed for whatever reason. Instead of returning error codes all the way up, from the inner layers to the presentation, you throw an exception, and you’re done. To gain even more control, you can, for example, build your own hierarchy of exception objects, specific to the application. (I’ll return on this in a moment.)

Referencing the Request

The IExceptionHandlerFeature interface only returns information about the exception that was thrown at some point but nothing about the URL in the execution of which the exception threw. To get that, you need to request the IExceptionHandlerPathFeature interface to the HTTP context object, as shown below.

public IActionResult Error()
{
    // Retrieve error information in case of internal errors
    var path = HttpContext
              .Features
              .Get<IExceptionHandlerPathFeature>();
    if (path == null)
         return View(model);

    // Use the information about the exception 
    var exception = path.Error;
    var pathString = path.Path;
    ...
}

Note that the IExceptionHandlerPathFeature interface inherits from the aforementioned IExceptionHandlerFeature meaning that with a single request you get both the exception and path information.

What About 404 Errors?

The safety net captures any exceptions whether they originate in the processing of the request or the preparation of it, such as during model binding or in case of a missing route. With the approach shown earlier, however, you can’t get to know about the specific HTTP status code returned by the pipeline without an exception being thrown. The most notable example is a 404, but also any request that returns an error HTTP code such as 500.

To be sure your code can handle any HTTP status code of interest, you should add another piece of middleware to the pipeline.

app.UseStatusCodePages();

The status code middleware is another terminating middleware that directly returns some output to the end-user. In particular, it writes a plain text string to the output stream with the error code and the basic description. To handle the HTTP status code programmatically, you need another piece of middleware on top of the exception handler.

App.UseStatusCodePagesWithReExecute("/system/error/{0}");

The parameter {0} will be set with the HTTP status code if any. The re-execute in the name of the method just denotes that the request that generated a response with an error status code will be logically redirected (re-execute means no 302 to the browser) to the specified URL. Here’s how to rewrite the method to display the above error page to the user.

public IActionResult Error(
     [Bind(Prefix = "id")] int statusCode = 0)
{
    // Switch to the appropriate page
    switch(statusCode)
    {
        case 404:
           return Redirect(...);
        ...
    }


    // Retrieve error information in case of internal errors
    var path = HttpContext
              .Features
              .Get<IExceptionHandlerPathFeature>();
    if (path == null)
         return View(model);

    // Use the information about the exception 
    var exception = path.Error;
    var pathString = path.Path;
    ...
}

The HTTP status code is received through the model binding layer, and, if it matches one of those, the method can handle it and select the appropriate action. Note that the way in which the status code parameter is bound to the method is up to you. The approach shown above of using an id parameter is only an example.

Going One Step Further

How would you arrange the error page? Aside from some standard graphical template, what would you put in it? For sure, you want to render some message, whether the native message of the caught exception or some other more generic message. Anything else you would do?

Personally, I tend to have two levels of messages in any exception and a list of recovery links. The first level message is the native message of the exception (or any other programmatically selected message related to the error occurred). The second level message is any other descriptive message you might optionally want to display.

A recovery link, instead, is an application URL (or even an external URL) that you add to the error page to let users resume with the application from one or more safe places. Typically, one recovery link can be the home page. Recovery links should be bound to each exception type so that you specify them by the time you throw.

The idea is to set a base exception type on a per-application basis. For example, you can have a MyAppException base class as below:

public class MyAppException : Exception
{
   public MyAppException()
   {
       RecoveryLinks = new List<RecoveryLink>();
   }
   public MyAppException(string message) : base(message)
   {
       RecoveryLinks = new List<RecoveryLink>();
   }
   public MyAppException(Exception exception) : 
                      this(exception.Message)
   {
   }
   public List<RecoveryLink> RecoveryLinks { get; }
   public MyAppException AddRecoveryLink(
           string text, string url)
   {
        RecoveryLinks.Add(new RecoveryLink(text, url));
        return this;
   }
   public MyAppException AddRecoveryLink(RecoveryLink link)
   {
        RecoveryLinks.Add(link);
        return this;
   }
}

The RecoveryLink class is a custom class with a couple of properties—text and URL. Any other application-specific exception will inherit from MyAppException and will have available handy methods to deal with recovery links.

public IActionResult Throw()
{
    throw new MyAppException("A severe error occurred")
        .AddRecoveryLink("Google", "http://www.google.com");
}

In the exception handler, you just try to cast the exception object retrieved to MyAppException. If successful, you can then access the recovery links and use them in the user interface.

<ul>
@foreach(var link in Model.Error.RecoveryLinks)
{
    <li><a href="@link.Url">@link.Text</a></li>
}
</ul>

FIGURE 1-A sample error page using exception handlers and recovery links.

Summary

When it occurs, an exception breaks the normal flow of execution of a program and causes a re-routing. If you foresee that something could go wrong at some specific point, then the best option is probably using a try/catch block to wrap the risky code. However, sometimes, exceptions occur inadvertently and having a safety net around the application is quite useful. In this article, I’ve reviewed the ASP.NET Core 3.0 equipment for global error handling and suggested a couple of improvements for smoothing exception handling in real-world applications.

The post ASP.NET Core 3.0 Exception Handling appeared first on Simple Talk.



from Simple Talk https://ift.tt/2SMH9cD
via

No comments:

Post a Comment