Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / ASP.NET

Exception Handling in WebAPI

4.88/5 (13 votes)
26 Feb 2014CPOL4 min read 106.2K   1.8K  
3 different ways by which Exception handling can be done in ASP.NET WebApi

Introduction

In this article, I will try and explain 3 different ways by which you can implement exception handling in ASP.NET WebApi. I assume the developer will have basic knowledge about how WebApi functions. I will be using Visual Studio 2013 IDE for development. WebApi will be hosted in a self-hosting environment. For hosting WebApi service, I will be using Console Application in Admin mode. Admin mode will be required since hosting mechanism will need to gain access to port which you have configured. For demo, we will be using JSON output.

Background

In the project which I am working on, the service needs to implement exception handling in WebApi. Hence, I decided to evaluate what are all the mechanisms available inside WebApi for implementing exception handling. After spending a day, I was able to find three different ways by which we can implement exception handling. They are:

  • Exception Handling inside a method
  • Exception Handling using Attribute
  • Global exception handling using IHttpActionInvoker

Using the Code

Let’s first setup the required development environment for developing a sample with all possible scenarios. Open Visual Studio in administrator mode & create a sample application using console template. To add reference to required assemblies, you can simply install Microsoft.AspNet.WebApi.SelfHost via Nuget package installer.

Once you have successfully installed WebApi.SelfHost Nuget, add the following namespaces which are required for the sample application.

C#
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Filters; 

At different layers such as Business layer, Dataaccess layer, I prefer creating custom exception class. This class does help us in understanding the type & layer of error. This will also help us in sending customized error message if we are raising business validation exception. Let’s create two sample exception classes (Business & DataAccess) which are derived from CustomException.

C#
public class CustomException : Exception
   {
       public virtual string ErrorCode { get; set; }
       public virtual string ErrorDescription { get; set; }
   }
C#
public class BusinessLayerException : CustomException
    {
       
        public BusinessLayerException(string errorCode, string errorDescription)
            : base()
        {
            base.ErrorCode = errorCode;
            base.ErrorDescription = errorDescription;
        }
    } 
C#
public class DataLayerException : CustomException
    {
        public DataLayerException(string errorCode, string errorDescription)
            : base()
        {
            base.ErrorCode = errorCode;
            base.ErrorDescription = errorDescription;
        }
    } 

Custom Exception class has two properties, ErrorCode & ErrorDescription which we will be using while raising custom exception.

Let’s add a new controller class which will expose our web methods to external process. For demo purposes, we will have three methods:

  • GetErrorMethod will return customized error response from method directly.
  • GetHandlerException will raise a business exception but here exception will be processed by different mechanism.
  • The third method GetGlobalErrorMessage is been added for creating internal server error.

C#
public class TestController : System.Web.Http.ApiController
{
    public HttpResponseMessage GetMethodError()
    {
        try
        {
            throw new Exception("Get Custom error message from Method");
        }
        catch (Exception Ex)
        {
            var errorMessagError
                = new System.Web.Http.HttpError(Ex.Message) { { "ErrorCode", 405 } };

            throw new
                HttpResponseException(ControllerContext.Request.CreateErrorResponse
                (HttpStatusCode.MethodNotAllowed, errorMessagError));
        }
    }

    [MethodAttributeExceptionHandling]
    public HttpResponseMessage GetHandlerException()
    {
        throw new
            BusinessLayerException("4001",
            "Your account is in negative. please recharge");
    }

    public HttpResponseMessage GetGlobalErrorMessage()
    {
        int i = 0;
        var val = 10 / i;
        return new
            HttpResponseMessage(HttpStatusCode.OK);
    }
}

GetMethodError implements try catch block internally. This method handles any error which is generated internally. It returns HttpResponseException in case any error is being generated. It is also recommended practice to use HttpError while returning exception.

C#
catch (Exception Ex)
           {
               var errorMessagError
                   = new System.Web.Http.HttpError(Ex.Message) { { "ErrorCode", 405 } };

               throw new
                   HttpResponseException
                   (ControllerContext.Request.CreateErrorResponse(HttpStatusCode.MethodNotAllowed, errorMessagError));
           }

Now we need to host our service so that other processes can access this method. Hosting is again divided into two parts, Configuration & hosting mechanism. For configuration, I have created a separate static class so that all the configuration logic is part of same code block.

C#
public static class WebApiConfig
{
 public static void Register(System.Web.Http.HttpConfiguration config)
  {
     //Route Configuration
     config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{action}"
     );
            
     //Only JSON OUPUT
     var appXmlType = config.Formatters.XmlFormatter.SupportedMediaTypes.FirstOrDefault
     (t => t.MediaType == "application/xml");
     config.Formatters.XmlFormatter.SupportedMediaTypes.Remove(appXmlType);
  }
} 

Self-hosting will be done from Program.Main method. Code for the same looks like this:

C#
public static void Main()
{
   var config = new System.Web.Http.SelfHost.HttpSelfHostConfiguration("http://Localhost:8080");
   WebApiConfig.Register(config);
 
   using (var server = new System.Web.Http.SelfHost.HttpSelfHostServer(config))
   {
        server.OpenAsync().Wait();
        Console.WriteLine("Press Enter to quit.");
        Console.ReadLine();
   }
} 

Output you get from URL: http://localhost:8080/api/test/getMethodError is as follows:

C#
{"Message":"Get Custom error message from Method","ErrorCode":405} 

GetHandlerException triggers business exception. If you notice, at the top of this method I have added an attribute (MethodAttributeExceptionHandling) which takes care of any exception raised from this method. This attribute is the custom attribute which we have created inheriting ExceptionFilterAttribute . We have overridden OnException method from ExceptionFilterAttribute. This method gets executed once the error is being raised. For getting the exception detail, you can access object actionExecutedContext.Exception .

C#
[MethodAttributeExceptionHandling]
public HttpResponseMessage GetHandlerException()
{
   throw new 
         BusinessLayerException("4001", "Your account is in negative. please recharge");
 
}  

Code for MethodAttributeExceptionHandling looks like this:

C#
public class MethodAttributeExceptionHandling : ExceptionFilterAttribute
{
 
        public override void OnException(HttpActionExecutedContext actionExecutedContext)
        {
            if (actionExecutedContext.Exception is BusinessLayerException)
            {
                var businessException = actionExecutedContext.Exception as BusinessLayerException;
                var errorMessagError = new System.Web.Http.HttpError(businessException.ErrorDescription) { { "ErrorCode", businessException.ErrorCode } };
                actionExecutedContext.Response =
                    actionExecutedContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, errorMessagError);
 
            }
            else if (actionExecutedContext.Exception is DataLayerException)
            {
                var dataException = actionExecutedContext.Exception as DataLayerException;
                var errorMessagError = new System.Web.Http.HttpError(dataException.ErrorDescription) { { "ErrorCode", dataException.ErrorCode } };
                actionExecutedContext.Response =
                    actionExecutedContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, errorMessagError);
 
            }
            else
            {
                var errorMessagError = new System.Web.Http.HttpError("Oops some internal Exception. Please contact your administrator") { { "ErrorCode", 500 } };
                actionExecutedContext.Response =
                    actionExecutedContext.Request.CreateErrorResponse(HttpStatusCode.InternalServerError, errorMessagError);
            }
        }
}  

MethodAttributeException needs to configure for processing exception. This can be done by adding this object to configuration.

C#
config.Filters.Add(new MethodAttributeExceptionHandling()); 

Output you get from URL: http://localhost:8080/api/test/GetHandlerException is as follows:

C#
{"Message":"Your account is in negative. please recharge","ErrorCode":4001} 

Now what if we want to have global exception wrapper? This can be done by adding custom class which inherits ApiControllerActionInvoker. I have created custom class CustomApiControllerActionInvoker which overrides InvokeActionAsync method. Logic here is similar to what we have done in MethodAttributeExceptionHandling. We need to make changes in the way in which we access Exception & its return type. Here, we will get an exception in the form of collection & we need to return HttpResponseMessage in the form task.

C#
public class CustomApiControllerActionInvoker : ApiControllerActionInvoker
    {
        public override Task<HttpResponseMessage> InvokeActionAsync(HttpActionContext actionContext, CancellationToken cancellationToken)
        {
            var result = base.InvokeActionAsync(actionContext, cancellationToken);
 
            if (result.Exception != null && result.Exception.GetBaseException() != null)
            {
                var baseException = result.Exception.InnerExceptions[0];//result.Exception.GetBaseException();

                if (baseException is BusinessLayerException)
                {
                    var baseExcept = baseException as BusinessLayerException;
                    var errorMessagError = new System.Web.Http.HttpError(baseExcept.ErrorDescription) 
                    { { "ErrorCode", baseExcept.ErrorCode } };
                    return Task.Run<HttpResponseMessage>(() => 
                    actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, errorMessagError));
                }
                else if (baseException is DataLayerException)
                {
                    var baseExcept = baseException as DataLayerException;
                    var errorMessagError = new System.Web.Http.HttpError(baseExcept.ErrorDescription) 
                    { { "ErrorCode", baseExcept.ErrorCode } };
                    return Task.Run<HttpResponseMessage>(() => 
                    actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, errorMessagError));
                }
                else
                {
                    var errorMessagError = new System.Web.Http.HttpError
                    ("Oops some internal Exception. Please contact your administrator") 
                    { { "ErrorCode", 500 } };
                    return Task.Run<HttpResponseMessage>(() => 
                    actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, errorMessagError));
                }
            }
 
            return base.InvokeActionAsync(actionContext, cancellationToken);
        }
    } 

CustomApiControllerActionInvoker needs to be configured with Service. This can be done replacing IHttpActionInvoker from service.

C#
config.Services.Replace(typeof(IHttpActionInvoker), new CustomApiControllerActionInvoker());  

Note that once we replace IHttpActionInvoker , MethodAttributeExceptionHandling logic will not function.

Output you get from URL: http://localhost:8080/api/test/GetGlobalErrorMessage is as follows:

C#
{"Message":"Oops some internal Exception. Please contact your administrator","ErrorCode":500} 

This is how the complete code block will look like:

C#
using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;
 
namespace TempTestConsoleApplication
{
    class Program
    {
        public static void Main()
        {
            var config = new System.Web.Http.SelfHost.HttpSelfHostConfiguration("http://Localhost:8080");
            WebApiConfig.Register(config);
 
            using (var server = new System.Web.Http.SelfHost.HttpSelfHostServer(config))
            {
                server.OpenAsync().Wait();
                Console.WriteLine("Press Enter to quit.");
                Console.ReadLine();
            } 
        }
    }
 
    public class CustomException : Exception
    {
        public virtual string ErrorCode { get; set; }
        public virtual string ErrorDescription { get; set; }
    }
 
    public class BusinessLayerException : CustomException
    {
       
        public BusinessLayerException(string errorCode, string errorDescription)
            : base()
        {
            base.ErrorCode = errorCode;
            base.ErrorDescription = errorDescription;
        }
    }
 
    public class DataLayerException : CustomException
    {
        public DataLayerException(string errorCode, string errorDescription)
            : base()
        {
            base.ErrorCode = errorCode;
            base.ErrorDescription = errorDescription;
        }
    }
 
    public static class WebApiConfig
    {
        public static void Register(System.Web.Http.HttpConfiguration config)
        {
            //Route Configuration
            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{action}"
            );
            
            config.Filters.Add(new MethodAttributeExceptionHandling());
            config.Services.Replace(typeof(IHttpActionInvoker), new CustomApiControllerActionInvoker());
 
            //Only JSON OUPUT
            var appXmlType = config.Formatters.XmlFormatter.SupportedMediaTypes.
            FirstOrDefault(t => t.MediaType == "application/xml");
            config.Formatters.XmlFormatter.SupportedMediaTypes.Remove(appXmlType);
        }
    }
 
    public class MethodAttributeExceptionHandling : ExceptionFilterAttribute
    {
 
        public override void OnException(HttpActionExecutedContext actionExecutedContext)
        {
            if (actionExecutedContext.Exception is BusinessLayerException)
            {
                var businessException = actionExecutedContext.Exception as BusinessLayerException;
                var errorMessagError = new System.Web.Http.HttpError(businessException.ErrorDescription) 
                { { "ErrorCode", businessException.ErrorCode } };
                actionExecutedContext.Response =
                    actionExecutedContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, errorMessagError);
 
            }
            else if (actionExecutedContext.Exception is DataLayerException)
            {
                var dataException = actionExecutedContext.Exception as DataLayerException;
                var errorMessagError = new System.Web.Http.HttpError(dataException.ErrorDescription) 
                { { "ErrorCode", dataException.ErrorCode } };
                actionExecutedContext.Response =
                    actionExecutedContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, errorMessagError);
 
            }
            else
            {
                var errorMessagError = new System.Web.Http.HttpError("Oops some internal Exception. 
                Please contact your administrator") { { "ErrorCode", 500 } };
                actionExecutedContext.Response =
                    actionExecutedContext.Request.CreateErrorResponse
                    	(HttpStatusCode.InternalServerError, errorMessagError);
            }
        }
    }
 
    public class CustomApiControllerActionInvoker : ApiControllerActionInvoker
    {
        public override Task<HttpResponseMessage> 
        InvokeActionAsync(HttpActionContext actionContext, CancellationToken cancellationToken)
        {
            var result = base.InvokeActionAsync(actionContext, cancellationToken);
 
            if (result.Exception != null && result.Exception.GetBaseException() != null)
            {
                var baseException = result.Exception.InnerExceptions[0];//result.Exception.GetBaseException();

                if (baseException is BusinessLayerException)
                {
                    var baseExcept = baseException as BusinessLayerException;
                    var errorMessagError = new System.Web.Http.HttpError
                    (baseExcept.ErrorDescription) { { "ErrorCode", baseExcept.ErrorCode } };
                    return Task.Run<HttpResponseMessage>(() => 
                    actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, errorMessagError));
                }
                else if (baseException is DataLayerException)
                {
                    var baseExcept = baseException as DataLayerException;
                    var errorMessagError = new System.Web.Http.HttpError
                    (baseExcept.ErrorDescription) { { "ErrorCode", baseExcept.ErrorCode } };
                    return Task.Run<HttpResponseMessage>(() => 
                    actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, errorMessagError));
                }
                else
                {
                    var errorMessagError = new System.Web.Http.HttpError
                    ("Oops some internal Exception. Please contact your administrator") 
                    { { "ErrorCode", 500 } };
                    return Task.Run<HttpResponseMessage>(() => 
                    actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, errorMessagError));
                }
            }
 
            return base.InvokeActionAsync(actionContext, cancellationToken);
        }
    }
 
    public class TestController : System.Web.Http.ApiController
    {
        public HttpResponseMessage GetMethodError()
        {
            try
            {
                throw new Exception("Get Custom error message from Method");
            }
            catch (Exception Ex)
            {
                var errorMessagError 
                    = new System.Web.Http.HttpError(Ex.Message) { { "ErrorCode", 405 } };
 
                throw new 
                    HttpResponseException(ControllerContext.Request.CreateErrorResponse
                    (HttpStatusCode.MethodNotAllowed, errorMessagError));
            }
        }
 
        [MethodAttributeExceptionHandling]
        public HttpResponseMessage GetHandlerException()
        {
            throw new 
                BusinessLayerException("4001", 
                "Your account is in negative. please recharge");
 
        }
 
        public HttpResponseMessage GetGlobalErrorMessage()
        {
            int i = 0;
            var val = 10 / i;
            return new 
                HttpResponseMessage(HttpStatusCode.OK);
        }
    }
}  

Points of Interest

In the above example, I have presented three different ways by which we can handle exception in WebApi. These three ways are:

  1. Exception Handling inside a method
  2. Exception Handling using Attribute
  3. Global exception handling using IHttpActionInvoker

You can also go ahead and evaluate Delegate Handlers for implementing Exception handling.

Reference

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)