Wednesday, January 27, 2010

Exception Handling in ASP.NET MVC



Exception handling in ASP.NET MVC Application using ActionFilter


 


1.      Introduction


Exceptions occur in software applications due to various reasons i.e. a remote service is not available or application database is down.  So a consistent strategy is required for processing exceptions while building an application that answers following questions.


·         How to handle a particular exception as exceptions are of different types?


·         How to segregate these exceptions and their handling mechanism?


·         Should a specific exception be propagating to next layer or be handled in the same layer?


·         Should a specific exception be wrapped with other generic exception and then propagate to the next layer?


·         Should a specific exception warrant no action?


·         How to notify the exception?


·         How to log the exception


This article focuses on how to handle exception in an ASP.NET MVC application using Action Filters.


MVC as a framework divides an application’s implementation into three component roles: Models, Views and Controllers. "Models" in a MVC based application are the components of the application that are responsible for maintaining state.  "Views" in a MVC based application are the components responsible for displaying the application's user interface.  "Controllers" in a MVC based application are the components responsible for handling end user interaction, manipulating the model, and ultimately choosing a view to render to display UI.  The right place to handle exceptions in an ASP.NET MVC is in the Controllers because they play a pivot role in an ASP.NET MVC application. Hence the exceptions that arise in a model should be allowed to be propagating to the controllers.


2.      ASP.NET MVC Action Filters


An action filter is an attribute that you can apply to a controller action or an entire controller that modifies the way in which the action is executed. The ASP.NET MVC framework includes several action filters:



  • OutputCache – This action filter caches the output of a controller action for a specified amount of time.

  • HandleError – This action filter handles errors raised when a controller action executes.

  • Authorize – This action filter enables you to restrict access to a particular user or role.

3.      Motivation for creating our own Action Filter for Exception Handling.


The out of box action filter “HandleError” handles the exception and can redirect the user to a user defined error view that is inherited from HandleErrorInfo class.  The HandleErrorInfo class which is used as the model for the error view only contains the Exception, Controller name and Action name. It means any custom property added to the attribute would not be available from the View Model. So one of the approaches for this is to create our own HandleErrorAttribute. The other benefits of creating our own HandlerErrorAttribute are:


·         Add a property to the Attribute and access it from Error View Model class.


·         Add exception handling code i.e. rollback operations, log the error.


·         Define the error view.


·         Implement a custom model class for the error view.


 


4.      Steps to create our own ActionFilter


ASP.NET MVC provides following base types and interfaces to create your own action filter. If you want to implement a particular type of filter, then you need to create a class that inherits from the base Filter class FilterAttribute and implements one or more of the IAuthorizationFilter, IActionFilter, IResultFilter, or IExceptionFilter interfaces.


So for our purpose we will derive our action filter from FilterAttribute and IExceptionFilter .


FilterAttribute:  This is the base class for any action filter attribute. The following property is available:



  • Int Order: Gets or sets the order.

IExceptionFilter:  Defines an operation that will be called when an exception occurs. The following operation is available:



  • void OnException(ExceptionContext filterContext): Called when an exception occurs.


So for our purpose we will derive our action filter from FilterAttribute and IExceptionFilter .


[SuppressMessage("Microsoft.Performance", "CA1813:AvoidUnsealedAttributes",
Justification = "This attribute is AllowMultiple = true and users might want to override behavior.")]
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
public class CustomHandleErrorAttribute : FilterAttribute, IExceptionFilter
{
}

Now add following properties to the class.


View – Gets or sets the View that will be rendered in case of the exception.


Master – Gets or sets the master page of the error view.


Exception Types – Gets or sets the type of exception that the attribute should handle. If the incoming exception is not of this type, then it should return without any action.


The next step is to implement the OnException(ExceptionContext filterContext) method of IExceptionFilter which is called when an exception occurs.  This is the place where you should write your exception handling code.  The filterContext provides vital information about the exception. It has following important properties.


Exception – This is the exception object.


HttpContext – HttpContext object that provides access to the Request and Response object of current http request.


ExceptionHandled – Gets or sets a value indicating whether the exception has been handled. This property is useful to check if the exception is already handled by a previous instance of the HandleError attribute when several instances get invoked.


RouteData.Values ["controller"] – Returns the name of the Controller where the exception occurs.


RouteData.Values ["action"] - Returns the name of the Action where the exception occurs.


Result – Gets or sets the ActionResult.


Steps for exception handling


1.       Checks if the incoming exception is of System.Exception Type. If it is, then handle it else return.


2.       Set the View Result to the error view.


3.       Logs the exception.


4.       Set ExceptionHandled property to true.


 


1.       Checks if the incoming exception is of Exception type and of type Http 500. If it is, then handle it else return.


Exception exception = filterContext.Exception;

// If this is not an HTTP 500 (for example, if somebody throws an HTTP 404 from an action method),
// ignore it.
if (new HttpException(null, exception).GetHttpCode() != 500)
{
return;
}

if (!ExceptionType.IsInstanceOfType(exception))
{
return;
}

2.       Set the View Result.


string controllerName = (string)filterContext.RouteData.Values["controller"];
string actionName = (string)filterContext.RouteData.Values["action"];
CustomHandleErrorInfo model = new CustomHandleErrorInfo(filterContext.Exception, controllerName, actionName, Message);
filterContext.Result = new ViewResult
{
ViewName = View,
MasterName = Master,
ViewData = new ViewDataDictionary<CustomHandleErrorInfo>(model),
TempData = filterContext.Controller.TempData
};

3.       Logs the exception


////Log the error
LogError(model);

4.       Set ExceptionHandled property to true


filterContext.ExceptionHandled = true;
filterContext.HttpContext.Response.Clear();
filterContext.HttpContext.Response.StatusCode = 500;

Complete source code of the component.


namespace Sample.Web
{
using System;
using System.Diagnostics.CodeAnalysis;
using System.Collections.Generic;
using System.Globalization;
using System.Web;
using System.Web.Mvc.Resources;
using System.Web.Mvc;
using Sample.Domain.Services;
[SuppressMessage("Microsoft.Performance", "CA1813:AvoidUnsealedAttributes",
Justification = "This attribute is AllowMultiple = true and users might want to override behavior.")]
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
public class CustomHandleErrorAttribute : FilterAttribute, IExceptionFilter
{
private const string DefaultView = "Error";
private Type _exceptionType = typeof(Exception);
private string _master;
private string _view;
public Type ExceptionType
{
get
{
return _exceptionType;
}
set
{
if (value == null)
{
throw new ArgumentNullException("value");
}
if (!typeof(Exception).IsAssignableFrom(value))
{
throw new ArgumentException(String.Format(CultureInfo.CurrentUICulture,
"The type '{0}' does not inherit from Exception.", value.FullName));
}

_exceptionType = value;
}
}

public string Master
{
get { return _master ?? String.Empty; }

set { _master = value; }
}

public string View
{
get { return (!String.IsNullOrEmpty(_view)) ? _view : DefaultView; }
set { _view = value; }
}

public string Message
{
get;
set;
}

public virtual void OnException(ExceptionContext filterContext)
{

if (filterContext == null)
{
throw new ArgumentNullException("filterContext");
}

// If custom errors are disabled, we need to let the normal ASP.NET exception handler
// execute so that the user can see useful debugging information.
if (filterContext.ExceptionHandled || !filterContext.HttpContext.IsCustomErrorEnabled)
{
return;
}

Exception exception = filterContext.Exception;

// If this is not an HTTP 500 (for example, if somebody throws an HTTP 404 from an action method),
// ignore it.
if (new HttpException(null, exception).GetHttpCode() != 500)
{
return;
}

if (!ExceptionType.IsInstanceOfType(exception))
{
return;
}

string controllerName = (string)filterContext.RouteData.Values["controller"];
string actionName = (string)filterContext.RouteData.Values["action"];
CustomHandleErrorInfo model = new CustomHandleErrorInfo(filterContext.Exception, controllerName, actionName, Message);

////Log the error
LogError(model);

filterContext.Result = new ViewResult
{
ViewName = View,
MasterName = Master,
ViewData = new ViewDataDictionary<CustomHandleErrorInfo>(model),
TempData = filterContext.Controller.TempData
};


filterContext.ExceptionHandled = true;
filterContext.HttpContext.Response.Clear();
filterContext.HttpContext.Response.StatusCode = 500;

// Certain versions of IIS will sometimes use their own error page when
// they detect a server error. Setting this property indicates that we
// want it to try to render ASP.NET MVC's error page instead.
filterContext.HttpContext.Response.TrySkipIisCustomErrors = true;
}

private void LogError(CustomHandleErrorInfo errorModel)
{
IExceptionLogger logger = new ExceptionLogger();
logger.LogError(errorModel.Exception);
}
}
}

5.      Web.Config changes


<customErrors mode="RemoteOnly" /> - This will show friendly errors to remote users only while allowing the developers to see exceptions and stack traces for debugging purposes.


<customErrors mode="On" /> - This will show friendly error to both remote and local users.


6.      CustomHandleErrorInfo class – This class is used as the model for the error view.


namespace Sample.Web
{
using System;
using System.Web.Mvc.Resources;

public class CustomHandleErrorInfo
{
public CustomHandleErrorInfo(Exception exception, string controllerName, string actionName, string message)
{
if (exception == null)
{
throw new ArgumentNullException("exception");
}
if (String.IsNullOrEmpty(controllerName))
{
throw new ArgumentException("Value cannot be null or empty.", "controllerName");
}
if (string.IsNullOrEmpty(actionName))
{
throw new ArgumentException("Value cannot be null or empty.", "actionName");
}

Exception = exception;
ControllerName = controllerName;
ActionName = actionName;
Message = message;
}

public string ActionName { get; private set; }

public string ControllerName { get; private set; }

public Exception Exception { get; private set; }

public string Message { get; private set; }

}
}


7.      Usage of CustomErrorHandleAttribute


Controllers in MVC are implemented as a set of Action methods (Get/Post).  Exception handling can be done at the Action level or Controller level. When should we apply CustomErrorHandleAttribute at Action level and Controller level?  To explain this I will take following scenarios:


 


You should apply CustomErrorHandleAttribute at action level if you


·         Display a message to the end user specific to the context of the Action. For ex: If an action method carries out user Registration activity and you want to display a message such as “The system failed to register you because of some system error” when an exception occurs.


·         You want to add some vital information to the attribute for system recovery specific to the context of the Action.


 


Except the above scenarios, in all other cases Error Handle attribute should be applied at the Controller level.  The good news is you can apply the attribute at the Action level as well as its Controller level. In such a case the Action level attribute will take precedence over Controller level provided Order = 0 is set for the Controller’s attribute instead of leaving the default value -1.


 


Applying CustomErrorHandleAttribute at Controller level – This will handle any exception happens in anywhere in the controller and redirect the user to the default error view.


[CustomHandleErrorAttribute()]
public class ProfileController : Controller
{
public virtual ActionResult Register()
{
return View();
}
}

Applying CustomErrorHandleAttribute at Action Level – This will override the Controller level attribute. It will redirect the user to “RegisterErrorView” instead of the default error view. But don’t forget to set Order = 0 for the Controller’s attribute. By default, the Order = -1 implicitly. If we don’t set Order = 0 then, the Controller’s attribute will take precedence over Action method’s attribute and the user will be always redirected to the View defined in the Controller’s attribute.


[CustomHandleErrorAttribute(Order = 0)]
public class ProfileController : Controller
{
[CustomHandleErrorAttribute(View = "RegisterErrorView")]
public virtual ActionResult Register()
{
return View();
}
}

Applying CustomErrorHandleAttribute to handle a specific exception type – This usage redirects to a different view based on the Exception type.


[CustomHandleErrorAttribute(ExceptionType = typeof(ArgumentNullException), View = "NullErrorPage")]
public class ProfileController : Controller
{
public virtual ActionResult Register()
{
return View();
}
}


8.      Other approaches for exception handling – ELMAH


ELMAH – Error Logging Modules and Handlers


HTTP modules and handlers can be used in ASP.NET to provide a high degree of componentization for code that is orthogonal to a web application, enabling entire sets of functionalities to be developed, packaged and deployed as a single unit and independent of an application. ELMAH (Error Logging Modules and Handlers) illustrates this approach by demonstration of an application-wide error logging that is completely pluggable. It can be dynamically added to a running ASP.NET web application, or even all ASP.NET web applications on a machine, without any need for re-compilation or re-deployment.


For an introductory piece on ELMAH, see the “Using HTTP Modules and Handlers to Create Pluggable ASP.NET Components” on MSDN, which was co-authored with Scott Mitchell of 4GuysFromRolla.


9.      Conclusion:


Custom Action Filter helps team to quickly add exception handling to ASP.NET MVC Applications without much configuration. It gives the developer control to add additional information to the exception for better troubleshooting.


 


10.      References:


http://www.asp.net/LEARN/mvc/tutorial-14-cs.aspx



 


No comments:

Post a Comment