Retrofit ASP.NET Aspx Urls with System.Web.Routing
asp.net, programming, routing, web July 8th, 2009Here’s a sample of a technique that might be of general interest. It came up retrofitting a particular url structure onto an existing ASP.NET application originally built with the Web Client Software Factory. I came into the project fairly late in the game and the ROI for migrating to MVC would be nearly zero, so instead a technique was used which welds System.Web.Routing onto a web forms app in a fairly transparent fashion.

Putting Lipstick on a Web Site
To start with there are a few ground rules and goals to establish. One is that a 301 permanent redirect is desirable from older urls onto new routed urls. Another is that not all urls in the app need to be fixed “in place” in the original html response assuming the 301 redirects will keep the resulting urls canonical. Third is that it’s highly desirable to be able to calculate the new route given an old url server-side, for such things as the site map and updating the links on pages which are particularly significant.
Another consideration is that if a site is currently using Request.QueryString values and relies on postback, both of those should continue to work transparently. A final consideration is the range of possible values in a piece of the route could be driven by a database or source which is non-static or has a huge number of possible distinct values. At least a number too large for adding multiple routes or combining into regex restriction.
Downloads: retrofit.zip source code and sample project.
It turns out the System.Web.Routing module is particularly well suited for this, especially because of the bidirectional nature of it’s routes. The attached solution has a sample web app and a class library named Retrofit.Routing which provides an example of using System.Web.Routing in this way.
First of all is the class Retrofit.Routing.WebFormsRouteHandler which implements IRouteHandler. You’ll find in it’s implementation the method GetHttpHandler which will use BuildManager.CreateInstanceFromVirtualPath to obtain an instance of the targetted web page. The web page is targetted with the RouteData Value parameter named “VirtualPath”, much like in an MVC app the controller is targetted with a RouteData Value named “Controller”. The other thing this class does is call HttpContext.RewritePath to extend the number of values present on the QueryString.
You won’t use the WebFormsRouteHandler directly, instead you’ll create an instance of WebFormsRoute. This WebFormsRoute is a descendant of the standard Route class and is used in almost exactly the same way.
public static void RegisterRoutes(RouteCollection routes)
{
// enable routing of existing files so WebFormsRoute has the chance redirect old urls
routes.RouteExistingFiles = true;
// matches customer with a data-driven constraint
routes.Add(new WebFormsRoute(
"Customer/{*CompanyName}",
new RouteValueDictionary(new {VirtualPath = "~/Contacts/Customer.aspx"}),
new RouteValueDictionary(new {CompanyName = new ValidCompanyNameConstraint()})
));
// matches orders requests
routes.Add(new WebFormsRoute(
"Customer/Orders/{OrderID}",
new RouteValueDictionary(new {VirtualPath = "~/Orders/Details.aspx"})
));
}
As we can see defaults can be provided, which is how the value for VirtualPath is defaulted in when a request pattern is matched. The values like {CompanyName} and {OrderID} which are matched above will become CompanyName= and OrderID= querystring parameters passed to the aspx page as it runs, so the code behind does not need to be modified for those cases.
There’s even an example above of how you would provide an IRouteConstraint class if you would like to dynamically restrict the set of urls a route is allowed to match.
public class ValidCompanyNameConstraint : IRouteConstraint
{
public ValidCompanyNameConstraint()
{
// pre-load valid company names
// in a real app this would be queries each call to match,
// or it would be re-cached when the data had changed.
var data = new NorthwindDataContext();
CachedNames = data.Customers.ToDictionary(x => x.CompanyName, x => x.CustomerID);
}
private Dictionary<string, string> CachedNames { get; set; }
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
var parameterValue = Convert.ToString(values[parameterName]);
return CachedNames.ContainsKey(parameterValue);
}
}
It’s a contrived example, but it is just a sample after all.
And the WebFormsRoute is performing one other task related to the need for 301 redirects. As requests are coming in it’s not only matching the request against the new url pattern, it’s also testing the request url against the old virtual path and query string parameters needed.
// do the work to check if the incoming request could also produce
// a meaningful route to redirect towards
var probeVirtualPathData = TestVirtualPath(httpContext, virtualPath);
if (probeVirtualPathData == null)
return null;
// the request is on a page which this route controls, 301 redirect
// onto the correct location
return new RouteData(this, new PermanentRewriteHandler(probeVirtualPathData.VirtualPath));
}
If the incoming request is match for the old url structure then it takes advantage System.Web.Routing’s ability to calculate the correct url in the new pattern. This new url is used as the parameter of a handler which will return a 301 permanent redirect to keep the browser’s address bar clean-looking and to ensure web crawlers are indexing the site with at this single, canonical location.
The rewrite handler itself is a pretty simple device.
public class PermanentRewriteHandler : IRouteHandler, IHttpHandler
{
private readonly string _path;
public PermanentRewriteHandler(string path)
{
_path = path;
}
public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
return this;
}
public bool IsReusable
{
get { return false; }
}
public void ProcessRequest(HttpContext context)
{
context.Response.Buffer = true;
context.Response.Redirect("~/" + _path, false);
context.Response.StatusCode = 301;
context.Response.StatusDescription = "Moved Permanently";
}
}
At this point nearly all of the goals are met. You can route incoming requests on new urls onto existing WebForms pages, it will 301 redirect incoming old urls onto the new location, the route values are converted to querystring values transparently, and POST to the WebForms life-cycle will pass through unimpeded on the old or new urls.
The final piece of the puzzle is the need to return the new urls in links returned from pages on the server without relying on the 301 redirects. That’s done easily enough with an extension method Page.ResolveRouteUrl that is used in the same way as Page.ResolveUrl.
public static string ResolveRouteUrl(this Page page, string relativeUrl)
{
var url = relativeUrl;
var virtualPathData = GetVirtualPathData(relativeUrl);
if (virtualPathData != null)
{
url = "~/" + virtualPathData.VirtualPath;
}
return page.ResolveUrl(url);
}
And it’s used like this:
Company: <asp:HyperLink ID="HyperLink1" runat="server"
NavigateUrl='<%# Page.ResolveRouteUrl("~/Contacts/Customer.aspx?CompanyName=" + Eval("CompanyName")) %>' Text='<%# Eval("CompanyName") %>'></asp:HyperLink>
Downloads: retrofit.zip source code and sample project.
July 8th, 2009 at 9:34 am
Smooth. Good work. *bookmarked* :)
July 9th, 2009 at 9:26 am
[...] Retrofit ASP.NET Aspx Urls with System.Web.Routing (Louis DeJardin) [...]
July 16th, 2009 at 3:45 am
Hi yeah Would you please explain a little more about permamenet redirect. i mean what type of url that will be redirected to new URL. How to reach to that statement
RouteData(this, new PermanentRewriteHandler(probeVirtualPathData.VirtualPath));
July 22nd, 2009 at 3:31 am
Hi there kindly get back to me please
Hi yeah Would you please explain a little more about permamenet redirect. if my old link was wwww.host/oldlink.aspx?s=2 and now i want thatlink to be permamenet redirect to wwww.host/newlink what rule should i use using ur rouing model
July 28th, 2009 at 10:48 am
Hi, Salman! Sorry for the delayed response. I’ve had some non-stop vacation problems.
If the newlink is a constant value you could say use the route url “newlink” and provide the defaults new {VirtualPath=”~/oldlink.aspx”, s=”2″}
That way the request /newlink will be converted to /oldlink.aspx?s=2 and if a request /oldlink.aspx?s=2 came in it would be 301 directed back to /newlink.
But you’re probably talking about a case where the “newlink” and “2″ will vary. The problem with that is the routing is mostly helpful to transform between two path forms, but doesn’t help very much if you’re trying to augment or derive information at the same time.
For example if you had a querystring parameter, call it ‘u’, that held the new label you could put it into the url…
/oldlink.aspx?s=2&u=newlabel
then say a route url like “{s}/{u}” would mean an incoming request for /2/newlabel would execute as /oldlink.aspx?s=2&u=newlabel - and an incoming request for /oldlink.aspx?s=2&u=newlabel would 301 redirect to /2/newlabel
But that’s not probably as helpful as you were looking for. I’m guessing what you’d need in the case you have is more of a plugin type of parameter that could add or remove the routevalue fields for a request, not just recognize them where they occur.
July 31st, 2009 at 9:33 am
thanks for your messsage. howveer i am still little confused all i want to know under your function GetRouteData there is a statement return new RouteData(this, new PermanentRewriteHandler(probeVirtualPathData.VirtualPath));. How can i get to this statement what type of rule i should add in global.asax or what type of URL should be in order to reachthis statement thank you
August 11th, 2009 at 9:06 pm
test to check validation
August 13th, 2009 at 9:42 am
Where’s Lou ? for my July 31st, 2009 at 9:33 am thanks :)
August 28th, 2009 at 9:06 am
routes.Add(new WebFormsRoute(
“Customer/{*CompanyName}”,
new RouteValueDictionary(new {VirtualPath = “~/Contacts/Customer.aspx”}),
new RouteValueDictionary(new {CompanyName = new ValidCompanyNameConstraint()})));
this one redirects the old link to new link. But in your WebFormsRoute.cs line no 65-67 which is
var virtualPath = httpContext.Request.AppRelativeCurrentExecutionFilePath;
if (string.Equals(DefaultVirtualPath, virtualPath, StringComparison.InvariantCultureIgnoreCase) == false)
return null;
checks for the same pages. Means virtual path and defaultpath has to be same to permanent redirect.
But what if we want to redirect from anotherpage to new page which are not same. And i know URLS as well. I can simple write a small code for it but was just curious if you routing give solution to that
November 3rd, 2009 at 3:19 pm
Well done! I really like the way you build the extended query string, rewrite, and then swap the old one back in prerender. It’s great being able to keep the original pages ignorant of the RouteData.