CHAPTER 25

image

Error Handling

In this chapter, I complete my description of the Web API dispatch process by showing how errors are handled. I’ll show you the different ways you can deal with problems that you anticipate during development and what happens when unexpected problems arise. I explain how to control the response that is sent to the client and how to manage and log unhandled exceptions for the entire application. Table 25-1 summarizes this chapter.

Table 25-1. Chapter Summary

Problem

Solution

Listing

Trigger the default error handling policy.

Throw an exception from an action method or filter.

1–3

Throw an exception that generates a specific result status code.

Throw an instance of HttpResponseException.

4

Return a response for error that has been anticipated.

Return an implementation of the IHttpActionResult interface.

5

Control the additional data that is sent to the client when an error occurs.

Use an HttpError object.

6–9

Control how information is sent to the client for an unhandled exception.

Set the HttpConfiguration.IncludeErrorDetailPolicy property.

10

Receive additional error data at the client.

Read the jqXHR.responseJSON property to get a JavaScript object decoded from the response and display the Message property, if it exists, to the user.

11

Change the default policy for dealing with unhandled exceptions.

Create a global exception handler.

12–14

Log unhandled exceptions.

Create a global exception logger.

15–16

Preparing the Example Project

I am going to continue working with the Dispatch project I created in Chapter 19. To prepare for this chapter, I have the removed filters from the Products controller, as shown in Listing 25-1.

Listing 25-1. The Contents of the ProductsController.cs File

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Web.Http;
using Dispatch.Infrastructure;
using Dispatch.Models;

namespace Dispatch.Controllers {

    public class ProductsController : ApiController {
        private static List<Product> products = new List<Product> {
                new Product {ProductID = 1, Name = "Kayak", Price = 275M },
                //new Product {ProductID = 2, Name = "Lifejacket", Price = 48.95M },
                //new Product {ProductID = 3, Name = "Soccer Ball", Price = 19.50M },
                //new Product {ProductID = 4, Name = "Thinking Cap", Price = 16M },
            };

        public IEnumerable<Product> Get() {
            return products;
        }

        [LogErrors]
        public Product Get(int id) {
            return products[id];
            //return products.Where(x => x.ProductID == id).FirstOrDefault();
        }

        public Product Post(Product product) {
            product.ProductID = products.Count + 1;
            products.Add(product);
            return product;
        }
    }
}

I have applied a LogErrors attribute to one of the Get action methods in the Product controller. This is the application of a simple exception filter that I defined by adding a LogErrorsAttribute.cs file to the Infrastructure folder and defining the class shown in Listing 25-2.

Listing 25-2. The Contents of the LogErrorsAttribute.cs File

using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Http.Filters;

namespace Dispatch.Infrastructure {
    public class LogErrorsAttribute : Attribute, IExceptionFilter {
        public Task ExecuteExceptionFilterAsync(HttpActionExecutedContext
            actionExecutedContext, CancellationToken cancellationToken) {

            Debug.WriteLine(string.Format(
                "Exception Type: {0}", actionExecutedContext.Exception.Message));
            Debug.WriteLine(string.Format(
                "Exception Message: {0}", actionExecutedContext.Exception.GetType()));

            return Task.FromResult<object>(null);
        }

        public bool AllowMultiple {
            get { return false; }
        }
    }
}

The filter writes out the message and type of exceptions that it is asked to process, which I will use to highlight differences in the way that some errors are processed. Finally, I have updated the WebApiConfig.cs file to comment out the custom classes that I added in earlier chapters. Listing 25-3 shows the revised configuration.

Listing 25-3. The Contents of the WebApiConfig.cs File

using System.Web.Http;
using System.Web.Http.Controllers;
using Dispatch.Infrastructure;

namespace Dispatch {
    public static class WebApiConfig {
        public static void Register(HttpConfiguration config) {

            config.Routes.MapHttpRoute(
                name: "ActionMethods",
                routeTemplate: "api/nrest/{controller}/{action}/{day}",
                defaults: new { day = RouteParameter.Optional }
            );

            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );

            //config.Services.Replace(typeof(IHttpActionSelector),
            //    new PipelineActionSelector());
            //config.Filters.Add(new SayHelloAttribute { Message = "Global Filter" });
            //config.MessageHandlers.Add(new AuthenticationDispatcher());
        }
    }
}

Dealing with Errors

All web services will run into problems, but what separates the good applications from the bad is the way that you deal with those problems and how you present them to the client and user.

In broad terms, there are two kinds of errors: the ones you anticipated during development and the ones that catch you by surprise in production. Careful coding and thorough testing can help minimize surprises, but you can’t foresee every issue. It is important to have a plan to deal with the problems you anticipate and have a fallback position for responding to the ones you don’t see coming. Web API provides features for dealing with both kinds of problem, as I explain in the sections that follow. Table 25-2 puts the Web API error handling support into context.

Table 25-2. Putting Error Handling in Context

Question

Answer

What is it?

Exception handling is the process of responding to problems and exceptions so that they are presented to clients via HTTP responses.

When should you use it?

You should handle as many problems as possible within action methods and filters and rely on the default behavior as little as you can.

What do you need to know?

You can change the default behavior by implementing a new global exception handler, as I describe in the “Responding to Errors Globally” section.

Relying on the Default Behavior

The simplest way to deal with problems is to ignore them and let the default behavior take care of generating a response for the client, which is to send a 500 (Internal Server Error) status code along with some diagnostic data.

To see the default behavior, start the application and use the browser to navigate to the /Home/Index URL. Click the Get One button to send the Ajax request that will invoke the Get action method. The value of the id parameter taken from the requested URL exceeds the number of data items available, which causes an exception to be thrown. The exception is expressed to the client as a 500 (Internal Server) response, as shown in Figure 25-1.

9781484200865_Fig25-01.jpg

Figure 25-1. The default Web API exception handling

Relying on the default behavior is the least useful thing you can do, and it should be relied on only for unforeseen problems. The issue is that the 500 (Internal Server Error) status code conveys no useful information to the client except that the request could not be processed.

When I see a 500 status code, I am reminded of a car that my mother used to drive. It was an old Peugeot and had a red STOP light on the dashboard that came on whenever the car detected a problem. The light would come on for everything from an interior lightbulb blowing to a serious engine fault, and there was no way to tell whether it was safe to continue and fix the problem tomorrow or whether you should pull to the side of the road and call a fire truck. The 500 status code is like the STOP light: it doesn’t convey any useful information beyond a problem having occurred. It does not explain what caused the problem, how severe the problem is, or how the problem might be resolved.

To deal with the lack of context, Web API includes additional data in the response body, like this:

{"Message":"An error has occurred.",
 "ExceptionMessage":"Index was out of range. Must be non-negative and less than the
      size of the collection. Parameter name: index",
 "ExceptionType":"System.ArgumentOutOfRangeException",
 "StackTrace":"   at System.ThrowHelper.ThrowArgumentOutOfRangeException() at
       System.Collections.Generic.List`1.get_Item(Int32 index)    at
       Dispatch.Controllers.ProductsController.Get(Int32 id) in ..."}

I explain how this part of the response is created and formatted in the “Using the HttpError Class” section, but for now, it is enough to know that the client is sent four pieces of information.

  • A message that describes the problem
  • The message from the exception
  • The .NET type of the exception
  • The stack trace (which I have edited for brevity)

This appears more useful than it is in reality. The information is vague (“an error has occurred” is no more informative than the response status code) and contains information that is only of use to the web service developer.

Finally, as you would expect from having read Chapter 24, the LogErrors exception filter that I applied at the start of the chapter is executed, which produces the following messages in the Visual Studio Output window:

Exception Type: Index was out of range. Must be non-negative and less than the size of the collection.
Parameter name: index
Exception Message: System.ArgumentOutOfRangeException

Throwing a Special Exception

The default behavior is applied when an action method throws an exception (or fails to catch an exception thrown by code it calls), but there is one type of exception that does not trigger the default behavior: HttpResponseException. The constructor for the HttpResponseException class takes an HttpStatusCode parameter, which is used as the status code for the HTTP response. Listing 25-4 shows how I applied the HttpResponseException to the Get action of the Products controller.

Listing 25-4. Applying the HttpResponseException in the ProductsController.cs File

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Web.Http;
using Dispatch.Infrastructure;
using Dispatch.Models;

namespace Dispatch.Controllers {

    public class ProductsController : ApiController {
        private static List<Product> products = new List<Product> {
                new Product {ProductID = 1, Name = "Kayak", Price = 275M },
                //new Product {ProductID = 2, Name = "Lifejacket", Price = 48.95M },
                new Product {ProductID = 3, Name = "Soccer Ball", Price = 19.50M },
                new Product {ProductID = 4, Name = "Thinking Cap", Price = 16M },
            };

        public IEnumerable<Product> Get() {
            return products;
        }

        [LogErrors]
        public Product Get(int id) {
            Product product = products.Where(x => x.ProductID == id).FirstOrDefault();
            if (product == null) {
                throw new HttpResponseException(HttpStatusCode.BadRequest);
            }
            return product;
        }

        public Product Post(Product product) {
            product.ProductID = products.Count + 1;
            products.Add(product);
            return product;
        }
    }
}

I have taken a more nuanced approach to the implementation of the action method, using LINQ to try to locate a Product object with the ID specified by the client. If there is no match, then I throw a new HttpResponseException with the 400 (Bad Request) status code.

The way that HttpResponseException is handled by Web API is different from all other exceptions. Most importantly, exception filters are not executed. This is because the ApiControllerActionInvoker class (which is the default implementation of the IHttpActionInvoker interface, as I explained in Chapter 22) explicitly catches instances of HttpResponseException and processes them to create an HttpResponseMessage object before the normal error handling is executed.

Image Note  When you use the HttpResponseException, no context information—such as the stack trace—is included in the response.

Using an Implementation of the IHttpActionResult Interface

For problems you are expecting—especially those caused by a problem with the request—you can follow the standard approach available through the ApiController base class and return an object that implements the IHttpActionResult interface. Listing 25-5 shows how I reworked the Get action method in the Products controller to return this kind of result if the requested data object doesn’t exist.

Listing 25-5. Returning an IHttpActionResult in the ProductsController.cs File

...
[LogErrors]
public IHttpActionResult Get(int id) {
    Product product = products.Where(x => x.ProductID == id).FirstOrDefault();
    if (product == null) {
        return BadRequest("No such data object");
    }
    return Ok(product);
}
...

I find this approach feels less natural than using an HttpResponseException because the result from the action method has to be IHttpActionResult in order to allow successful and error results to be produced, although this is mitigated a little by the convenience methods that the ApiController class provides for creating results, such as Ok and BadRequest. When you use convenience methods, such as BadRequest, the string argument is included in the body of the response sent to the client, like this:

{"Message":"No such data object"}

To see this data, start the application, use the browser to navigate to the /Home/Index URL, and click the Get One button. Using the F12 tools, you will be able to see that the response contains a JSON object with a Message property.

Using the HttpError Class

The content in the body of an error HTTP response is controlled by the HttpError class, which has been created and populated behind the scenes in the previous examples. You can get more direct control over the way in which errors are expressed to the client by creating an HttpResponseMessage object within the action method and providing an HttpError object with the information you want included in the response body.

Image Tip  The HttpError object is subject to serialization through media type formatters, which I described in Part 2. This means the format will adapt to the client’s preferences, which is why the HttpError objects I create in this chapter are all expressed as JSON. See Part 2 for how objects are serialized and how the client specifies which formats it prefers to deal with.

I am showing you how to use the HttpError object to send data to the client for completeness, but the data itself is of limited use. HTTP web service clients are required to deal only with the response status code, and there is no standard for the body data and how it should be used. You should take care to ensure that the response status code accurately reflects the nature of the problem and not rely on the client being able to parse and respond to any additional information you have chosen to include.

Additional data can be helpful during development when you are responsible for creating the web service and the client, but I find it more helpful to use the Visual Studio debugger and the browser F12 tools to figure out what is happening when things go wrong. Table 25-3 puts the HttpError object into context.

Table 25-3. Putting the HttpError Class in Context

Question

Answer

What is it?

The HttpError class is used to send additional data to the client when something goes wrong.

When should you use it?

The HttpError class is used automatically when Web API handles uncaught exceptions, but you can also create instances directly for use with HttpResponseMessage objects.

What do you need to know?

There is no agreed standard on how web services send error data to the client, and the default data sent by Web API is generally of use only to developers.

Using an Error Response and an HttpError Object

The HttpError class, which is defined in the System.Web.Http namespace, defines the properties shown in Table 25-4.

Table 25-4. The Properties Defined by the HttpError Class

Name

Description

ExceptionMessage

Gets or sets a descriptive string, usually used to hold the message from the exception that the HttpError represents.

ExceptionType

Gets or sets the type of the exception that the HttpError represents, expressed as a string.

InnerException

Gets or sets an HttpError that represents a nested error.

Message

Gets or sets the user-readable message that describes the problem the HttpError object represents, expressed as a string.

MessageDetail

Gets or sets a message intended for the client developer that describes the error the HttpError represents, expressed as a string.

ModelState

Gets an HttpError that contains details of model validation errors. To set this property, create a new instance of the HttpError class using the constructor that accepts a ModelStateDictionary object. See the “Including Model State Errors in the HTTP Response” section for a demonstration.

StackTrace

Gets the stack trace for the error that the HttpError object represents, expressed as a string.

To take control of the data includes in the response body, you create an instance of HttpError, set the properties you want to include, and then create an HttpResponseMessage that will convey the data back through the dispatch chain and to the client. Listing 25-6 shows the use of the HttpError object in the Get action method of the Products controller.

Listing 25-6. Creating an Error Response in the ProductsController.cs File

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Web.Http;
using Dispatch.Infrastructure;
using Dispatch.Models;
using System.Net.Http;

namespace Dispatch.Controllers {

    public class ProductsController : ApiController {
        private static List<Product> products = new List<Product> {
                new Product {ProductID = 1, Name = "Kayak", Price = 275M },
                //new Product {ProductID = 2, Name = "Lifejacket", Price = 48.95M },
                new Product {ProductID = 3, Name = "Soccer Ball", Price = 19.50M },
                new Product {ProductID = 4, Name = "Thinking Cap", Price = 16M },
            };

        public IEnumerable<Product> Get() {
            return products;
        }

        [LogErrors]
        public HttpResponseMessage Get(int id) {
            Product product = products.Where(x => x.ProductID == id).FirstOrDefault();
            if (product == null) {
                return Request.CreateErrorResponse(HttpStatusCode.BadRequest,
                    new HttpError {
                        Message = "No such data item",
                        MessageDetail = string.Format("No item ID {0} was found", id)
                });
            }
            return Request.CreateResponse(product);
        }

        public Product Post(Product product) {
            product.ProductID = products.Count + 1;
            products.Add(product);
            return product;
        }
    }
}

In the listing, I use the CreateErrorResponse extension method on the HttpRequestMessage object to create an HttpResponseMessage. The version of the CreateErrorResponse method that I used takes an HTTP status code and an HttpError object, for which I set the Message and MessageDetail properties.

You can see how the property values I set in Listing 25-6 are processed by invoking the action method. Start the application, use the browser to navigate to /Home/Index, and click the Get One button. The URL that the client requests will trigger the creation of the error response, and you will see the following data in the response in the browser F12 tools:

{"Message":"No such data item","MessageDetail":"No item ID 2 was found"}

Adding Extra Information to the HttpError Object

Although the HttpError class defines the set of properties shown in Table 25-4, the class itself is derived from Dictionary<string, object>, which means you can add arbitrary data to the response sent to the client. Listing 25-7 shows how I have modified the Get method in the Product controller to send additional information through the HttpError object.

Listing 25-7. Adding Extra Error Information in the ProductsController.cs File

...
[LogErrors]
public HttpResponseMessage Get(int id) {
    Product product = products.Where(x => x.ProductID == id).FirstOrDefault();
    if (product == null) {
        HttpError error = new HttpError();
        error.Message = "No such data item";
        error.Add("RequestID", id);
        error.Add("AvailbleIDs", products.Select(x => x.ProductID));
        return Request.CreateErrorResponse(HttpStatusCode.BadRequest, error);
    }
    return Request.CreateResponse(product);
}
...

In this example, I set the Message property described in Table 25-4 and add two custom properties to provide additional information about the error. I include the requested product ID that was received by the action method (which can be useful to check to see whether there have been parameter/model binding errors as the request was processed) and return a list of the IDs of the data objects that are available. Here is the data included in the HTTP response:

{"Message":"No such data item","RequestID":2,"AvailbleIDs":[1,3,4]}

I don’t recommend including lists of valid IDs in real projects because there can be a lot of them to deal with, but it provides a nice demonstration in this chapter of how you can pass arbitrary objects to the HttpError and leave them to be serialized as part of the response (in this case, an IEnumerable<int> that is expressed as an array of numeric values).

Including Model State Errors in the HTTP Response

In Chapter 18, I explained how data validation errors are expressed through model state. You can include model state data in an HttpError object, as shown by Listing 25-8.

Listing 25-8. Adding Model State Data to the Error in the ProductsControllers.cs File

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Web.Http;
using Dispatch.Infrastructure;
using Dispatch.Models;
using System.Net.Http;

namespace Dispatch.Controllers {

    public class ProductsController : ApiController {
        private static List<Product> products = new List<Product> {
                new Product {ProductID = 1, Name = "Kayak", Price = 275M },
                //new Product {ProductID = 2, Name = "Lifejacket", Price = 48.95M },
                new Product {ProductID = 3, Name = "Soccer Ball", Price = 19.50M },
                new Product {ProductID = 4, Name = "Thinking Cap", Price = 16M },
            };

        public IEnumerable<Product> Get() {
            return products;
        }

        [LogErrors]
        public HttpResponseMessage Get(int id) {
            Product product = products.Where(x => x.ProductID == id).FirstOrDefault();
            if (product == null) {
                HttpError error = new HttpError();
                error.Message = "No such data item";
                error.Add("RequestID", id);
                error.Add("AvailbleIDs", products.Select(x => x.ProductID));
                return Request.CreateErrorResponse(HttpStatusCode.BadRequest, error);
            }
            return Request.CreateResponse(product);
        }

        public HttpResponseMessage Post(Product product) {
            if (!ModelState.IsValid) {
                HttpError error = new HttpError(ModelState, false);
                error.Message = "Cannot Add Product";
                error.Add("AvailbleIDs", products.Select(x => x.ProductID));
                return Request.CreateErrorResponse(HttpStatusCode.BadRequest, error);
            }
            product.ProductID = products.Count + 1;
            products.Add(product);
            return Request.CreateResponse(product);
        }
    }
}

The HttpError class defines a constructor that accepts a ModelStateDictionary object, which can be obtained through the ApiController.ModelState property. The second constructor argument specifies whether details of the validation exceptions should be included in the HttpError object.

Image Tip  Be careful not to include model state data unless there is a validation error; otherwise, you will send data to the client that simply confirms that the model state was valid. The purpose of error data is to explain what went wrong, not what worked as expected.

To test the changes I made to the Post action method, I added some validation attributes to the Product class, as shown in Listing 25-9 and which I described in Chapter 18.

Listing 25-9. Applying Validation Attributes in the Product.cs File

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Web.Http;

namespace Dispatch.Models {

    public class Product {

        [HttpBindNever]
        public int ProductID { get; set; }

        [Required]
        public string Name { get; set; }

        [Range(20, 500)]
        public decimal Price { get; set; }
    }
}

To test the effect of adding validation errors to the HTTP response, start the application, use the browser to navigate to the /Home/Index URL, and click the Post button. The default Price value that the client sends is less than the lower bound I applied with the Range validation attribute, which ensures that the model state will be invalid.

Using the F12 tools to see the response sent from the web service will reveal the data from the HttpError object, as follows:

{"Message":"Cannot Add Product",
 "ModelState":{"product.Price":["The field Price must be between 20 and 500."]},
 "AvailbleIDs":[1,3,4]}

The properties that I set directly in the action method are sent alongside the validation errors that were detected.

Controlling Error Detail

Every predefined HttpError property except Message is considered to be detailed information and, as I explained at the start of this section, is generally useful only to developers. You can control whether properties other than Message are sent to the client through the HttpConfigutation.IncludeErrorDetailPolicy property, which is set to a value from the IncludeErrorDetailPolicy enumeration, as listed in Table 25-5.

Table 25-5. The Values Defined by the IncludeErrorDetailPolicy Enumeration

Name

Description

Always

All of the HttpError property values are sent to the client.

Default

Use the behavior defined by the customErrors configuration element in the Web.config file. Use this value only if your application is hosted by ASP.NET, and use the LocalOnly value for other hosts.

LocalOnly

All of the HttpError properties are sent to clients on the local machine, but only the Message property is sent to other clients.

Never

Only the Message property is sent, irrespective of where the client request originated or the host configuration.

Listing 25-10 shows how I set the detail policy in the WebApiConfig.cs file.

Listing 25-10. Setting the Exception Detail Policy in the WebApiConfig.cs File

using System.Web.Http;
using System.Web.Http.Controllers;
using Dispatch.Infrastructure;

namespace Dispatch {
    public static class WebApiConfig {
        public static void Register(HttpConfiguration config) {

            config.Routes.MapHttpRoute(
                name: "ActionMethods",
                routeTemplate: "api/nrest/{controller}/{action}/{day}",
                defaults: new { day = RouteParameter.Optional }
            );

            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );

            //config.Services.Replace(typeof(IHttpActionSelector),
            //    new PipelineActionSelector());
            //config.Filters.Add(new SayHelloAttribute { Message = "Global Filter" });
            //config.MessageHandlers.Add(new AuthenticationDispatcher());

            config.IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.Never;
        }
    }
}

Image Caution  The IncludeErrorDetailPolicy setting affects the HttpError objects that Web API creates only when dealing with a regular unhandled exception. It doesn’t have any effect on HttpError objects that you create directly, where you can control the data sent to the client explicitly.

Displaying HttpError Information in the Client

If you are responsible for writing the client that consumes the Web API web service, then you can take advantage of the HttpError information to increase the user’s understanding of what caused a problem, beyond the basic characterization provided by the HTTP status code. Listing 25-11 demonstrates how to read the data from an error returned in response to a jQuery Ajax request.

Image Caution  You should display only the Message property to users and ensure that the messages you send to the client are meaningful and helpful to a typical end user. Keep technical details and information about the structure of your application to a minimum and limited to the HttpError properties intended for developers.

Listing 25-11. Processing Error Information in the dispatch.js File

...
error: function (jqXHR) {
    gotError(true);
    products.removeAll();
    if (jqXHR.responseJSON && jqXHR.responseJSON.Message) {
        response(jqXHR.status + " (" + jqXHR.responseJSON.Message + ") ");
    } else {
        response(jqXHR.status);
    }
}
...

jQuery makes the response body as a JavaScript object parsed from the JSON data, available through the argument passed to the error function. In this listing, I have replaced the status message text with the value of the responseJSON.Message property, which corresponds to the HttpError.Message property I set in Listing 25-8. Figure 25-2 shows the effect.

9781484200865_Fig25-02.jpg

Figure 25-2. Displaying additional error data

Responding to Errors Globally

All the errors I have dealt with so far in this chapter have been generated by action methods, which are the source of most problems. However, you have seen just how many different ways there are to customize the way that Web API dispatches requests, and all of them have the potential to throw exceptions.

Web API defines two services that can be used to deal with exceptions wherever they occur in the application: the global exception handler and the global exception logger. I explain both in the sections that follow, and Table 25-6 puts them into context.

Table 25-6. Putting the Global Error Services in Context

Question

Answer

What are they?

The global error services allow you to change the default behavior for uncaught exceptions and to log those exceptions.

When should you use them?

Use the global exception handler when you want to change the response sent for exceptions. Use the global exception logger to record exceptions for future analysis.

What do you need to know?

Change the default fallback behavior with caution because sending a 500 (Internal Server Error) is usually the best approach for dealing with unforeseen problems.

Handling Exceptions

A global exception handler implements the IExceptionHandler interface, which is defined as follows:

using System.Threading;
using System.Threading.Tasks;

namespace System.Web.Http.ExceptionHandling {

    public interface IExceptionHandler {

        Task HandleAsync(ExceptionHandlerContext context,
            CancellationToken cancellationToken);
    }
}

The HandleAsync method is called when an exception is thrown and not handled elsewhere in the application. The HandleAsync method accepts an instance of the ExceptionHandlerContext class, which defines the properties described in Table 25-7.

Table 25-7. The Properties Defined by the ExceptionHandlerContext Class

Name

Description

CatchBlock

This property returns an ExceptionContextCatchBlock object, which describes where the exception originated. See Table 25-8 for details.

Exception

This property returns the Exception that has been thrown.

ExceptionContext

This property returns an ExceptionContext object, which provides access to the same objects as the ExceptionHandlerContext as well as the HttpActionContext and HttpControllerContext objects associated with the current request.

Request

This property returns the HttpRequestMessage object that represents the request being dispatched.

RequestContext

This property returns the HttpRequestContext object associated with the request being dispatched.

Result

This property is set by the exception handler to handle the exception and specify the IHttpActionResult that will be used to generate the response to the client.

A custom global exception handler can change the default Web API behavior by setting the ExceptionHandlerContext.Result property to an IHttpActionResult object, which is processed to create an HttpResponseMessage that can used to send a response to the client. If a custom global exception handler doesn’t set the Result property, then Web API uses a fallback exception handler, which generates the standard 500 (Internal Server Error) response.

The CatchBlock property of the ExceptionHandlerContext class provides information about where the exception originated, expressed as one of the values in Table 25-8. There are additional values that are host-specific and that are used when an exception is encountered when sending a response to the client. I use the CatchBlock property in the “Creating a Custom Global Exception Logger” section later in this chapter.

Table 25-8. The Values Used for the CatchBlock Property

Name

Description

HttpServer

The exception originated from the SendAsync method of the HttpServer class.

HttpControllerDispatcher

The exception originated from the SendAsync method of the HttpControllerDispatcher class.

IExceptionFilter

The exception originated from the ExecuteAsync method of the controller.

Creating a Custom Global Exception Handler

Before you create a custom global exception handler, I recommend you take a moment and consider the problem you are trying to solve. Bear in mind that global exception handlers are used only for exceptions that are not handled elsewhere in the application and that the default behavior of sending a 500 (Internal Server Error) response is usually appropriate. After all, if there is a more appropriate response, then you should add code to your action methods that anticipate the problem and return the appropriate response to the client.

Global exception handlers are best used to make sweeping changes to all or most unhandled exceptions, and if you find yourself writing endless conditional statements to deal with specific exception and request types, then you should consider a different technique that pushes the logic closer to the action method, where it is easier to understand, test, and maintain.

To demonstrate the use of a global exception handler, I added a class file called CustomExceptionHandler.cs to the Infrastructure folder and used it to define the class shown in Listing 25-12.

Listing 25-12. The Contents of the CustomExceptionHandler.cs File

using System.Net;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Http.ExceptionHandling;
using System.Web.Http.Results;

namespace Dispatch.Infrastructure {
    public class CustomExceptionHandler : IExceptionHandler {

        public Task HandleAsync(ExceptionHandlerContext context,
                CancellationToken cancellationToken) {

            context.Result = new StatusCodeResult(HttpStatusCode.InternalServerError,
                context.Request);
            return Task.FromResult<object>(null);
        }
    }
}

You can set the ExceptionHandlerContext.Result property to any implementation of the IHttpActionResult interface. I listed the built-in implementation classes from the System.Web.Http.Results namespace in Chapter 11, but you must instantiate them directly since the convenience methods I used in that chapter are implemented by the ApiController class and are not available to global exception handlers. In the listing, I use the ResponseMessageResult class, which lets me create the HttpResponseMessage within the scope of an IHttpActionResult.

The custom exception handler generates a standard 500 (Internal Server Error) response by creating an instance of the StatusCodeResult but doesn’t include any of the additional data that comes from an HttpError object.

Registering and Testing a Custom Global Exception Handler

Global exception handlers are registered through the services collection in the WebApiConfig.cs file, as shown in Listing 25-13.

Listing 25-13. Registering a Global Exception Handler in the WebApiConfig.cs File

using System.Web.Http;
using System.Web.Http.Controllers;
using Dispatch.Infrastructure;
using System.Web.Http.ExceptionHandling;

namespace Dispatch {
    public static class WebApiConfig {
        public static void Register(HttpConfiguration config) {

            config.Routes.MapHttpRoute(
                name: "ActionMethods",
                routeTemplate: "api/nrest/{controller}/{action}/{day}",
                defaults: new { day = RouteParameter.Optional }
            );

            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );

            //config.Services.Replace(typeof(IHttpActionSelector),
            //    new PipelineActionSelector());
            //config.Filters.Add(new SayHelloAttribute { Message = "Global Filter" });
            //config.MessageHandlers.Add(new AuthenticationDispatcher());

            config.IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.Never;

            config.Services.Replace(typeof(IExceptionHandler),
                new CustomExceptionHandler());
        }
    }
}

There can be only one global exception handler in an application, but the built-in handler is still used as a fallback so that it can apply the default behavior if the custom handler doesn’t set the Result property of the ExceptionHandlerContext object.

Before I can test the custom handler, I need to create a reliable source of exceptions. Listing 25-14 shows how I have changed the Get action method in the Products controller to remove the error handling code and throw an exception when requests are received for data objects that don’t exist.

Listing 25-14. Throwing Exceptions in the ProductsController.cs File

...
[LogErrors]
public Product Get(int id) {
    Product product = products.Where(x => x.ProductID == id).FirstOrDefault();
    if (product == null) {
        throw new ArgumentOutOfRangeException("id");
    }
    return product;
}
...

To test the handler, start the application, use the browse to navigate to /Home/Index, and click the Get One button. The changes that I made to the Get method mean that the URL that is requested will cause the action method to throw an exception, which will be passed to the custom global exception handler. You can see the result in Figure 25-3, but the effect is subtle because the only difference from the previous examples is the omission of the error message text.

9781484200865_Fig25-03.jpg

Figure 25-3. Using a custom global exception handler

Logging Exceptions

The global exception logger allows you to record the exceptions that your application encounters. This isn’t much help at runtime, but it can be useful when diagnosing recurring problems and planning maintenance and enhancements for future releases. Global exceptions loggers implement the IExceptionLogger interface in the System.Web.Http.ExceptionHandling namespace, which is defined as follows:

using System.Threading;
using System.Threading.Tasks;

namespace System.Web.Http.ExceptionHandling {

    public interface IExceptionLogger {

        Task LogAsync(ExceptionLoggerContext context,
            CancellationToken cancellationToken);
    }
}

The LogAsync method is called when there is an unhandled exception and receives an instance of the ExceptionLoggerContext class as a parameter, which defines the properties shown in Table 25-9.

Table 25-9. The Properties Defined by the ExceptionLoggerContext Class

Name

Description

CallsHandler

This property returns true if the exception can be handled by the IExceptionHandler to produce a response message. Some exceptions can occur after the response has started to be sent to the client, in which case this property will return false.

CatchBlock

This property returns an ExceptionContextCatchBlock object, which describes where the exception was caught.

Exception

This property returns the Exception to be logged.

ExceptionContext

This property returns an ExceptionContext object, which provides access to the same objects as the ExceptionHandlerContext as well as the HttpActionContext and HttpControllerContext objects associated with the current request.

Request

This property returns the HttpRequestMessage object for the request being processed when the exception occurred.

RequestContext

This property returns the HttpRequestContext object associated with the request.

Creating a Custom Global Exception Logger

Unlike the global exception handler, there can be multiple global exception handlers, and each can choose which exceptions it records and how they are recorded. To demonstrate a simple exception logger, I added a class file called CustomExceptionLogger.cs to the Infrastructure folder and used it to define the class shown in Listing 25-15.

Listing 25-15. The Contents of the CustomExceptionLogger.cs File

using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Http.ExceptionHandling;

namespace Dispatch.Infrastructure {

    public class CustomExceptionLogger : IExceptionLogger {

        public Task LogAsync(ExceptionLoggerContext context,
            CancellationToken cancellationToken) {

            Debug.WriteLine("Log Exception Type: {0}, Originated: {1}, URL: {2}",
                context.Exception.GetType(),
                context.CatchBlock,
                context.Request.RequestUri);

            return Task.FromResult<object>(null);
        }
    }
}

The custom logger writes details of the exception, where it originated, and the requested URL to the Visual Studio Output window.

Image Tip  Writing exceptions to the Visual Studio Output window isn’t helpful for a production environment. If you do not have an existing logging system with which to integrate, then a good place to start is the open source package ELMAH. See https://code.google.com/p/elmah/ for details.

Registering and Testing a Custom Exception Logger

Custom exception loggers are registered with the services collection, as shown in Listing 25-16.

Listing 25-16. Registering a Custom Global Exception Logger in the WebApiConfig.cs File

using System.Web.Http;
using System.Web.Http.Controllers;
using Dispatch.Infrastructure;
using System.Web.Http.ExceptionHandling;

namespace Dispatch {
    public static class WebApiConfig {
        public static void Register(HttpConfiguration config) {

            config.Routes.MapHttpRoute(
                name: "ActionMethods",
                routeTemplate: "api/nrest/{controller}/{action}/{day}",
                defaults: new { day = RouteParameter.Optional }
            );

            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );

            //config.Services.Replace(typeof(IHttpActionSelector),
            //    new PipelineActionSelector());
            //config.Filters.Add(new SayHelloAttribute { Message = "Global Filter" });
            //config.MessageHandlers.Add(new AuthenticationDispatcher());

            config.IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.Never;

            config.Services.Replace(typeof(IExceptionHandler),
                new CustomExceptionHandler());
            config.Services.Add(typeof(IExceptionLogger), new CustomExceptionLogger());
        }
    }
}

Notice that I used the Add method to register the logger, rather than the Replace method I used for the global handler. This is because there can be multiple loggers in an application, but only one handler and the service collection class will throw an exception if you try to Add a single-instance type.

To test the exception logger, start the application and use the browser to navigate to the /Home/Index URL. Click the Get One button, and the client will request a URL that will cause an exception to be thrown in the Get action method of the Products controller. The exception is unhandled and will be passed to the exception logger, producing the following message in the Visual Studio Output window:

Log Exception Type: System.ArgumentOutOfRangeException, Originated: IExceptionFilter, 
    URL: http://localhost:49412/api/products/2

Summary

In this chapter, I explained how errors are handled by Web API. I showed you the default exception handling behavior and how to change it, how to use the HttpError class to control the additional data sent to a client, and, finally, how to log the unhandled exceptions that your application encounters.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.221.136.142