Get to Know Action Filters in ASP.NET MVC 3 Using HandleError

Update – for folks who learn best visually, I’ve posted a follow-up screencast of the demo steps discussed below, as a DevNuggets video. You can view the video here.

What’s an Action Filter?

If you’re just getting started with ASP.NET MVC, you may have heard of something called action filters , but haven’t had the chance to use them yet. Action filters provide a convenient mechanism for attaching code to your controllers and/or action methods that implements what are referred to as cross-cutting concerns, that is, functionality that isn’t specific to one particular action method, but rather is something you’d want to re-use across multiple actions.

An action filter is a .NET class that inherits from FilterAttribute or one of its subclasses, usually ActionFilterAttribute, which adds the OnActionExecuting, OnActionExecuted, OnResultExecuting, and OnResultExecuted methods, providing hooks for code to be executed both before and after the action and result are processed.

Because action filters are subclasses of the System.Attribute class (via either FilterAttribute or one of its subclasses), they can be applied to your controllers and action methods using the standard .NET metadata attribute syntax:


    1: [MyNamedAttribute(MyParam = MyValue)]
    2: public ActionResult MyActionMethod()
    3: {
    4:    // do stuff
    5: }


    1: <MyNamedAttribute(MyParam:=MyValue)> _
    2: Public Function MyActionMethod() As ActionResult
    3:    ' do stuff
    4: End Function

This makes action filters an easy way to add frequently-used functionality to your controllers and action methods, without intruding into the controller code, and without unnecessary repetition.

To be clear, action filters aren’t new to MVC 3, but there’s a new way to apply them in MVC 3 that I’ll discuss later on in this post.

What’s in the Box?

ASP.NET MVC provides several action filters out of the box:

  • Authorize – checks to see whether the current user is logged in, and matches a provided username or role name (or names), and if not it returns a 401 status, which in turn invokes the configured authentication provider.

  • ChildActionOnly – used to indicate that the action method may only be called as part of a parent request, to render inline markup, rather then returning a full view template.

  • OutputCache – tells ASP.NET to cache the output of the requested action, and to serve the cached output based on the parameters provided.

  • HandleError – provides a mechanism for mapping exceptions to specific View templates, so that you can easily provide custom error pages to your users for specific exceptions (or simply have a generic error view template that handles all exceptions).

  • RequireHttps – forces a switch from http to https by redirecting GET requests to the https version of the requested URL, and rejects non-https POST requests.

  • ValidateAntiForgeryToken – checks to see whether the server request has been tampered with. Used in conjunction with the AntiForgeryToken HTML Helper, which injects a hidden input field and cookie for later verification (Here’s a post showing one way you can enable this across the board).

  • ValidateInput – when set to false, tells ASP.NET MVC to set ValidateRequest to false, allowing input with potentially dangerous values (i.e. markup and script). You should properly encode any values received before storing or displaying them, when request validation is disabled. Note that in ASP.NET 4.0, request validation occurs earlier in the processing pipeline, so in order to use this attribute, you must set the following value in your web.config file:

     <httpRuntime requestValidationMode="2.0"/>

    Note also that any actions invoked during the request, including child actions/partials, must have this attribute set, or you may still get request validation exceptions, as noted in this Stack Overflow thread.

Learning From Our Errors

I often find that the best way for me to learn something new is by actually implementing it, so to that end, I’m going to walk through the process of handling a specific error using the HandleError action filter, and in the process explain a few more things about how these filters work.

First, let’s create a new ASP.NET MVC 3 Web Application (if you don’t have ASP.NET MVC 3 installed, you can grab it quickly and painlessly using the Web Platform Installer):


We’ll go with the Internet Application template, using the Razor View engine. Visual Studio helpfully opens up our HomeController for us when the project is loaded:

    1: namespace HandleErrorTut.Controllers
    2: {
    3:     public class HomeController : Controller
    4:     {
    5:         public ActionResult Index()
    6:         {
    7:             ViewModel.Message = "Welcome to ASP.NET MVC!";
    9:             return View();
   10:         }
   12:         public ActionResult About()
   13:         {
   14:             return View();
   15:         }
   16:     }
   17: }

Nothing in there about handling errors, though, right? Yes, and no. Thanks to a new feature of ASP.NET MVC 3 called Global Filters, our application is already wired up to use the HandleErrorAttribute. How? Simple. In global.asax.cs, you’ll find the following code:

    1: public static void RegisterGlobalFilters(GlobalFilterCollection filters)
    2: {
    3:     filters.Add(new HandleErrorAttribute()); 
    4: }
    6: protected void Application_Start()
    7: {
    8:     RegisterGlobalFilters(GlobalFilters.Filters); 
    9:     RegisterRoutes(RouteTable.Routes);
   10: }

By adding the HandleErrorAttribute to the GlobalFilters.Filters collection, it will be applied to every action in our application, and any exception not handled by our code will cause it to be invoked. By default, it will simply return a View template by the name of Error, which conveniently has already been placed in Views > Shared for us:


So what does this look like? Let’s find out. Add code to the About action to cause an exception, as shown below:

    1: public ActionResult About()
    2: {
    3:     throw new DivideByZeroException();
    4:     return View();
    5: }

Then run the application using Ctrl+F5, and click the About link in the upper-right corner of the page. Whoops! That doesn’t look much like a View template, does it?


Why are we getting the yellow screen of death? Because by default, CustomErrors is set to RemoteOnly, meaning that we get to see the full details of any errors when running locally. The HandleErrors filter only gets invoked when CustomErrors is enabled. To see the HandleErrors filter in action when running locally, we need to add the following line to web.config, in the system.web section:

    1: <customErrors mode="On"/>

Now run the application again, and click About. You should see the following screen:


Now HandleError has been invoked and is returning the default Error View template. In the process, HandleError also marks the exception as being handled, thus avoiding the dreaded yellow screen of death.

Taking Control

Now let’s assume that the generic error view is fine for most scenarios, but we want a more specific error view when we try to divide by zero. We can do this by adding the HandleError attribute to our controller or action, which will override the global version of the attribute.

First, though, let’s create a new View template. Right-click the Views > Shared folder and select Add > View, specifying the name as DivByZero, and strongly typing the view to System.Web.Mvc.HandleErrorInfo, which is passed by the HandleError filter to the view being invoked:


Using a strongly-typed view simplifies our view code significantly by binding the view to the HandleErrorInfo instance passed to the view, which can then be accessed via the @Model.propertyname syntax, as shown below:

    1: @model System.Web.Mvc.HandleErrorInfo
    3: @{
    4:     View.Title = "DivByZero";
    5:     Layout = "~/Views/Shared/_Layout.cshtml";
    6: }
    8: <h2>DivByZero</h2>
   10: <p>
   11:   Controller: @Model.ControllerName
   12: </p>
   13: <p>
   14:   Action: @Model.ActionName
   15: </p>
   16: <p>
   17:   Message: @Model.Exception.Message
   18: </p>
   19: <p>
   20:   Stack Trace: @Model.Exception.StackTrace
   21: </p>

Next, go back to HomeController, and add the HandleError attribute to the About action method, specifying that it should return the DivByZero View template:

    1: [HandleError(View="DivByZero")]
    2: public ActionResult About()
    3: // remaining code omitted

Now, if you run the application and click the About link, you’ll see the following:


But we now have a problem…the filter attached to the About action method will return the DivByZero View template regardless of which exception occurs. Thankfully, this is easy to fix by adding the ExceptionType parameter to the HandleError attribute:

    1: [HandleError(View="DivByZero", ExceptionType = typeof(DivideByZeroException))]

Now the HandleError attribute attached to our action method will only be invoked for a DivideByZeroException, while the global version of the HandleError attribute will be invoked for any other exceptions in our controllers or actions.

You can get even more control over how and when your filters are invoked by passing in the Order parameter to the attribute, or even creating your own custom filters. For example, you could create a custom filter that inherits from FilterAttribute, and implements IExceptionFilter, then implement IExceptionFilter’s OnException method to provide your own custom error handling, such as logging the error information, or performing a redirect. This is left as an exercise for the reader (or perhaps, for a future blog post!).


In this post, I’ve explained what action filters are, and some of the things they can do for you, and demonstrated, through the use of the HandleError filter, how you can customize their behavior. I’ve also shown how you can apply these filters across all of your controllers and actions through the use of the new global action filters feature of ASP.NET MVC 3. I also encourage you to read through the MSDN docs on Filtering in ASP.NET MVC for additional information.

I hope you’ve found this information useful, and welcome your feedback via the comments or email.