Error handling in ASP.NET MVC Part 2: Our implementation
•Posts in this series
- Error handling in ASP.NET MVC Part 1: Our options
- Error handling in ASP.NET MVC Part 2: Our implementation (This post)
Our implementation consists of two components.
- A custom exception filter that’s registered globally to handle any exceptions while processing filters, actions, and views.
- The Application_Error method in Global.aspx to catch any other errors generated while ASP.NET processes the request.
The custom exception filter
First we’ll create the custom exception filter. This will manage exceptions encountered while we’re processing filters, actions, and views.
Every error we log will get a correlation Id. This helps us track down the error when a user reports a problem to us. We can get the exact details of the error by just asking them to give us the correlation id displayed on the error page.
We then either specify a view result of JSON or HTML depending on if the request was the result of an AJAX call, set the correct HTTP status code, and mark the error as handled and return the view.
Here’s the code, please note it has been simplified for readability.
public void OnException(ExceptionContext filterContext)
{
//Don't bother if custom errors are turned off or if the exception is already handled
if (filterContext.ExceptionHandled || !filterContext.HttpContext.IsCustomErrorEnabled)
return;
//Get currently logged in user (null if anonymous)
Guid? loggedInUserId = ...
//Log the exception and get a correlation id
Guid? correlationId = Logger.LogException(…)
var httpException = filterContext.Exception as HttpException;
//Set the view correctly depending if it's an AJAX request or not
if (filterContext.HttpContext.Request.Headers["X-Requested-With"] == "XMLHttpRequest")
{
filterContext.Result = new JsonResult
{
JsonRequestBehavior = JsonRequestBehavior.AllowGet,
Data = new
{
error = true,
correlationId = correlationId
}
};
}
else
{
string view = "~/Views/Error/Index.cshtml";
if (httpException != null)
{
if (httpException.GetHttpCode() == 404)
{
view = "~/Views/Error/NotFound.cshtml";
}
}
filterContext.Result = new ViewResult
{
ViewName = view,
ViewData = new ViewDataDictionary<Guid?>(correlationId)
};
}
//If it's not a httpException, just set the status code as 500
if (httpException != null)
{
filterContext.HttpContext.Response.StatusCode = httpException.GetHttpCode();
}
else
{
filterContext.HttpContext.Response.StatusCode = 500;
}
filterContext.Result.ExecuteResult(filterContext.Controller.ControllerContext);
filterContext.ExceptionHandled = true;
filterContext.HttpContext.Response.Clear();
filterContext.HttpContext.Response.TrySkipIisCustomErrors = true;
}
The global exception handler
Finally if it’s some other general ASP.NET error like if the page doesn’t exist then we just do some basic logging and render the error page.
protected void Application_Error()
{
var exception = Server.GetLastError();
Server.ClearError();
var httpException = exception as HttpException;
//Logging goes here
var routeData = new RouteData();
routeData.Values["controller"] = "Error";
routeData.Values["action"] = "Index";
if (httpException != null)
{
if (httpException.GetHttpCode() == 404)
{
routeData.Values["action"] = "NotFound";
}
Response.StatusCode = httpException.GetHttpCode();
}
else
{
Response.StatusCode = 500;
}
// Avoid IIS7 getting involved
Response.TrySkipIisCustomErrors = true;
// Execute the error controller
IController errorsController = new ErrorController();
HttpContextWrapper wrapper = new HttpContextWrapper(Context);
var rc = new RequestContext(wrapper, routeData);
errorsController.Execute(rc);
}
The result
If we hit an exception in the ASP.NET MVC pipeline such as a error in our razor view we’ll now get the following result
Not only do we have a nice view but we also return the correct HTTP status code
If it’s a ASP.NET error like a page that doesn’t exist we’ll get the following result
And it also gives us a nice HTTP status code
Conclusion
Now we have a simple exception handling strategy that enables detailed logging in MVC context but also provides support for general ASP.NET errors such as page not found exceptions. However this approach does require you to add an error controller with two actions called Index and NotFound. This is required to render the views from the Application_Error method after we have lost the current MVC context.