Learning MVC: Backwards compatible routes

I’m lucky enough to have sneaked on to some MVC training created and hosted by my talented colleagues Simon Ince and Stuart Leeks. For some time now I’ve threatened to migrate theJoyOfCode.com blogging engine from ASP.NET WebForms to MVC and this is the perfect excuse to make this happen. As always, the whole idea of writing and hosting my own blogging engine is one of learning.

One of the great benefits of the MVC approach to web applications is friendly URLs. It’s now easy to build sites that take parameters from URLs in the form /Posts/Archives/2010/08/ as opposed to the somewhat more obtuse /Posts/Archive.aspx?year=2010&month=08 without authoring a whole bunch of URL rewriting gubbins.

Here on theJoyOfCode.com we have such URL rewriting taking place which creates our ‘friendly’ URLs of the form:

https://www.thejoyofcode.com/ {PostLinkTitle} .aspx

Of course, because the blog was initially targeting IIS version 6 and there was no Integrated Pipeline it was preferable to terminte the url with an extension that would automaticaly be handled by ASP.NET (e.g. .aspx). Now, we’re running on IIS 7 (definitely the preferred choice for anyone looking to use MVC) we can create an even more friendly URL, something like:

https://www.thejoyofcode.com/ {PostLinkTitle}

Life-changing, I’m sure you’ll agree. Setting this up in MVC really is a doddle. We just add a route (inside RegisterRoutes() in the global.asax) as follows:

 routes.MapRoute(
    "DisplayPost",
    "{linkTitle}",
    new { 
        controller = "Post", 
        action = "Display", 
        linkTitle = UrlParameter.Optional },
    );

Of course, when updating my blog I need to be very careful to maintain compatibility with any existing links that are already out there pointing at my site. This means I need to support both types of ‘friendly’ URLs. At first glance, this seems pretty easy in MVC too. We can add a second route that maps to the same controller and action but adds the .aspx suffix:

 routes.MapRoute(
    "DisplayPostAspx",
    "{linkTitle}.aspx",
    new { 
        controller = "Post", 
        action = "Display", 
        linkTitle = UrlParameter.Optional }
    );

It’s important that this second route “DisplayPostAspx” is added after the previous route “DisplayPost” so that any generated action links use the new, more friendly URL format.

However, the problem we’d see here is that the “DisplayPostAspx” route would never be hit – even if the URL ended with ‘.aspx’. What would happen in practice is our “DisplayPost” route would receive the full linkTitle including the ‘.aspx’ extension as part of the linkTitle parameter. D’oh! Therefore we need to find a way to explicitly exclude the “DiplayPost” route from handling URLs that end in ‘.aspx’.

Stuart was kind enough to help me solve this problem by introducing me to the seductively simple IRouteConstraint interface. Meet our simple implementation:

 private class NotAspxExtensionConstraint : IRouteConstraint
{
    public bool Match(
        HttpContextBase httpContext, 
        Route route, 
        string parameterName, 
        RouteValueDictionary values, 
        RouteDirection routeDirection)
    {
        string value = (string) values[parameterName];
        return !value.EndsWith(
              ".aspx", 
              StringComparison.InvariantCultureIgnoreCase);
    }
}

As you can see, the NotAspxExtentionConstraint.Match returns false if the ‘parameterName’ route value ends with .aspx. How do we apply this to the first “DisplayPost” route and how does it know what the ‘parameterName’ is?

 routes.MapRoute(
    "DisplayPost",
    "{linkTitle}",
    new { 
        controller = "Post", 
        action = "Display", 
        linkTitle = UrlParameter.Optional },
    new { linkTitle = new NotAspxExtensionConstraint() }
    );

Nice. The last piece of the puzzle is the homepage. If a user hits https://www.thejoyofcode.com/ our “DisplayPost” route will match with an empty linkTitle parameter and the incorrect controller and action will be invoked. For this instance, we want to use our HomeController and Index action. Again, easily solved by adding a new Route before both “DisplayPost” and “DisplayPostAspx”:

 routes.MapRoute(
    "Home",
    "",
    new { controller = "Home", action = "Index" }
    );

Thanks to Stuart and Simon for an excellent two days training.

Originally posted by Josh Twist on 1st September 2010 here: https://www.thejoyofcode.com/Learning_MVC_Backwards_compatible_routes.aspx