Skip Navigation LinksHome > View Post

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:

http://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:

http://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 http://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.

Tags: MVC ASP.NET

 
Josh Post By Josh Twist
12:53 PM
01 Sep 2010

» Next Post: Taming ClickOnce – taking charge of updates
« Previous Post: Changing the size of a column in SQL Server Management Studio

Comments are closed for this post.

Posted by valhallasw @ 01 Sep 2010 1:10 PM
Wouldn't it be better to change the .aspx version into an HTTP 301 redirect? In the current situation a search engine would index two pages, although they have the same content.

Posted by Josh @ 01 Sep 2010 1:50 PM
Great point - this is a work in progress (you're still looking at the web forms site) so I'll absolutely work that in.

Any recommendations on how you'd do this? With a specific controller and action that Redirects to the correct action?

Posted by Rab @ 02 Sep 2010 4:43 PM
I use an action to redirect my legacy URLs - unfortunately the RedirectResult returns a 302 which is no use, so I currently use something like this -

public virtual ActionResult DisplayPostAspx(string linkTitle)
{
Response.StatusCode = 301;
Response.RedirectLocation = Url.Action("Post", "Display", new { linkTitle = linkTitle });
Response.End();

return null;
}

Someone might know a nicer, more MVCy way of doing this ;)

Posted by Stuart @ 03 Sep 2010 6:42 PM
I posted a few thoughts: http://blogs.msdn.com/b/stuartleeks/archive/2010/09/03/backwards-compatibility-and-and-preserving-search-engine-ranking.aspx

© 2005 - 2014 Josh Twist - All Rights Reserved.