Exception Handling In ASP.NET MVC Web API

Exception handling is required in any application. It is a very interesing issue where different apps have their own various way(s) to handle that. I plan to write a series of articles to discuss this issue for ASP.NET MVC (1), ASP.NET Web API (2), and ASP.NET MVC Core (3), ASP.NET Web API Core (4), respectively.
 
In this article, we will be discussing various ways of handling an exception in ASP.NET MVC Web API.
 

Introduction

 
For error handling, the major difference between ASP.NET MVC Web API from MVC or a Web application is that MVC or a Web app will return a web page to handle the error while Web API is to return a RESTfule output, JSON. In ASP.NET MVC Web API, we may have three ways to handle exceptions:
  • HttpResponseException, associated with HttpStatusCode, --- Locally in Try-catch-final
  • Exception filter, --- in the level of Action, Controller, and Globally
  • Exception Handler, --- Globally
We will set up a test sample app first, and then discuss the three above one by onve.
 
 

Set up the test sample app

 
Step 1 - Create an ASP.NET MVC Web API app
 
We use the current version of Visual Studio 2019 16.8 and .NET Framework 4.7.2 to build the app.
  • Start Visual Studio and select Create a new project.
  • In the Create a new project dialog, select ASP.NET Web Application (.NET Framework) > Next.
  • In the Configure your new project dialog, enter ErrorHandlingWebAPI for Project name > Create.
  • In the Create a new ASP.NET Web Application dialog, select Web API > Creat
Build and run the app, you will see the following image shows the app,
 
 
Step 2 - Add one empty apiController
 
Add one empty apiController into the app, with the name as ErrorHandlingWebAPIController:
  • Right click Controllers > add > controller.
  • In the Add New Scaffolded Item dialog, select Web API in the left pane, and
  • Web API 2 Controller - Empty > Add.
  • In the Add Controller dialog, Change ErrorHandlingWebAPIController for controller name > Add.
Step 3 - Add Swagger Client
 
To test Web API, we could use Postman or Fiddler, here we use Swagger that can test the Web API automatically, and actually is by default installed for the Web API Core. 
 
Installing Swagger from NuGet Package Manager, from Visual Studio project: 
  • Right click Tools > NuGet Package Manager > Manage NuGet for Solution
  • In the opened NuGet-Solution dialog: Click Browse, then Search Swagger
  • Install Swashbuckle v5.6.0
 
After installation, and App_Start folder, there will be a file, SwaggerConfig.cs, added with code:
  1. public class SwaggerConfig  
  2. {  
  3.     public static void Register()  
  4.     {  
  5.         var thisAssembly = typeof(SwaggerConfig).Assembly;  
  6.         GlobalConfiguration.Configuration  
  7.           .EnableSwagger(c => c.SingleApiVersion("v1""ErrorHandlingWebAPI"))  
  8.           .EnableSwaggerUi();   
  9.     }  

Step 4 - Change the default start up page to the swagger
 
Right click Project > Properties to open the properties page, then choose Web, in Start Url, add /swagger,
 
We will demonstrate the result from the next section, after adding a web api controller.
 
 

HttpResponseException

 
Add code into controller
 
We add the following code into the controller,
  1. [Route("CheckId/{id}")]  
  2. [HttpGet]     
  3. public IHttpActionResult CheckId(int id)  
  4. {         
  5.     if (id < 10) // No error hanbdling at all:  
  6.     {  
  7.         int a = 1;  
  8.         int b = 0;  
  9.         int c = 0;  
  10.         c = a / b; //it would cause exception.     
  11.     }  
  12.     else if (id <20) // Error handling by HttpResponseException with HttpStatusCode  
  13.     {  
  14.         throw new HttpResponseException(HttpStatusCode.BadRequest);  
  15.     }  
  16.     else if (id < 30) // Error handling by HttpResponseException with HttpResponseMessage  
  17.     {  
  18.         var response = new HttpResponseMessage(HttpStatusCode.BadRequest)  
  19.         {  
  20.             Content = new StringContent(string.Format("No Employee found with ID = {0}", 10)),  
  21.             ReasonPhrase = "Employee Not Found"  
  22.         };  
  23.   
  24.         throw new HttpResponseException(response);  
  25.     }  
  26.   
  27.     return Ok(id);  
  28. }  
Run the app, we have 
 

Discussion

 
Without Error Handling
 
What happens if a Web API controller throws an uncaught exception? By default, most exceptions are translated into an HTTP response with status code 500, Internal Server Error.
 
In our sample, // No error hanbdling at all case: we choos id = 1
 
 
The result in Swagger: only indicated that "An error has occurred." without detailed info,, and the response code is 500 --- Internal Server Error,
 
 
Error handled by HttpResponseException
 
The HttpResponseException type is a special case. This exception returns any HTTP status code that you specify in the exception constructor. For our sample, // Error handling by HttpResponseException with HttpStatusCode, the returns 404, Not Found, if the id parameter is not valid, say id = 11.
 
For more control over the response, you can also construct the entire response message and include it with the HttpResponseException,
 // Error handling by HttpResponseException with HttpResponseMessage, id = 21
 
 
Note
if you use Postman as a client, you can get one more piece of info: Employee Not Found associated with error code 400
 
 

Exception Filters

 
Exception Eilter
 
An exception filter is executed when a controller method throws any unhandled exception that is not an HttpResponseException exception. The HttpResponseException type is a special case, because it is designed specifically for returning an HTTP response.
 
Exception filters implement the System.Web.Http.Filters.IExceptionFilter interface. The simplest way to write an exception filter is to derive from the System.Web.Http.Filters.ExceptionFilterAttribute class and override the OnException method. 
  1. public class NotImplExceptionFilterAttribute : ExceptionFilterAttribute  
  2. {  
  3.     public override void OnException(HttpActionExecutedContext context)  
  4.     {  
  5.         if (context.Exception is NotImplementedException)  
  6.         {  
  7.             var resp = new HttpResponseMessage(HttpStatusCode.NotFound)  
  8.             {  
  9.                 Content = new StringContent("This method is not implemented"),  
  10.                 ReasonPhrase = "Not implemented"  
  11.             };  
  12.             throw new HttpResponseException(resp);  
  13.         }  
  14.     }  

Register Exception Eilter
 
Exception filter must be registered in order to be used. There are three ways to register a Web API exception filter,
  • By action
  • By controller
  • Globally
We add another apiController named as TestController with code,
  1. using System;  
  2. using System.Web.Http;  
  3.   
  4. namespace ErrorHandlingWebAPI.Controllers  
  5. {  
  6.     [NotImplExceptionFilter]
  7.     public class TestController : ApiController  
  8.     {  
  9.         [Route("Test/{id}")]  
  10.         [HttpGet]  
  11.         [NotImplExceptionFilter]  
  12.         public IHttpActionResult Test(int id)  
  13.         {  
  14.             throw new NotImplementedException("This method is not implemented");  
  15.         }  
  16.   
  17.         [Route("Test1/{id}")]  
  18.         [HttpGet]  
  19.           
  20.         public IHttpActionResult Test1(int id)  
  21.         {  
  22.             throw new NotImplementedException("This method is not implemented");  
  23.         }  
  24.     }  
  25. }  
Add attribute [NotImplExceptionFilter] in Test action level and in the controller level, and register in App_Start/WebApiConfig file,
  1. public static class WebApiConfig  
  2. {  
  3.     public static void Register(HttpConfiguration config)  
  4.     {  
  5.         // Web API configuration and services  
  6.         config.Filters.Add(new NotImplExceptionFilterAttribute());  
  7.   
  8.         // Web API routes  
  9.         config.MapHttpAttributeRoutes();  
  10.   
  11.         config.Routes.MapHttpRoute(  
  12.             name: "DefaultApi",  
  13.             routeTemplate: "api/{controller}/{id}",  
  14.             defaults: new { id = RouteParameter.Optional }  
  15.         );  
  16.     }  
  17. }  
We add one more option in the previous controller/action: ErrorHandlingWebAPIController/CheckID to test the global registered exception filter
  1. else if (id < 50)  
  2. {  
  3.     throw new NotImplementedException();  
  4. }  
 Then, run the app, and test the results,
 
 
 This is the for action level test result,
 
 
 

Exception Handler

 
Unhandled exceptions can be processed via exception filters, but there are a number of cases that exception filters can't handle. For example,
  1. Exceptions thrown from controller constructors.
  2. Exceptions thrown from message handlers.
  3. Exceptions thrown during routing.
  4. Exceptions thrown during response content serialization.
 Those could be handled by Exception Handler.
 
We add a global error handler into Global.asax file,
  1. public class GlobalExceptionHandler : ExceptionHandler  
  2. {  
  3.     public override void Handle(ExceptionHandlerContext context)  
  4.     {  
  5.         var response = new HttpResponseMessage(HttpStatusCode.BadRequest)  
  6.         {  
  7.             Content = new StringContent("This is handled by Global Exception Handler"),  
  8.             ReasonPhrase = "Exception Handler"  
  9.         };  
  10.   
  11.         context.Result = new ResponseMessageResult(response);  
  12.     }  
  13. }  
and register in App_Start/WebApiConfig file,
  1. public static void Register(HttpConfiguration config)  
  2. {  
  3.     // Web API configuration and services  
  4.     config.Filters.Add(new NotImplExceptionFilterAttribute());  
  5.     config.Services.Replace(typeof(IExceptionHandler), new GlobalExceptionHandler());  
  6.   
  7.     // Web API routes  
  8.     config.MapHttpAttributeRoutes();  
  9.   
  10.     config.Routes.MapHttpRoute(  
  11.         name: "DefaultApi",  
  12.         routeTemplate: "api/{controller}/{id}",  
  13.         defaults: new { id = RouteParameter.Optional }  
  14.     );  
  15. }  
Add one more test option in the testController
  1. using System;  
  2. using System.Web.Http;  
  3.   
  4. namespace ErrorHandlingWebAPI.Controllers  
  5. {  
  6.     [NotImplExceptionFilter]  
  7.     public class TestController : ApiController  
  8.     {  
  9.         [Route("Test/{id}")]  
  10.         [HttpGet]  
  11.         [NotImplExceptionFilter]  
  12.         public IHttpActionResult Test(int id)  
  13.         {  
  14.             throw new NotImplementedException("This method is not implemented");  
  15.         }  
  16.   
  17.         [Route("Test1/{id}")]  
  18.         [HttpGet]  
  19.           
  20.         public IHttpActionResult Test1(int id)  
  21.         {  
  22.             throw new NotImplementedException("This method is not implemented");  
  23.         }  
  24.   
  25.         [Route("Test2/{id}")]  
  26.         [HttpGet]  
  27.   
  28.         public IHttpActionResult Test2(int id)  
  29.         {  
  30.             throw new DivideByZeroException();  
  31.         }  
  32.     }  
  33. }  
Due to the exception DivideByZeroException() not handled by exception filter we made privously, it will be caught by the global error handler,
 
 

Summary

 
In ASP.NET MVC Web API, we usually have three ways to handle exceptions,
  • HttpResponseException, associated with HttpStatusCode, --- Locally in Try-catch-final
  • Exception filter, --- in the level of Action, Controller, and Globally
  • Exception Handler, --- Globally
 The order to exection is HttpResponseException ==> Exception filter ==> Exception Handler.
 
References