Brandon Satrom (gravatar)

Handling Broken Links With ASP.NET Routing

One of the unfortunate realities of migrating your blog from one engine to another is that you run the risk of leaving a trail of broken links from Google and other external sources if your new engine has a different way of generating URLs for your content than your old engine did. Most do and, as a result, your new engine often gives you the ability to customize your new scheme to a legacy scheme, and things are seamless. Wordpress does this with an admin screen that specifies five options for creating Permalinks to your site, as shown in the screen below.

 

Wordpress Permalink Menu

So if your old blog used a /<year>/<month>/<post> scheme, your new one can too.

But what if you like the new engine’s scheme and you want to use it going forward? You can’t very well serve broken links to people--that would break the internet--so you need to find a way to both leverage the new routing scheme and preserve legacy URLs from external sources.

 

Enter ASP.NET Routing

Routing is a new feature for ASP.NET that was released in 3.5 SP1 after the MVC team realized that its benefit extended well beyond MVC applications. So, while used most visibly in ASP.NET MVC applications, routing is available to leverage in forms applications as well.

From the MSDN Article (emphasis mine):

“ASP.NET routing enables you to use URLs that do not have to map to specific files in a Web site. Because the URL does not have to map to a file, you can use URLs in a Web application that are descriptive of the user's action and therefore more easily understood by users.

In ASP.NET routing, you define URL patterns that contain placeholders for values that are used when you handle URL requests. At run time, the pieces of the URL that follow the application name are parsed into discrete values, based on a URL pattern that you have defined. For example, in the request for http://server/application/Products/show/beverages, the routing parser can pass the values Products, show, and beverages to a handler for the request. In contrast, in a request that is not managed by URL routing, the /Products/show/beverages fragment would be interpreted as the path of a file in the application.”

So, rather than /products/item.aspx?id=2, you can use /products/item/2 or /products/sprocket. In fact, you can use both to route to the same location or action.

(Aside: If you’re thinking, “this sounds similar to what I already have in PHP and Rails,” you would be right. The platform is evolving and taking on new ideas all the time. Now come back to .NET, you know you miss it here.)

 

Routing and ASP.NET MVC

Bottom line: Routing enables me to define URL patterns for my site and map those patterns to an action that responds to the request. In MVC, which responds to requests rather than events (as ASP.NET Forms does), this means that routing enables me to define URL patterns that map to actions on my controllers and which provide data that the controller needs to pass a model to my views. In the context of MVC, routing is the transit system that ensures that the right Controller and the right Action are found every time.

(Aside two: I’m not going to dive much more into what routing is and will instead show how I’m using into to solve the problem of preserving links after content migration. If you’d like to know more about routing itself, I’d recommend the MSDN article and the “Routes and URLs” chapter in the Wrox Professional ASP.NET MVC 1.0 book.)

 

What This Means for Content

In my mind, a URL is not just a location of a resource. It’s also:

1) A contract with the user that the thing of value you expect to find is on the other end of this link. A broken link is a broken contract with the user, and should be avoided at all costs. (a rule I’ve broken myself)

2) A key component of the web User Interface (and User Experience). Meaning that a URL like http://awesomesite.com/page?id=0432drtG56689MdRR is no better than presenting the user with this:

wgetgui-screenshot

The URL is part of the user’s interaction with your site, and it should be designed with the same care as the site layout itself.

The good news is that Routing meets both of these needs nicely.

 

So I Moved My Blog and Broke Some Links

As I mentioned in my last post, after five years of running this blog on Wordpress, I moved to Oxite, an Open Source content engine built on ASP.NET MVC. I did this knowing that links like this:

http://www.userinexperience.com/2009/02/16/debugging-workflows-wf-tracing-when-nothing-else-works-try-this

Would break as Oxite would use this default URL instead:

http://www.userinexperience.com/Blog/debugging-workflows-wf-tracing-when-nothing-else-works-try-this

With routing, the solution is simple. I simply add something like this to my site RouteCollection (note that MapRoute is an MVC-specific implementation of ASP.NET Routing. Default routing is typically called through the routes.Add() method):

 

   1:  routes.MapRoute(
   2:     "LegacyUrl",                                  // Route name
   3:     "{year}/{month}/{day}/{postSlug}",            // URL with parameters
   4:     new { controller = "Post", action = "Item"}   // Parameter defaults
   5:  );

Using this pattern, all of my broken links are handled and routed properly (provided that my postSlug value is the same), while giving me the flexibility of new URL patterns going forward.

 

Making It Work In Oxite

The code above won’t work in Oxite, though. If you want to do something similar in the Alpha version of Oxite (where they are using ASP.NET Routing and not MVC routing), you can add the following to OxiteRoutes.cs:

   1:  AddRoute(
   2:     "LegacyUrl",
   3:     "{year}/{month}/{day}/{slug}/{dataFormat}",
   4:     new { areaName = "Blog", controller = "Post", action = "Item", dataFormat = "" },
   5:     new { dataformat = "(|RSS|ATOM)" },
   6:     new { Namespaces = controllerNamespaces }
   7:  ); 

AddRoute is a private method that calls the routes.Add() method.

If you’re working with a more-recent version of Oxite, you’ll notice that the engine now uses MVC routing. As such, you can add the following to OxiteRegisterRoutes.cs:

 

   1:  MapRoute(
   2:     "LegacyUrl",
   3:     "{year}/{month}/{day}/{postSlug}",
   4:     new { areaName = "Blog", controller = "Post", action = "Item", dataFormat = "" },
   5:     new { areaName = new AreaConstraint(container) },
   6:     controllerNamespaces
   7:  ); 

 

So It Works, But Can It Be Better?

So problem solved, right? Well, yes, but in the context of Oxite, I’ve made a engine-behavior change to resolve an implementation-specific issue. Not everyone needs to route legacy URLs, and even those who do may or may not need to route those URLs using the exact same pattern that I need. Some might need a completely different scheme altogether.

So this really is no long-term solution. A better solution is a Routing Plugin for Oxite that allows a site administrator to create routes declaratively, using one or more canned options, or defining their own as needed. This would enable both Legacy URL support, and support for additional patterns (vanity URLs, etc).

That’s something I’ll be working on in Oxite soon, and I’ll make the source available here as I’m working on it.

1 Comment

Your Information
Mrs. Gravatar (gravatar)

<-- It's a gravatar

your comment