I'm creating URL aliasing functionality; the ability to map resource A to resource B.
I'm fitting this into a project with a catch-all URL mapped to a custom Route that parses the incoming URL string and looks up the URL parts inside our database. Basically, the entire URL string is dynamic and maps to a category hierarchy (e.g. /tyres/car /tyres/van, /suspension/ford/focus). This is all working fine, and only exists to route to one controller.
Currently I'm integrating my code for URL aliasing into this Route, since we're currently only allowing aliasing of these dynamic routes. It looks up the incoming URL to our database to see if an alias exists, and then grabs the original URL it's mapped to and passes that string to our URL parser. This feels dirty, separation of concerns and all.
I think a better approach would be two separate routes mapped. One, URLAliasRoute, and the other CustomWebsiteRoute. Is it possible for a Route to affect the URL that other routes will see?
So, imagine I have the following routes mapped:
UrlAliasRoute aliasRoute = new UrlAliasRoute("{*url}");
routes.Add(aliasRoute );
CustomWebsiteRoute customRoute = new CustomWebsiteRoute("{*url}");
routes.Add(customRoute );
routes.MapRoute("Contact Us", "contact", new { controller = "Contact" });
routes.MapRoute("About Us", "about", new { controller = "About" });
//etc
and the URL car/tyres aliased to car-tyres
So when an incoming request for /car-tyres comes in, my UrlAliasRoute will look it up in the database, see that is has a mapping for "car/tyres", and then changes the RouteData URL from "car-tyres" to "car/tyres". All other Routes are then using car/tyres as the URL to match against the Route constraints?
This would also have the benefit of allowing for aliasing of any predefined routes that developers have coded...potentially dangerous but something I'll be careful about.
Seems like a nice solution, but I've been struggling to find how to change the URL that other routes will receive after GetRouteData() is called.
I generally use a method such as below. Firstly within Routeconfig near the top I register a new Routebase (called LegacyURLRoute):
routes.Add(new LegacyUrlRoute());
A cut down version of this as follows:
public class LegacyUrlRoute : RouteBase
{
public override RouteData GetRouteData(HttpContextBase httpContext)
{
var request = httpContext.Request;
var response = httpContext.Response;
var legacyUrl = request.Url.ToString().ToLower();
var legacyPath = request.Path.ToString().ToLower();
Tuple<bool, bool, string> result = DervieNewURL(legacyPath);
bool urlMatch = result.Item1;
bool urlMatchGone = result.Item2;
var newUrl = result.Item3;
if (urlMatch)
{//For 301 Moved Permanently
response.Clear();
response.StatusCode = (int)System.Net.HttpStatusCode.MovedPermanently;
response.RedirectLocation = "http://www.example.com" + newUrl;
response.End();
}
else if (urlMatchGone)
{// 410 Gone
response.Clear();
response.StatusCode = (int)System.Net.HttpStatusCode.Gone;
response.End();
}
return null;
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
return null;
}
}
The DervieNewURL above can then include the lookup in the DB, but recently on a project I have had that as a HTMLHelper that also allows me to pass in the URL's that come directly from a DB column and these can then be parsed and updated as appropriate.
Simple example of DervieNewURL could be as follows, but you would obviously lookup in a table rather than having bits hard coded as below.
public static Tuple<bool, bool, string> DervieNewURL(string url)
{
/*
return type from this function is as follows:
Tuple<bool1, bool2, string1, string2>
bool1 = indicates if we have a replacement url mapped
bool2 = indicates if we have a url marked as gone (410)
string1 = indicates replacement url
*/
Tuple<bool, bool, string> result = new Tuple<bool, bool, string>(false, false, null);
if (!String.IsNullOrWhiteSpace(url))
{
string newUrl = null;
bool urlMatch = false;
bool urlMatchGone = false;
switch (url.ToLower())
{
case "/badfoldergone/default.aspx": { urlMatchGone = true; } break;
case "/oldfoldertoredirect/default.aspx": { urlMatch = true; newUrl = "/somecontroller/someaction"; } break;
default: { } break;
}
result = new Tuple<bool, bool, string>(urlMatch, urlMatchGone, newUrl);
}
return result;
}
If you require wildcard matches then I guess you could amend the above or build this into the DB call matching criteria.
By employing this route early on you can redirect legacy url's to other routes that will then be triggered as the request cascades down the routing table. Ultimately you will end up at your Default route similar to:
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
If developers have hard coded links then you will just need employ routes that will be triggered by these url's. By employing something such as above can capture most early on and add to the list in your DB with appropriate 301 Moved or 410 gone if required.