Site Meter
 
 

Implementing a custom route priority order

Yesterday, 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 :)

15 Responses to Implementing a custom route priority order

  1. Tommy

    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!

  2. David Kemp

    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&gt ()
    {
    return typeof ( T ).Namespace;
    }

  3. David Kemp

    Sorry, nearly got that right:
    public static string NamespaceOf<T> ()
    {
    return typeof ( T ).Namespace;
    }
    (never know what HTML blog comments will accept!)

  4. Steve

    @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?

  5. Pingback: Steve Sanderson’s blog » Blog Archive » Adding HTTPS/SSL support to ASP.NET MVC routing

  6. Mike

    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.

  7. Really cool, just what I needed.

  8. Namespaces are definitely CLR-level and not unique to C#. Full type names always include namespaces.

  9. Steve

    @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.

  10. Hi Steve – just wondering whether this is still possible with the latest releases of MVC? (given that you now can’t cast RequestContext to a ControllerContext in order to fetch the controller?)

    Thanks!

  11. Steve

    @James – there must be some way to do it, but I haven’t checked recently. I may well be coming back to update some of these older posts if I get time.

  12. Ace

    I am struggling with Routing and some how landed on this post. Seems like a good idea but don’t really know if it solves my problem. I don’t even know if what I am experiencing is an actual “Problem”. I get around the problem by re-arranging routes.maproute() but I really would like to know how some thing works rather than just taking it for granted that it works! The problem is: I have a number of routes registered but don’t understand how routes are actually matched.

    For Example:

    RouteTable.Routes.Add(
    new Route(“Properties/{area}/List/all”,
    new RouteValueDictionary(new {controller = “Property”, action = “List”, area = “”}),
    new CustomRouteHandler())
    );

    RouteTable.Routes.Add(
    new Route(“Admin/Agents/All/Page/{Page}”,
    new RouteValueDictionary(new { controller = “Admin”, action = “Index”, page = string.Empty }),
    new CustomRouteHandler())
    );

    I have adapted a paging mechanism that generates page numbers so that the “Index” action on “Admin” controller returns a list and is paged. The page 2 however is getting mapped to href=”http://domain/Properties/List/All?page=2″ by the Routes.GetVirtualPath () but I actually want it to be mapped to href=”http://domain/Admin/Agents/All/Page/2″. If I re-arrange them, they get mapped fine. I would really appreciate if any one could shed some light on this or even if someone can point me to an article that explains how GetVirtualPath resolves the path… Thanks – Ace

  13. anon

    Yep, I get “Cannot convert type ‘System.Web.Routing.RequestContext’ to ‘System.Web.Mvc.ControllerContext’ via a reference conversion, boxing conversion, unboxing conversion, wrapping conversion, or null type conversion.”

    =(

  14. There is a site on a question interesting you.
    Bravo, is simply excellent phrase :)
    You are not right. I am assured.
    I precisely know, what is it — an error.
    Excuse for that I interfere … But this theme is very close to me. Write in PM.

  15. Greetings I recently finished reading through through your blog along with I’m very impressed. I truly do have a couple inquiries for you personally however. Do you consider you’re thinking about doing a follow-up putting up about this? Will you be gonna keep bringing up-to-date also?