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.
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
.
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;
}
}
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.
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.
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.
public static class WebApiConfig
{
public static void Register(System.Web.Http.HttpConfiguration config)
{
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{action}"
);
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:
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:
{"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
.
[MethodAttributeExceptionHandling]
public HttpResponseMessage GetHandlerException()
{
throw new
BusinessLayerException("4001", "Your account is in negative. please recharge");
}
Code for MethodAttributeExceptionHandling
looks like this:
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.
config.Filters.Add(new MethodAttributeExceptionHandling());
Output you get from URL: http://localhost:8080/api/test/GetHandlerException is as follows:
{"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.
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];
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.
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:
{"Message":"Oops some internal Exception. Please contact your administrator","ErrorCode":500}
This is how the complete code block will look like:
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)
{
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{action}"
);
config.Filters.Add(new MethodAttributeExceptionHandling());
config.Services.Replace(typeof(IHttpActionInvoker), new CustomApiControllerActionInvoker());
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];
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:
- Exception Handling inside a method
- Exception Handling using Attribute
- Global exception handling using
IHttpActionInvoker
You can also go ahead and evaluate Delegate Handlers for
implementing Exception handling.
Reference