Archive for April, 2008

MVC Membership Starter Kit – 1.2

// April 28th, 2008 // 6 Comments » // MVC

This weekend I posted a new release of the MVC Membership Starter Kit. This release is an update to migrate the starter kit to the new interim release of the MVC framework. If you do not feel comfortable using the interim release, please continue using the 1.1 release and wait for Microsoft to release Preview 3; we will update the Starter Kit soon thereafter.

Changes in 1.2:

  • WindowsLive is now a supported authentication scenario (read Maarten’s blog post on this).
  • Per Andrew Arnott’s suggestion, the starter kit now uses the DotNetOpenId library rather than the code previously used (which was created by Mads Kristensen). This gives us a more robust and secure implementation that will develop and improve independently of this project.
  • All actions that previously expected a username in the route now expect the user’s ProviderUserKey (a Guid) instead. This was done because users with OpenID urls as their username could not previously be accessed.
  • "Whitelist support" has been added to the OpenID implementation, allowing you to setup regular expressions that dictate which OpenID providers are allowed to be used when logging into your site. By default there is no whitelist, so all providers are allowed.
  • The starter kit now offers greater control over which authentication scenarios your site supports and which is the default. Out-of-the-box only FormsAuthentication is enabled and is obviously the default.
Kick It on DotNetKicks.comShout It on DotNetShoutOuts.com

Massive BlogEngine.net Security Hole

// April 13th, 2008 // No Comments » // Blogging

A massive security hole in BlogEngine.net was just revealed that allows anyone to see your passwords… Danny Douglass just added a post to his blog where he explains the issue and provides a patched BlogEngine.Core assembly to resolve the issue until the next release of BlogEngine is available.

I would advise anyone running BlogEngine.net to immediately go to Danny's blog and download & install the fix.

The faster we can get word out about this, the faster we can shut down this particular attack vector, so please try and get the word out to any BlogEngine.net users you are aware of and please kick Danny's post at DotNetKicks.

Thanks Danny!

Kick It on DotNetKicks.comShout It on DotNetShoutOuts.com

MVC: New Membership Starter Kit Release

// April 11th, 2008 // 12 Comments » // MVC

The Starter Kit

If you haven’t had a chance to read about the MVC Membership Starter Kit I’ve created, read this post first.

New Release

Since we first created the starter kit a week and a half ago, Maarten Balliauw and I have been hard at work fleshing out the implementation to provide as much functionality as possible. Last night we finished the last stretch of things we had identified for this release and have posted the code as a new release on CodePlex. Keep in mind you can also always download our latest builds from CodePlex as well without waiting for a new release.

New Features

OpenID

Mads Kristensen released a lightweight OpenID consumer earlier this year that I then proceeded to flesh out with a security patch. The reason I did so was so that I could include OpenID in this release of the Starter Kit.

Out of the box you can create a route to the OpenIDLogin action, which displays the following view:

OpenID

Once the user has entered their OpenID url, the starter kit will take care of the rest for you, with one critical exception: you have to map the url to a user in your membership database. To do so, you simple override a virtual method and return a MembershipUser, like so:

   1: protected override MembershipUser AssociateOpenIDToMembershipUser( string identity, string name, string email )
   2: {
   3:     return Membership.GetUser(identity);
   4: }

Note that the above implementation maps the OpenID url to a user’s UserName, which may or may not be what you want for your application. Adjust accordingly.

Password Recovery Tools

Maarten did a great job providing users with a way to manage their passwords. While logged in they can change their password:

ChangePassword

Or if they are having trouble logging in, they can submit their username…

ForgotPassword

…and then answer their password question (if the system is configured to require it)…

PasswordQuestion

…and they will then receive their password via email (or a newly generated password — depending on system configuration).

Client-Side Validation

All non-administrative forms now include basic client-side validation. The validations even change based upon your Membership settings.

For instance, by default the ASP.Net Membership provider requires passwords to contain at least 1 non-alphanumeric character. If a user entered a password of “password” they would see the following alert:

ClientSideValidation

Components: Login & LoginStatus

Maarten created components that emulate the functionality of the old Login and LoginStatus controls. Now it is easy to have a Login box on every page.

Major Refactoring

Most of the controller and filter code has been broken out into a separate assembly.

Your FormsAuthentication and FormsAuthenticationAdministration controllers should now inherit from a base version of each. Maarten has created a boat load of virtual method hooks for each action (OnBeforeBlah, OnAfterBlah, OnErrorBlah) that provides you with easy extensibility points without needing to directly modify the starter kit base code.

Hopefully this refactoring will make it easy for you to upgrade to future versions of the starter kit’s code as they become available.

The Future

Currently we’ve cleared our plate and have no more planned features to attend to. Does this mean that we are done? No. This is what you can expect to see us working on next:

  • Preview3 updates, whenever it becomes available.
  • Validations on the administrative side.
  • Bug fixes, of course. :-)
  • If you have suggestions for what you would like to see in the next release, please drop me a line and let me know!

You can download the new release from CodePlex.

Kick It on DotNetKicks.comShout It on DotNetShoutOuts.com

OpenID Check_Authentication In C#

// April 11th, 2008 // 12 Comments » // C#

Earlier this year Mads Krisensen (of BlogEngine.net fame) posted a lightweight implementation of OpenID using C#. In the comments on Mads’ post, Andrew Arnott (a developer of the DotNetOpenId library) mentioned that the example Mads had posted could "be hacked with a single change of a word in the URL." This is what is technically referred to as a Very Bad Thing. Andrew and another poster named "neil" went on to elaborate that implementing OpenID’s "check_authentication" algorithm would close this security hole. Unfortunately as of the writing of this article neither Mads nor any of the commentors have provided an implementation of check_authentication that works with the class Mads posted (bear in mind that Andrew only brought up this issue less than a week ago, so Mads may very well be working on it).

Fast forward to yesterday when I was researching my options for implementing OpenID for the next release of the ASP.Net MVC Membership Starter Kit. I liked Mads solution more than the other OpenID libraries that are current available because of its brevity and how easy it is to include it in a project without introducing an extra assembly dependency, so I decided to go ahead and add the check_authentication functionality. A quick read of that portion of the OpenID spec and a couple hours of coding/testing and I think I’m about finished.

Here is the method you need to add to Mads’ class:

   1: private static bool CheckAuthentication( NameValueCollection query )
   2: {
   3:  
   4:     //### get data required for check_authentication
   5:     string mode = "check_authentication";
   6:     string handle = query["openid.assoc_handle"];
   7:     string signature = query["openid.sig"];
   8:     string signed = query["openid.signed"];
   9:     string extra = string.Empty;
  10:  
  11:     //### loop through fields required by "openid.signed" and retrieve that data
  12:     if( !string.IsNullOrEmpty(signed) )
  13:     {
  14:         string[] exemptions = { "mode", "assoc_handle", "sig", "signed" };
  15:         string[] fields = signed.Split(',');
  16:         foreach( string field in fields )
  17:         {
  18:             if( exemptions.Contains(field) ) continue;
  19:             extra += string.Format( "openid.{0}={1}&", field, HttpUtility.UrlEncode( query[ "openid." + field ] ) );
  20:         }
  21:         extra = "&" + extra.Substring( 0, extra.Length - 1 );
  22:     }
  23:  
  24:     //### combine all the data together to form the request
  25:     string post = string.Format( "openid.mode={0}&openid.assoc_handle={1}&openid.sig={2}&openid.signed={3}{4}",
  26:         mode,
  27:         HttpUtility.UrlEncode( handle ),
  28:         HttpUtility.UrlEncode( signature ),
  29:         HttpUtility.UrlEncode( signed ),
  30:         extra
  31:     );
  32:  
  33:     //### begin sending request
  34:     HttpWebRequest request = (HttpWebRequest)WebRequest.Create( query["openid.op_endpoint"] );
  35:     request.Method = "POST";
  36:     request.ContentType = "application/x-www-form-urlencoded";
  37:     request.ContentLength = post.Length;
  38:  
  39:     //### transmit POST data
  40:     using( StreamWriter sw = new StreamWriter(request.GetRequestStream()) )
  41:         sw.Write(post);
  42:  
  43:     //### get response
  44:     string html = "";
  45:     using( HttpWebResponse response = (HttpWebResponse)request.GetResponse() )
  46:         using( StreamReader sr = new StreamReader( response.GetResponseStream() ) )
  47:             html = sr.ReadToEnd();
  48:  
  49:     //### determine if check_authentication passed or not
  50:     if( string.IsNullOrEmpty(html) || !html.StartsWith( "is_valid:" ) || html.StartsWith( "is_valid:false" ) )
  51:         return false;
  52:     else if( html.StartsWith( "is_valid:true" ) )
  53:         return true;
  54:     else
  55:         throw new InvalidOperationException( "Unexpected return from OpenID check_authentication." );
  56:  
  57: }

Now you need to make sure that method is called from somewhere. I chose to make the method private and just call it from inside the Authenticate method. Within the Authenticate method replace:

   1: // Make sure the incoming request's identity matches the one stored in session
   2: if( query["openid.claimed_id"] != data.Identity )
   3:     return data;

… with…

   1: // Make sure the incoming request's identity matches the one stored in session
   2: if( query["openid.claimed_id"] != data.Identity )
   3:     return data;
   4: else if( !CheckAuthentication( query ) )
   5:     throw new UnauthorizedAccessException( "OpenID False Verification Detected" );

And that’s it!

If anyone with more experience than I in OpenID waters sees a problem with my implementation, let me know and I will try to get it fixed quickly.

NOTE: I am aware that Mads’ implementation and the use of ‘check_authentication’ is considered an overly chatty use of OpenID. It seems to me that the extra complexity required to implement OpenID 2.0 protocol is just not worthwhile for most OpenID consumers. Feel free to let me know why this is a stupid position to take.

Kick It on DotNetKicks.comShout It on DotNetShoutOuts.com

Rob Conery’s PagedList Class (Updated)

// April 8th, 2008 // 13 Comments » // C#

NOTE:
A new, improved version of this class is now available at:

http://www.squaredroot.com/post/2008/07/08/PagedList-Strikes-Back.aspx

Robert Muehsig has posted a great user control for the MVC framework that adds pagination links to the bottom of a paged list. In it he used a slightly customized version of Rob Conery’s PagedList class that Rob was kind enough to post way back when the first CTP was released. This reminded me that I should probably post the version I have customized, as I think it makes it a bit easier to use and maintain. I’ve included the code below.

   1: using System;
   2: using System.Linq;
   3:  
   4: namespace System.Collections.Generic
   5: {
   6:  
   7:     public interface IPagedList
   8:     {
   9:         int TotalPages { get; }
  10:         int TotalCount { get; }
  11:         int PageIndex { get; }
  12:         int PageSize { get; }
  13:         bool HasPreviousPage { get; }
  14:         bool HasNextPage { get; }
  15:         bool IsFirstPage { get; }
  16:         bool IsLastPage { get; }
  17:     }
  18:  
  19:     public class PagedList<T> : List<T>, IPagedList
  20:     {
  21:  
  22:         public PagedList( IEnumerable<T> source, int index, int pageSize )
  23:         {
  24:  
  25:             //### set source to blank list if source is null to prevent exceptions
  26:             if( source == null )
  27:                 source = new List<T>();
  28:  
  29:             //### set properties
  30:             this.TotalCount = source.Count();
  31:             this.PageSize = pageSize;
  32:             this.PageIndex = index;
  33:             if( this.TotalCount > 0 )
  34:                 this.TotalPages = (int)Math.Ceiling( (double)this.TotalCount / (double)this.PageSize );
  35:             else
  36:                 this.TotalPages = 0;
  37:             this.HasPreviousPage = ( this.PageIndex > 1 );
  38:             this.HasNextPage = ( this.PageIndex < this.TotalPages );
  39:             this.IsFirstPage = ( this.PageIndex == 1 );
  40:             this.IsLastPage = ( this.PageIndex == this.TotalPages );
  41:  
  42:             //### argument checking
  43:             if( index < 1 || index > this.TotalPages )
  44:                 throw new ArgumentOutOfRangeException( "PageIndex out of range." );
  45:             if( pageSize < 1 )
  46:                 throw new ArgumentOutOfRangeException( "PageSize cannot be less than 1." );
  47:  
  48:             //### add items to internal list
  49:             if( this.TotalCount > 0 )
  50:                 this.AddRange( source.Skip( ( index - 1 ) * pageSize ).Take( pageSize ).ToList() );
  51:  
  52:         }
  53:  
  54:         public int TotalPages { get; private set; }
  55:         public int TotalCount { get; private set; }
  56:         public int PageIndex { get; private set; }
  57:         public int PageSize { get; private set; }
  58:         public bool HasPreviousPage { get; private set; }
  59:         public bool HasNextPage { get; private set; }
  60:         public bool IsFirstPage { get; private set; }
  61:         public bool IsLastPage { get; private set; }
  62:  
  63:     }
  64:  
  65:     public static class Pagination
  66:     {
  67:         public static PagedList<T> ToPagedList<T>( this IEnumerable<T> source, int index, int pageSize )
  68:         {
  69:             return new PagedList<T>( source, index, pageSize );
  70:         }
  71:     }
  72:  
  73: }

Changes from Rob’s version:

  • Added a "TotalPages" property.

    If you’re going to loop through each of the pages to display page navigation, you’ll obviously need this.
  • Changed "IsPreviousPage" to "HasPreviousPage".

    It just sounds better.
  • Changed "IsNextPage" to "HasNextPage".

    See above.
  • Added a "IsFirstPage" property.

    The opposite way of using the above two properties. I prefer this way, but kept the original way for backwards compatibility (except the naming).
  • Added a "IsLastPage" property.

    See above.
  • Changed the first constructor to accept IEnumerable<T> rather than IQueryable<T>.

    I’m not exactly sure why Rob originally made it IQueryable. I’m aware that by passing an IQueryable (LINQ) object to this constructor you’ll avoid retrieving the entire set (only taking the results needed), but since IQueryable inherits from IEnumerable everything should be hunky-dory. He probably had a reason and I’m going to wind up breaking all of my stuff, but IEnumerable is just so much handier. =)
  • Removed the second constructor.

    The second constructor took List<T>, which is unnecessary after changing the first constructor to accept IEnumerable.
  • Cleaned up property declarations a bit.

    Mainly to make the page a bit shorter, but also to prevent the multiple calculations that could happen in the original. Also the original allowed the changing of certain properties after an instance was created, which would put the instance into an inconsistent state.
  • Added argument checking and handled a few exception scenarios more gracefully.

    Trying to make debugging a bit friendlier.
  • Removed the second extension method that didn’t specify a pageSize.

    I don’t really think that baking in an extension method that sets pageSize to 10 is a good idea, I’d prefer pageSize to be explicitly set elsewhere by the calling code.
  • Moved the code to the "System.Collections.Generic" namespace.

    I’m sure a lot of you are breaking out in a cold sweat to see me putting something into a System.* namespace, but I kind of feel like this is something that the .Net team just "forgot". =) Move it wherever makes you comfortable.

Please note that I took many of these ideas from the commentary below Rob’s original post. I’m sure many of you are using something similar, but I thought it would be useful to get something posted online that is a bit more fleshed out than the original example.

Thanks for the great work Rob & Robert!

Kick It on DotNetKicks.comShout It on DotNetShoutOuts.com

Review: Training With Stephen Walther

// April 4th, 2008 // 1 Comment » // MVC

I just finished a four day course on AJAX, LINQ, & MVC taught by Stephen Walther and wanted to let all of you know that if you are interested in training on any of these topics, I HIGHLY recommend Stephen.

You can learn more about the training options he offers by visiting his training website:
http://www.superexperttraining.com/

He also keeps a blog filled with incredibly information — and long ;-) — posts on various aspects of ASP.Net at:
http://weblogs.asp.net/StephenWalther/

Read on for more details about my experiences this week.

The Instructor

Stephen is a successful author, having literally “written the book” on ASP.Net (and even ASP Classic!). His book “ASP.NET 3.5 Unleashed” has received excellent reviews from Amazon and I look forward to reading it soon.

You can buy Stephen’s book from Amazon here:

AspNet3Unleashed

We all found Stephen to be very personable and he obviously has an excellent command on the language and tools available in the .Net ecosystem. Also, anyone that can put up with my constant interruptions and derails for four days in a windowless classroom is clearly a pro. =)

The Course

Danny Douglass originally contacted Stephen to request that he teach a class for seven of our developers at the organization we work for. Stephen was kind enough to then customize a course around the technologies we had requested, and that is how we ended up with a one day primer on LINQ & MVC followed by an in-depth training session on AJAX technologies and Microsoft’s ASP.Net AJAX implementation specifically.

As anyone that reads my blog regularly knows, I am heavily invested in learning the MVC framework and was highly impressed by Stephens grasp on such a new technology. We may very well have been the first ASP.Net MVC class that anyone has taught anywhere and Stephen managed to teach it quickly and confidently. I was also impressed that he was teaching us things based upon the Preview 2 release of the MVC framework which has only been available publicly for 3 weeks rather than the older December CTP release.

As someone that considers himself a strong JavaScript developer (but weak at AJAX), I came into the class a little worried that the JavaScript explanation portions of the program would be more remedial than I wanted. I was wrong. Stephen led a thorough review over the advanced JavaScript features developers need to know to develop world-class AJAX libraries and did so while keeping the pace brisk and managing not to leave anyone in the room behind.

As I said above, I recommend Stephen’s training courses to anyone organization interested in learning AJAX, JavaScript, MVC, LINQ, or ASP.Net in general. Thanks for the excellent class Stephen!

Disclaimer: I was not paid or compensated in any way for this endorsement.

Kick It on DotNetKicks.comShout It on DotNetShoutOuts.com

MVC: Membership Starter Kit

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

A newer version of the Membership Starter Kit is now available. Click here to see what has changed.

Introduction

One of my very first blog posts (and most definitely my most popular so far) revolved around how to integrate ASP.Net membership and forms authentication into the ASP.Net MVC framework which had just been released in it’s December CTP flavor. It has remained popular to this day, but unfortunately the Preview 2 release of the MVC framework has caused much of the code I released in that article to no longer function correctly.

Even before the release of Preview 2, I had been planning to extend the samples I was providing to offer more useful features. I don’t know about you, but nearly every website I ever create with ASP.Net requires some kind of security/membership system. My preference is to use the built-in system when possible (except for the horrible Profiles sub-system). This means creating login, logout, & registration functionality every time, as well as creating administrative screens for managing the users that enter your system.

WebForms provides some controls to help with the login and registration process, but user administration has always been delegated to either (a) the built-in tool that runs separately and doesn’t work remotely or (b) rolling your own solution. The development of the MVC framework seems to me like a good time to resolve this scenario and provide the community with an array of pre-built tools to help boot-strap projects so that we can stop working on infrastructure and start working on the heart of the individual application.

With that in mind I have created a CodePlex project: the ASP.Net MVC Membership Starter Kit. It currently provides controllers and views for all of the common authentication and user administration needs, including:

  • Login/Logout
  • Registration
  • List of Registered Users
  • User Details / Administration
  • Role Management

A big thanks goes out to Rob Conery, as I borrowed his recent Authentication Filters and included those, along with my recently released Error Handling Filters.

Okay, enough wall o’text. Let’s take a look at some screenshots:

Screenshot Tour

Here is the menu when you first log in:

Menu-LoggedOut

The registration page:

Registration

The login page (note that the Administrator’s credentials are displayed to make it easy to get started, you’ll obviously want to change them and remove that note):

Login

Having logged in, here is what the menu looks like now:

Menu-LoggedIn

The current options upon clicking the Security tab:

Security

Clicking “Manage Users” brings you to a list of all users currently registered:

ManageUsers

Clicking on the user takes you to a form that allows you to view their details and edit a few aspects of their profile…

User-Top

as well as view/change the roles they are in and help with password issues:

User-Bottom

From the Security tab you can also go to a list of all the system’s roles, from which you can add/delete roles as you need:

Roles

Clicking on a role from the roles list or the user profile allows you to view/modify the users in that role:

UsersInRole

And finally, the Security tab offers a link to another registration form, geared toward Administrators:

CreateUser

NSFAQ (Not-So Frequently Asked Questions)

Q: Does this compete with MvcContrib?
A: No. MvcContrib is a great project that aims to features like alternate routing and view engines, and IoC integration. This project is simply a starter kit to help get membership-based applications off the ground a little quicker. There is no reason you could not use both projects together.

Q: What dependencies does using this starter kit saddle me with?
A: Ideally, none, other than the ASP.Net Membership API. Every measure will be taken to avoid using third party libraries, be it JavaScript or .Net. We will strive to separate the code and rely on default settings as much as possible to make it easy for you to customize your installation without anything in the starter kit getting in the way.

Q: Who is responsible for this?
A: Currently, only me, but I’d like that to change! I’m keenly interested in finding a few other developers that would like to contribute to enhancing the starter kit. Please contact me if you’d like to help!

What’s Next?

There are a few features missing that I would like to include in the very short term. Primarily these are features for end-users, like Change Password and Forgot My Password (both of which are currently available on the administration side). Beyond that, visual cleanup of the forms (and separation of the style sheets) as well as a bit of AJAX-ification of the forms would be nice. If you use the starter kit and have suggestions or criticisms, please post to the Discussions forum or Issue Tracker!

You can download the latest release from the CodePlex project. I hope you all find it useful!

Kick It on DotNetKicks.comShout It on DotNetShoutOuts.com

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: How Do I RedirectToAction While Passing a Parameter?

// April 1st, 2008 // 4 Comments » // MVC

A common scenario that seems underdocumented to me in the latest MVC release, is how does one use RedirectToAction while passing a value to that action?

In the December CTP, you would do this:

RedirectToAction( new{
     controller = "Blah",
     action = "Blah",
     foo = "Bar"
} );

The new syntax is close, but not easily discoverable. Instead of the above, from now on you should use:

RedirectToAction( new RouteValueDictionary(
     new{
          controller = "Blah",
          action = "Blah",
          foo = "Bar"
     }
));

Kick It on DotNetKicks.comShout It on DotNetShoutOuts.com