ASP.NET MVC Permanent 301 redirects for legacy routes

Having a good url routing scheme is extremely important when developing an application. Urls should be canonical to aid in search engine optimization and discoverable in order to aid users in learning where to find application functionality for particular tasks. But there are points in an applications lifecycle where one wishes to change the url routing scheme in order to employ a better one. In such instances, URL redirection can be used to preserve search engine rankings, user bookmarks to specific pages or to allow more than one URL to serve up the same content, as is the case when employing URL shortening.

ASP.NET MVC has the RedirectResult to perform a 302 temporarily moved redirection response from one URL to another, but it does not have any built in way to handle permanent "301 moved permanently" redirections. This is the topic of today's post.

Dude, Where's my Content?

Using an ActionResult to perform redirections works great for the Post-Redirect-Get pattern in conjunction with 302 temporary redirections, but doesn't fit particularly well for 301 permanent redirections. In the latter case, it would be better if we could let our routing handle this for us and not require a controller action in conjunction with a route as exists in the former case. For permanent redirections then, let's define a LegacyRoute:

We inherit from Route and override GetVirtualPath, a method called when generating outbound URLs. Since we don't ever want legacy routes to take part in outbound URL generation, we simply return null. The plan is to register the legacy routes before other more generic routes, so we don't want legacy routes providing a match for a URL pattern over other routes.

Now that we have a route class to use, an IRouteHandler is also needed to process the request for a URL pattern matched by a legacy route. Enter LegacyRouteHandler:

When the LegacyRouteHandler is instantiated, it is passed route values in the constructor. These will be used when GetHttpHandler is called to construct the new URL that the old URL should be redirected to. An absolute URL (technically, URI) is constructed and passed to a RedirectHandler which will be used to set the Location HTTP Header on the response. Whilst most web browsers will support a relative URI, RFC 2616 mandates that the URI should be absolute. The RedirectHandler looks like so:

RedirectHandler implements IHttpHandler. When the ProcessRequest method is called by the framework, the handler uses the absolute URL passed to it in the constructor to set the Location HTTP header on the response and returns a 301 HTTP status code with accompanying status message.

With the classes defined above, one can start implementing redirections for legacy routes immediately using the following inside of RegisterRoutes(RouteCollection routes) in Global.asax.cs

The above will permanently redirect requests to /old-route to /Home/Index (well, in fact they would redirect to / as the HomeController and Index action are the default values of the Default route).

What about Areas?

We can see above that areas get registered before RegisterRoutes is called. This means that areas have the opportunity to register routes before the main application does. Since the order of routes in the application's RouteTable is important for route matching (a top to bottom operation that stops as soon as a match is found), we would want to register any legacy routes for URLs that may be matched by generic routes registered by areas before those generic routes. The following AreaRegistrationContext extension methods can be used for this purpose

The above will redirect the URL /Account/Home/old-route to /Home/Index (which, if using the routing in Global.asax.cs defined above would route to /).

If you're using T4MVC (which I highly recommend you do) then you can replace all of the magic string properties with the generated class properties inside of the anonymous objects passed as route values in MapLegacyRoute. One could go even further and define extension methods that take an ActionResult instead of a route values object, much like the way T4MVC overloads the common HtmlHelper methods.