Implementing a custom route priority order
ASP.NET, MVC, Routing July 31st, 2008Yesterday, I blogged about implementing MonoRail-style application areas in ASP.NET MVC. One of the goals was to have different controllers with the same name (in different namespaces, obviously), and enhance the routing system to cope with that. So, when generating outbound URLs (e.g. with Html.ActionLink()), it should “prioritize” entries matching the current controller’s namespace.
Fundamentally, this comes down to “can you implement a custom route priority order”? At first, I thought the answer was “no”: you can’t override RouteTable.Routes.GetVirtualPath(), so you’re stuck with its system of just starting from the top of the list and scanning downwards. That makes it hard to introduce any notion of context (e.g. current namespace context) into URL generation. I did come up with a solution but it was a bit nasty. I complained that the current routing system wasn’t extensible enough.
A cleaner solution
While driving to work this morning (yes, I should be working right now) it hit me: The current routing system is perfectly extensible enough. In fact, this is dead easy. All you need to do is create a “pseudo” route entry that sits at the top of the list, and directs the action from there. Sweet!
So, let’s define an abstract base class for a “custom priority order”:
public abstract class CustomPriorityOrder : RouteBase { protected abstract IEnumerable<RouteBase> RoutesInPriorityOrder(RouteCollection routes, RequestContext requestContext, RouteValueDictionary values); public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values) { using (RouteTable.Routes.GetReadLock()) { foreach (var route in RoutesInPriorityOrder(RouteTable.Routes, requestContext, values)) { var vpd = route.GetVirtualPath(requestContext, values); if (vpd != null) return vpd; } } return null; // Didn't pick any entry, so revert to the traditional priority order } public override RouteData GetRouteData(HttpContextBase httpContext) { return null; } }
Now, you can implement your own subclass. For example, to give priority to route entries matching the current controller’s namespace:
private class PrioritizeRoutesMatchingCurrentRequestNamespace : CustomPriorityOrder { protected override IEnumerable<RouteBase> RoutesInPriorityOrder(RouteCollection routes, RequestContext requestContext, RouteValueDictionary values) { IController currentController = GetCurrentController(requestContext); if (currentController != null) foreach (var route in routes.OfType<Route>().Where(r => RouteMatchesControllerNamespace(r, currentController))) yield return route; } private IController GetCurrentController(RequestContext requestContext) { var controllerContext = requestContext as ControllerContext; return controllerContext == null ? null : controllerContext.Controller; } private bool RouteMatchesControllerNamespace(Route route, IController controller) { if (route.DataTokens != null) { var namespaces = route.DataTokens["namespaces"] as IEnumerable<string>; if (namespaces != null) foreach (string ns in namespaces) if (controller.GetType().FullName.StartsWith(ns + ".")) return true; } return false; } }
It’s still a moderately big hunk of code, but much tidier than before. Now, to use this, all you have to do is drop a PrioritizeRoutesMatchingCurrentRequestNamespace instance at the top of your route table, e.g.:
public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); // Here it is! routes.Add(new PrioritizeRoutesMatchingCurrentRequestNamespace()); routes.Add(new Route("blog/{controller}/{action}/{id}", new MvcRouteHandler()) { Defaults = new RouteValueDictionary(new { controller = "Home", action = "Index", id = "" }), DataTokens = new RouteValueDictionary(new { namespaces = new[] { "MyApp.Controllers.Blog" } }) }); routes.Add(new Route("calendar/{controller}/{action}/{id}", new MvcRouteHandler()) { Defaults = new RouteValueDictionary(new { controller = "Home", action = "Index", id = "" }), DataTokens = new RouteValueDictionary(new { namespaces = new[] { "MyApp.Controllers.Calendar" } }) }); }
The rest of your routing configuration is unchanged (you can now use standard Route objects everywhere else). I think this is a reasonably tidy way to implement a custom route priority order. However, I’m still not convinced that anyone really needs to do this. It’s just nice to know that you could if a really valid scenario emerged.
Well done to the System.Web.Routing designers for making extensibility work nicely


July 31st, 2008 at 1:32 pm
Haven’t got to try it, but it looks pretty clean. Thanks for looking into this, I was wondering what would happen if you needed two controllers by the same name!
August 1st, 2008 at 12:14 pm
I know you’re using strings for the routing anyway, but would it not be better to use a helper function to get namespaces?
Something like:
public static string NamespaceOf<T> ()
{
return typeof ( T ).Namespace;
}
August 1st, 2008 at 12:15 pm
Sorry, nearly got that right:
public static string NamespaceOf<T> ()
{
return typeof ( T ).Namespace;
}
(never know what HTML blog comments will accept!)
August 1st, 2008 at 2:13 pm
@David: Yes, you probably could improve the code somehow along those lines. It’s a bit difficult to call a generic method (e.g. NamespaceOf<T>()) when you don’t know the type T at compile-time (as in this code), but you could change the helper method signature to NamespaceOf(Type t).
Actually, I’m surprised that there *is* a Type.Namespace property. I had understood namespaces to be purely a C# convention and unknown to the CLR, so I didn’t expect there to be a built-in way of fetching it at runtime. Does it just cut off the type name at the last dot (i.e. string manipulation), or is there more to it?
August 5th, 2008 at 6:30 pm
[…] if you used the pseudo-route entry trick I described in my previous post, you could intercept URLs after they’re generated, converting them to absolute URLs when […]
August 26th, 2008 at 8:18 am
Ideally, this would be declarative, using an attribute. Anyway, thanks for the code. I still hope the MVC team reads this, because I really want native support for this.
September 6th, 2008 at 2:54 am
Really cool, just what I needed.
October 17th, 2008 at 8:50 am
Namespaces are definitely CLR-level and not unique to C#. Full type names always include namespaces.
October 17th, 2008 at 1:34 pm
@Brad - Indeed, it’s clear that the CLR uses fully-qualified type names. The question here is about how the framework can, at runtime, determine which part of the full-qualified type name is the “namespace”. In “CLR via C#”, Jeffrey Richter says:
“Important: The CLR doesn’t know anything about namespaces. When you access a type, the CLR needs to know the full name of the type…”
So how does Type.Namespace work? (And MemberInfo.Name for that matter.) Does the compiled type metadata specify a namespace, or does Type.Namespace just follow a convention of splitting the type name on the last period character?
Well, you’ve prompted me to go and look it up, and it’s described in 22.14 of ECMA-335: The assembly metadata includes both a TypeName and a TypeNamespace field for each exported type, so Mr Richter was slightly misleading in claiming that the CLR doesn’t know about namespaces.