Wednesday, November 9, 2011
Global Error Handling in ASP.NET MVC 3 with Ninject
Download the code for this blog post.
In my last blog post I explained how to decouple infrastructure concerns, such as logging, from the rest of your application using a Dependency Injection container, such as Ninject. I implemented an ILoggingService interface with an open-source logger called NLog. In this post I will show you how to implement centralized error handling in an ASP.NET MVC 3 application, so that you can handle exceptions in one place, where you can perform logging as well as display a custom error page.
One of the worst things you can do to handle errors is place a try / catch block in every action of every controller. Not only does it clutter the app with unnecessary code, it creates the possibility that you or another developer will forget to place the try / catch on an action and an unexpected exception will slip through.
A better approach is to handle exceptions in one place through an interception mechanism. ASP.NET MVC allows you to hook into the request pipeline by defining an Action Filter. In fact, one such filter that comes out of the box is a HandleError attribute. While it allows you to direct users to a particular view when certain errors occur, you cannot use it to log the exception. The other problem is that you would have to place the attribute on every controller in your application – not much of an improvement over a proliferation of try / catch’s.
It turns out, however, that implementing IExceptionFilter is quite easy (especially if you useReflector to reverse engineer HandleErrorAttribute ). The difference is that you can add a constructor which accepts an ILoggingService interface and is injected into the filter by your DI container. The nice thing about Ninject is that there is an extension for MVC3 apps, which you can easily add as a NuGet package. After installing the package, you get a NinjectMVC3.csfile placed in an App_Start folder with bootstrapping code. There you can load a Ninject module for binding the ILoggingSservice to a concrete implementation. I had to hunt for the source code and documentation for the Ninject.MVC3 extension, but I eventually found it hereon Github.
I implemented IExceptionFilter in a HandleExceptionFilter class, which performs logging in addition to creating a ViewResult with a custom error view.
public class HandleExceptionFilter : IExceptionFilter
{
private string _view;
private string _master;
private ILoggingService _loggingService;
public HandleExceptionFilter(ILoggingService loggingService, string master, string view)
{
_loggingService = loggingService;
_master = master ?? string.Empty;
_view = view ?? string.Empty;
}
public virtual void OnException(ExceptionContext filterContext)
{
if (filterContext == null)
{
throw new ArgumentNullException("filterContext");
}
if (!filterContext.IsChildAction && (!filterContext.ExceptionHandled
&& filterContext.HttpContext.IsCustomErrorEnabled))
{
Exception exception = filterContext.Exception;
if ((new HttpException(null, exception).GetHttpCode() == 500))
{
// Log exception
_loggingService.Error(exception);
// Show error view
ErrorFilterHelper.SetFilerContext(filterContext, _master, _view);
}
}
}
}
I consolidated code to show the error view in an ErrorFilterHelper.
internal static class ErrorFilterHelper
{
public static void SetFilerContext(ExceptionContext filterContext, string master, string view)
{
// Show error view
string controllerName = (string)filterContext.RouteData.Values["controller"];
string actionName = (string)filterContext.RouteData.Values["action"];
var model = new HandleErrorInfo(filterContext.Exception, controllerName, actionName);
var result = new ViewResult
{
ViewName = view,
MasterName = master,
ViewData = new ViewDataDictionary<HandleErrorInfo>(model),
TempData = filterContext.Controller.TempData
};
filterContext.Result = result;
filterContext.ExceptionHandled = true;
filterContext.HttpContext.Response.Clear();
filterContext.HttpContext.Response.StatusCode = 200;
filterContext.HttpContext.Response.TrySkipIisCustomErrors = true;
}
}
The next issue is how to insert HandleExceptionFilter into the pipeline. Luckily Ninject.MVC3 comes with a BindFilter extension method which allows you to bind action filters to classes implementing IActionFilter. To use it simply add a using directive for Ninject.Web.Mvc.FilterBindingSyntax, then supply a type argument for your filter.
private static void RegisterServices(IKernel kernel)
{
// Load logging module
kernel.Load(new LoggingModule());
// Bind exception handling filter
kernel.BindFilter<HandleExceptionFilter>(FilterScope.Global, 0)
.When(r => true)
.WithConstructorArgument("master", string.Empty)
.WithConstructorArgument("view", "Error");
}
Unhandled exceptions will now be routed to the HandleExceptionFilter, which will log the exception and take the user to the “Error” view.
This covers all unexpected exceptions. But what if you want to show a different view for particular exceptions? For this you will need to create a class that extends FilterAttributeand has an ExceptionType property. In this case you will want to use an injected propertyrather than an overloaded constructor. You will also want to set the Order property to greater than zero, so that it will intercept and deal with errors before your global error handling filter.
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = true, AllowMultiple = true)]
public class HandleExceptionAttribute : FilterAttribute, IExceptionFilter
{
public HandleExceptionAttribute()
{
// Errors come here before other error filters
Order = 1;
}
[Inject]
public ILoggingService LoggingService { private get; set; }
public virtual void OnException(ExceptionContext filterContext)
{
Now you can place the HandleException attribute on an action, indicating the specific exception type you want to handle, and the name of the view you want to show the user. If another exception occurs, HandleErrorFilter will log the error and show the default Error view.
public class HomeController : Controller
{
// GET: /
// ?errorcode=1
[HandleException(ExceptionType = typeof(CustomException), View="CustomError")]
public ActionResult Index(int? errorCode)
{
switch (errorCode.GetValueOrDefault())
{
case 1:
// This code will result in a DivideByZeroException
// No need for try/catch because exception filter will handle it
int a = 1;
int b = 0;
int result = a / b;
break;
case 2:
throw new CustomException("Expected Error");
}
return View();
}
}
In this example, the CustomException thrown in case 2 will be intercepted by HandleExceptionAttribute, where it will be logged and the exception marked as handled. The user will then see the “CustomError” view specified as a parameter for the HandleException attribute that appears on the Home controller’s Index action. But the DivideByZeroException, which is unexpected, will be handled by the HandleErrorFilter, which will log the exception and show the default “Error” view. To enable error filters, you need to set the mode of the <customErrors> element in web.config to “On” (set it to “RemoteOnly” if you want the unhandled exception to appear in the “Yellow Screen of Death.”)
You can download the code for this blog post here.