次の方法で共有


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:

C#:

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

VB:

    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):

ActionFilters1

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!";
    8:  
    9:             return View();
   10:         }
   11:  
   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: }
    5:  
    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:

ActionFilters2

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?

ActionFilters3

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:

ActionFilters4

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:

ActionFilters5

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
    2:  
    3: @{
    4:     View.Title = "DivByZero";
    5:     Layout = "~/Views/Shared/_Layout.cshtml";
    6: }
    7:  
    8: <h2>DivByZero</h2>
    9:  
   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:

ActionFilters6

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!).

Conclusion

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.

Comments

  • Anonymous
    March 17, 2011
    Nice post - specifically, I like the strongly-typed model binding to the System.Web.Mvc.HandleErrorInfo in the view.  Never even thought about doing that; however, I do see that the default Error view does the same.  Oh how we can learn just by discovering and analyzing the obvious (what's already there).  Thanks...

  • Anonymous
    March 17, 2011
    Dan, Glad you like the post...and yes, there's lots of stuff that can be gleaned simply by example from what's already there. Although I'm a longtime Web Forms developer, I have to say that learning by example is an area that I find MVC to be superior. Thanks for your comment!

  • Anonymous
    March 18, 2011
    You've been kicked (a good thing) - Trackback from DotNetKicks.com

  • Anonymous
    March 19, 2011
    Thank you for submitting this cool story - Trackback from progg.ru

  • Anonymous
    March 19, 2011
    I've been searching for a solution online for a while now, on how to properly return a 404 error, but without the ugly default looking 404 page. I could always throw a new HttpException(404, "Resource not found"), but that oddly returns a 500 error. Can anyone point me in the right direction?

  • Anonymous
    March 20, 2011
    Martin,   There are several approaches you could take, from the simple, using CustomErrors: <customErrors mode="On">   <error statusCode="404" redirect="~/Error/NotFound"/> </customErrors> You could also use a combination of customErrors and HandleError, as described here: devstuffs.wordpress.com/.../how-to-use-customerrors-in-asp-net-mvc-2 If you're seeing a 500 HTTP status code and you're using HandleError, that's to be expected, since HandleError by default sets the status code to 500. If you want the status code to be 404, you may want to look at using HttpNotFoundResult, as described here: weblogs.asp.net/.../asp-net-mvc-3-using-httpnotfoundresult-action-result.aspx That gives you a very clean way to return a 404. Keep in mind that depending on how your application is configured, IIS may be responding to the exception as well, so be sure you understand which part of the platform (IIS or ASP.NET) is responding to the exception you're throwing. Hope that helps!

  • Anonymous
    March 21, 2011
    While preparing to record a video walkthrough of the action filters tutorial I recently published , I

  • Anonymous
    March 21, 2011
    I’ve published a new DevNugget screencast on Channel 9, and linked below. In it, I demonstrate the use

  • Anonymous
    March 22, 2011
    Hey, great post - but for some reason, this one particular project I have doesn't work. I've tried following the steps as you've outlined, but when customErrors is on - it can't find the view. Frustrating .. since it works for all other projects, just not this particular one. Any ideas?

  • Anonymous
    March 22, 2011
    Hi Kori, When you say "it can't find the view," what are the symptoms you're seeing? Are you getting a yellow screen of death indicating that the view is missing?