Posts Tagged ‘action filters’

MVC: Action Filter for Handling Errors

// April 2nd, 2008 // 17 Comments » // MVC

A few months ago I posted an article and some code that contained filters for forms authentication and error handling for the Preview 1 (CTP) release of the MVC framework. Unfortunately the Preview 2 release that was made available a few weeks ago changed enough that the code I posted no longer works.

What the Preview 2 release did provide, however, was a new built-in filter framework. Rob Conery has already gone through the trouble of creating authentication filters that cover most of the functionality I had before, but I have yet to see an implementation of a filter for error handling that I like. I've gone ahead and started from scratch, throwing away my old filters, and created a new filter that I think covers most of the same scenarios as my old ErrorHandler filters while being much simpler to implement and use. Hopefully you'll find it useful.

First let's take a look at a simple use case scenario:

   1: public void Product( int? id )
   2: {
   3:     if( id == null )
   4:         throw new ArgumentNullException( "No Product ID" );
   5:     RenderView( "DisplayProduct", GetProduct(id.Value) );
   6: }

In the code above, we have a simple action that displays a product based upon the ID specified. What do we do when no ID is specified though? The "correct" thing to do seems to be to throw an exception, as we've done, but now the user will see either (a) an ugly 500 error screen [worst case] or (b) be redirected to the generic error page [best case]. Sometimes we'd like a bit more control than that though…

Let's go ahead and add our error handling filter to this action and tell it that whenever ArgumentNullException is thrown, redirect to the "Products" page, where the user can select a product with a valid ID.

   1: [RedirectToUrlOnError(Type=typeof(ArgumentNullException),Url="/Products")]
   2: public void Product( int? id )
   3: {
   4:     if( id == null )
   5:         throw new ArgumentNullException( "No Product ID" );
   6:     RenderView( "DisplayProduct", GetProduct(id.Value) );
   7: }

So we've added a [RedirectToUrlOnError] attribute and supplied it with a Type property – detailing the exception to catch – and a Url property – specifying the Url to navigate to upon a matched exception. You'll notice we are making a call to the GetProduct(int) method to retrieve the product's model so that we can pass it into the view's ViewData. What if this method were to fail? What if we weren't entirely certain what exception it would throw, or maybe we didn't care, we just want to handle any exception except for ArgumentNullException (which is already being handled). In this case we'll add another filter, but this time we will not specify the Type of exception that it should catch and just tell it that if anything isn't caught by another error handler redirect to the homepage.

   1: [RedirectToUrlOnError(Type=typeof(ArgumentNullException),Url="/Products")]
   2: [RedirectToUrlOnError(Url="/")]
   3: public void Product( int? id )
   4: {
   5:     if( id == null )
   6:         throw new ArgumentNullException( "No Product ID" );
   7:     RenderView( "DisplayProduct", GetProduct(id.Value) );
   8: }

You can have as many error handler filters attached to an action as you need, but only one may have no Type specified.

Now let's take a look at the code for the filter itself:

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Linq;
   4: using System.Web.Mvc;
   5: 
   6: namespace SquaredRoot.Mvc.Filters.ErrorHandling
   7: {
   8:     public class RedirectToUrlOnErrorAttribute : RedirectOnErrorAttribute
   9:     {
  10: 
  11:         public string Url{ get; set; }
  12: 
  13:         protected override bool Validate( FilterExecutedContext filterContext )
  14:         {
  15: 
  16:             //### the url property is always needed
  17:             if( string.IsNullOrEmpty( Url ) || Url.Trim() == string.Empty )
  18:                 throw new ArgumentNullException( "RedirectToUrlOnErrorAttribute's Url property must have a value." );
  19: 
  20:             //### continue execution
  21:             return true;
  22: 
  23:         }
  24: 
  25:         protected override void Redirect( FilterExecutedContext filterContext )
  26:         {
  27:             filterContext.ExceptionHandled = true;
  28:             filterContext.HttpContext.Response.Redirect( Url, true );
  29:         }
  30: 
  31:     }
  32: }

So the [RedirectToUrlOnError] attribute inherits from the [RedirectOnError] attribute, which is where most of the hard work is done. We'll take a look at that base class in a bit, but first let's look at the other attribute you can use to trap and respond to errors – the [RedirectToActionOnError] attribute. We'll continue with the Product(id) sample from above, but this time redirect to an action rather than a hardcoded Url:

   1: [RedirectToActionOnError(
   2:     Type=typeof(ArgumentNullException),
   3:     Controller=typeof(ProductController),
   4:     Action="Index" )]
   5: [RedirectToUrlOnError(Url="/")]
   6: public void Product( int? id )
   7: {
   8:     if( id == null )
   9:         throw new ArgumentNullException( "No Product ID" );
  10:     RenderView( "DisplayProduct", GetProduct(id.Value) );
  11: }

You can see that this time instead of providing the Url property we are using providing the type of the controller that contains our target action, and the name of the action as a string. (Unfortunately lambda expressions are not allowed as parameters to an attribute, so I was limited in my options here. If you have a better idea, please let me know!) Also note that the catch-all is still there as a [RedirectToUrlOnError] attribute. You may use the [RedirectToActionOnError] attribute as a catch-all and you can mix and match the two attribute types, but still only one catch-all attribute total is allowed per action (in other words, you cannot have one of each).

Now let's see the code for this filter:

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Linq;
   4: using System.Web.Mvc;
   5: using System.Web.Routing;
   6: 
   7: namespace SquaredRoot.Mvc.Filters.ErrorHandling
   8: {
   9:     public class RedirectToActionOnErrorAttribute : RedirectOnErrorAttribute
  10:     {
  11: 
  12:         public Type Controller{ get; set; }
  13:         public string Action{ get; set; }
  14: 
  15:         protected override bool Validate( FilterExecutedContext filterContext )
  16:         {
  17: 
  18:             //### the url property is always needed
  19:             if(
  20:                 Controller == null ||
  21:                 ( string.IsNullOrEmpty( Action ) || Action.Trim() == string.Empty )
  22:             )
  23:                 throw new ArgumentNullException( "RedirectToUrlOnActionAttribute's Controller and Action properties must have values." );
  24: 
  25:             //### make sure the Contoller property is a Controller
  26:             if( !typeof(System.Web.Mvc.Controller).IsAssignableFrom( Controller ) )
  27:                 throw new ArgumentException( "RedirectToUrlOnActionAttribute's Controller property's value must derive from System.Web.Mvc.Controller." );
  28: 
  29:             //### continue processing
  30:             return true;
  31: 
  32:         }
  33: 
  34:         protected override void Redirect( FilterExecutedContext filterContext )
  35:         {
  36: 
  37:             //### turn "Foo.Foo.Foo.BarController" into "Bar"
  38:             string controllerName = Controller.ToString();
  39:             controllerName = controllerName.Substring( controllerName.LastIndexOf(".") + 1 );
  40:             controllerName = controllerName.Substring( 0, controllerName.LastIndexOf("Controller") );
  41: 
  42:             //### turn route data into url
  43:             RouteValueDictionary rvd = new RouteValueDictionary( new{
  44:                 controller = controllerName,
  45:                 action = Action
  46:             } );
  47:             ControllerContext ctx = new ControllerContext(
  48:                 filterContext.HttpContext,
  49:                 filterContext.RouteData,
  50:                 filterContext.Controller
  51:             );
  52:             VirtualPathData vpd = RouteTable.Routes.GetVirtualPath( ctx, rvd );
  53:             string url = vpd.VirtualPath;
  54: 
  55:             //### redirect
  56:             filterContext.ExceptionHandled = true;
  57:             filterContext.HttpContext.Response.Redirect( url, true );
  58: 
  59:         }
  60: 
  61:     }
  62: }

Other than some complexity with determining the Url, everything is very similar to the other filter. Again it appears the base class is doing the heavy lifting. Let's finally take a look at that base class:

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Linq;
   4: using System.Web.Mvc;
   5: 
   6: namespace SquaredRoot.Mvc.Filters.ErrorHandling
   7: {
   8:     public abstract class RedirectOnErrorAttribute : ActionFilterAttribute
   9:     {
  10: 
  11:         public Type Type { get; set; }
  12: 
  13:         public override void OnActionExecuted( FilterExecutedContext filterContext )
  14:         {
  15: 
  16:             //### check for errors
  17:             if( !Validate(filterContext) )
  18:                 return;
  19: 
  20:             //### make sure the Type property is an Exception
  21:             if( Type != null && !typeof(System.Exception).IsAssignableFrom( Type ) )
  22:                 throw new ArgumentException( "RedirectOnErrorAttribute's Type property's value must derive from System.Exception." );
  23: 
  24:             //### if no exception occurred, stop processing this filter
  25:             if( filterContext.Exception == null )
  26:                 return;
  27: 
  28:             //### get inner exception unless it is null (this should never happen?)
  29:             Exception ex = filterContext.Exception.InnerException ?? filterContext.Exception;
  30: 
  31:             //### if exception was thrown because of Response.Redirect, ignore it
  32:             if( ex.GetType() == typeof(System.Threading.ThreadAbortException) )
  33:                 return;
  34:             else if( Type == typeof(System.Threading.ThreadAbortException) )
  35:                 throw new ArgumentException( "Cannot catch exceptions of type 'ThreadAbortException'." );
  36: 
  37:             //### if the specified Type matches the thrown exception, process it
  38:             if( IsExactMatch(ex) )
  39:                 Redirect( filterContext );
  40:             //### if this attribute has no specified Type, investigate further (this attribute is a catch-all error handler)
  41:             else if( Type == null )
  42:             {
  43: 
  44:                 //### loop through all other RedirectToUrlOnErrorAttribute on this method
  45:                 foreach( RedirectOnErrorAttribute att in GetAllAttributes( filterContext ) )
  46:                     //### ignore self
  47:                     if( att.GetHashCode() == this.GetHashCode() )
  48:                         continue;
  49:                     //### if another catch-all attribute is found, throw an exception
  50:                     else if( att.Type == null )
  51:                         throw new ArgumentException( "Only one RedirectOnErrorAttribute per Action may be specified without its Type property provided." );
  52:                     //### if an exact match is found, stop processing the catch-all. that attribute has priority
  53:                     else if( att.IsExactMatch(ex) )
  54:                         return;
  55: 
  56:                 //### no exact matches were found. if the specified Type for the catch-all fits, process here
  57:                 Redirect(filterContext);
  58: 
  59:             }
  60:             else
  61:                 //### specified Type was not null, but did not match the thrown exception. don't process
  62:                 return;
  63: 
  64:         }
  65: 
  66:         public bool IsExactMatch( Exception exception )
  67:         {
  68:             if( Type != null && exception.GetType() == Type )
  69:                 return true;
  70:             else
  71:                 return false;
  72:         }
  73: 
  74:         private List<RedirectOnErrorAttribute> GetAllAttributes( FilterExecutedContext filterContext )
  75:         {
  76:             return filterContext.ActionMethod
  77:                 .GetCustomAttributes( typeof( RedirectOnErrorAttribute ), false )
  78:                 .Select( a => a as RedirectOnErrorAttribute )
  79:                 .ToList();
  80:         }
  81: 
  82:         protected abstract bool Validate( FilterExecutedContext filterContext );
  83:         protected abstract void Redirect( FilterExecutedContext filterContext );
  84: 
  85:     }
  86: }

That's all there is. Feel free to take it, use it, change it, whatever. I tried my best to document it thoroughly with comments, but if you have any questions just drop a comment below and I'll try to respond quickly. I do ask that if you make any improvements, please leave a comment here letting everyone know what you've changed so that we can all benefit.

Here are the filters in a downloadable format:

RedirectOnErrorAttributes.zip (2.63 kb)

Kick It on DotNetKicks.comShout It on DotNetShoutOuts.com

MVC Authentication and Errors

// January 4th, 2008 // 21 Comments » // MVC

NOTE:
This article was written for the December CTP release of the MVC framework. Unfortunately, it does not entirely apply to the Preview 2 release or subsequent releases.

I love working with the recent CTP release of the ASP.Net MVC framework, but it is definitely an early release and is lacking many of the developer friendly features that we have grown to rely upon in WebForms. One such feature is WebForm’s easy to understand authentication model.

In the WebForms world URLs can be referenced in a web.config file and then have authentication rules applied to them. A rule that says that you must be logged in to view a secure page may look something like:

1 <location path=loginRequired.aspx>

2 <system.web>

3 <authorization>

4 <deny users=? />

5 </authorization>

6 </system.web>

7 </location>

A rule that says only users in the Administrators group may view it might look like this:

1 <location path=adminRequired.aspx>

2 <system.web>

3 <authorization>

4 <allow roles=Administrators />

5 <deny users=* />

6 </authorization>

7 </system.web>

8 </location>

It didn’t take long for me to run into the lack of any central authentication scheme in the new MVC framework. I searched around and found some older information from prior to the CTP release posted by Fredrik Normén that seemed to address my issues, but unfortunately one of the features his solution requires did not make its way into the CTP release: attribute based exception handling.

Looking through the code samples on the page you see how he uses the .Net frameworks built in PrincipalPermission attribute (from System.Security.Permissions) to classify an action as demanding the user be in a specific role. If the user is not in that role the .Net framework will throw a SecurityException. What good does that do? Well take a look at line 3 in the below code:

1 [ControllerAction]

2 [PrincipalPermission(SecurityAction.Demand, Role="Admin"]

3 [ExceptionHandler("Error", typeof(SecurityException))]

4 public void Edit(int? id)

5 {

6

7 }

The ExceptionHandler attribute appears to take two values:

  1. The view to render in the event of an error.
  2. The type of error to match against.

So based upon this code the PrincipalPermission will interrogate the user’s roles when the action is requested and if the user is not in the “Admin” role it will throw a SecurityException. At that point the ExceptionHandler will wake up and say “hey I can handle that” and render the view named “Error”. Neat huh? Too bad ExceptionHandler doesn’t exist…

Personally I liked most of the concepts that were introduced in Frederik’s post, so I went ahead and began to implement the ExceptionHandler attribute. Along the way I realized that what was really needed was a way to apply filters to a controller. I’ve seen Ivan Carrero’s controller filter implementation, but I wanted filters that hooked straight into the MVC Controller’s three major lifecycle events: OnPreAction, OnPostAction, and OnError. By doing so I felt I would minimize the difference between code in a filter and code in a controller. Thus was born the FilterController.

Filter Controller

The FilterController is an abstract class deriving from the System.Web.Mvc.Controller. It’s primary purpose is to interrogate itself via reflection when it is created and to then load any attributes that implement the IControllerFilter interface:

1 /// <summary>

2 /// Descendent of the MVC Controller class that adds capability of processing filters specified by attributes.

3 /// </summary>

4 public abstract class FilterController : Controller

5 {

6

7 /// <summary>

8 /// Default constructor.

9 /// </summary>

10 public FilterController()

11 {

12 Filters = GetFilterAttributes();

13 foreach( IControllerFilter filter in Filters )

14 {

15 filter.Initialize(this);

16 }

17 }

The filters are then called for each of the three integration events: OnPreAction, OnPostAction, and OnError. Here is what the OnError event does:

1 /// <summary>

2 /// Passes control of the OnError event on to all filters that want to handle it.

3 /// </summary>

4 /// <param name=”actionName”>The name of the action being requested when the exception was thrown.</param>

5 /// <param name=”methodInfo”>Reflection object representing the action being requested when the exception was thrown.</param>

6 /// <param name=”exception”>The exception thrown while the action was being requested.</param>

7 /// <returns>A boolean denoting the successful handling of the event.</returns>

8 protected override bool OnError( string actionName, MethodInfo methodInfo, Exception exception )

9 {

10 bool handled = false;

11 bool allTrue = true;

12 foreach( IControllerFilter filter in Filters )

13 {

14 if( filter.HandleOnError )

15 {

16 handled = true;

17 if( !filter.OnError( actionName, methodInfo, exception ) )

18 allTrue = false;

19 }

20 }

21 if( !handled )

22 throw exception;

23 return allTrue;

24 }

The OnPreAction and OnPostAction events look almost exactly like the OnError event.

To fulfill my initial goal of obtaining functionality similar to that described in Frederik’s post, I have created two filters:

  • SecurityFilter
  • ErrorHandlerFilter

Security Filter

While the PrincipalPermission attribute used in Frederik’s post handles many security scenarios well, it wasn’t as flexible or keyboard friendly as I would prefer. I created the SecurityFilter and an arrangement of sub-filters to create what I think is an easier solution.

To use the security filter in your controller you must first inherit from FilterController and apply the [SecurityFilter] attribute.

1 using System;

2 using System.Web.Mvc;

3 using SquaredRoot.Mvc;

4

5 namespace Example

6 {

7

8 [SecurityFilter]

9 public class MyController : FilterController

10 {

11

12 }

This alone does nothing, but you are now able to add one or more of the SecurityFilter‘s sub-filters to this controller or it’s actions. The sub-filters I have created are:

  • RequireLogin
    Validates that the user is logged in.
  • RequireAnonymous
    Validates that the user is NOT logged in.
  • RequireRole
    Validates that the user is in the specified role.
  • RequireAnyRole
    Validates that the user is in at least one of the specified roles
  • RequireEachRole
    Validates that the user is in every one of the specified roles.

Let’s imagine a controller for a simple bulletin board system. In order to post to this forum you must be logged in, if you want to delete a post you must be in either the “Administrators” role or the “Moderators” role, and if you want to undelete a post you must be in the “Administrators” group. That controller would look something like:

1 [SecurityFilter]

2 [RequireLogin]

3 public class ForumController : FilterController

4 {

5

6 [ControllerAction]

7 public void Post( string message ){ … }

8

9 [ControllerAction,RequireAnyRole( "Administrators", "Moderators" )]

10 public void Delete( int id ){ … }

11

12 [ControllerAction,RequireRole( "Administrators" )]

13 public void Undelete( int id ){ … }

14

15 }

By applying the [RequireLogin] attribute to the class you have applied that filter to all of the actions as well, which means you must be logged in to call the Post method. The other two methods use the appropriate version of the role requirement filters to achieve their goal.

What happens if the filter validations fail? In the case of an anonymous user attempting to access a restricted resource an AnonymousAccessException (which derives from SecurityException) is thrown while all other scenarios throw a SecurityException. What you do with those exceptions leads us to…

Error Handler Filter

Using the above ForumController, let’s add the ErrorHandler filter:

1 [SecurityFilter]

2 [ErrorHandlerFilter]

3 [RequireLogin]

4 public class ForumController : FilterController

5 {

6

7 }

Like with the last filter, this filter by itself does nothing but allow us to use the ErrorHandler sub-filter. Let’s go ahead and add two sub-filters: one to handle security exceptions and one to handle all other exceptions.

In the event of a security exception we’ll render the “AccessDenied” view while all other exceptions will render the “SystemError” view:

1 [SecurityFilter]

2 [ErrorHandlerFilter(ErrorHandlerMode.Render)]

3 [RequireLogin]

4 [ErrorHandler( 1, "AccessDenied", typeof(SecurityException) )]

5 [ErrorHandler( 2, "SystemError", typeof(Exception) )]

6 public class ForumController : FilterController

7 {

8

9 }

First notice that we are now providing the ErrorHandlerFilter attribute with an option that says ErrorHandlerMode.Render. This is because in the event of an error we want the controller to render the view with the name passed in. Later on we’ll look at the other mode: ErrorHandlerMode.Redirect.

Next notice that we are providing three values to each of the two ErrorHandler sub-filters:

  1. The order in which the sub-filter should be processed. This is important because the order the attribute is returned by reflection is unknown.
  2. The name of the view to render. Just like calling RenderView() from an action, this view name must be accessible to the controller (either in the controller’s view directory or in the Shared directory).
  3. The type of the exception to match against.

Keep in mind that these sub-filters could be applied at either the class level or the method level and that method-level sub-filters are processed before class-level sub-filters. We’ll stick with class-level throughout this article.

An “Access Denied” page popping up whenever we try to go somewhere we aren’t allowed to without logging in isn’t the best user experience. Let’s improve it by sending anonymous users to the login page instead. This time however, we don’t want just render the login view, we want to actually redirect to the SecurityController‘s Login action. While we’re at it, I’ll show you an example of handling multiple exceptions with one sub-filter:

1 [ErrorHandler( 1, "Login,Security",

2 ErrorHandlerMode.Redirect,

3 typeof(AnonymousAccessException) )]

4 [ErrorHandler( 2, "AccessDenied",

5 typeof(SecurityException) )]

6 [ErrorHandler( 3, "BadArgument",

7 typeof(ArgumentException),

8 typeof(ArgumentNullException),

9 typeof(ArgumentOutOfRangeException) )]

10 [ErrorHandler( 4, "SystemError",

11 typeof(Exception) )]

Here in line 1 we specify not the name of the view to render, but the name of the action and controller to redirect to (in the format “action,controller”). The handler knows to process this as a redirect because we’ve changed the mode for this one sub-filter to ErrorHandlerMode.Redirect. Lines 7, 8, & 9 illustrate the capability for one sub-filter to match against many exceptions.

Download The Code

I hope you find these filters useful. If they don’t happen to match your particular problem, then feel free to write your own filters. To do so you only have to implement the IControllerFilter interface. I’ve attempted to make it even easier to do so by providing a base class named ControllerFilter that already implements the interface and has several hooks for you to take advantage of.

You are free to use or modify this code for anything including commercial purposes. The only restriction I ask is that you do not take credit for this work yourself, but I do not require any specific attribution.

I have packaged the code into four different releases:

NOTE: These projects were built on Vista x64. If you are running a 32-bit version of Windows you may initially have trouble building the source versions below. To fix this you’ll need to re-add the System.Web.Extensions reference. See my comment below for more details.

Full Source Release
The entire solution zipped up.
Controller Filters (Full Source).zip

Example Site & Binaries Release
(Recommended) Just the binaries zipped up with an example site.
Controller Filters (Example Site).zip

Filter Source Release
The source code for the FilterController and filters. No example site.
Controller Filters (Filter Source).zip

Filter Binaries Release
The binaries for the FilterController and filters only. No source.
Controller Filters (Filter Binaries).zip

Kick It on DotNetKicks.comShout It on DotNetShoutOuts.com