Grouping Routes Part 1, mvc, code 0 comments suggest edit

UPDATE: 2011/02/13: This code is now included in the RouteMagic NuGet package! To use this code, simply run Install-Package RouteMagic within the NuGet Package Manager Console.

One thing ASP.NET Routing doesn’t support is washing and detailing my car. I really pushed for that feature, but my coworkers felt it was out of scope. Kill joys.

Another thing Routing doesn’t support out of the box is a way to group a set of routes within another route. For example, suppose I want a set of routes to all live under the same URL path. Today, I’d need to make sure all the routes started with the same URL segment. For example, here’s a set of routes that all live under the “/blog” URL path.

RouteTable.Routes.MapRoute("r1", "blog/posts");
RouteTable.Routes.MapRoute("r2", "blog/posts/{id}");
RouteTable.Routes.MapRoute("r3", "blog/archives");

If I decide I want all these routes to live under something other than “blog” such as in the root or under a completely different name such as “archives”, I have to change each route. Not such a big deal with only three routes, but with a large system with multiple groupings, this can be a hassle.

I suppose one easy way to solve this is to do the following:

string baseUrl = "blog/";
RouteTable.Routes.MapRoute("r1", baseUrl + "posts");
RouteTable.Routes.MapRoute("r2", baseUrl + "posts/{id}");
RouteTable.Routes.MapRoute("r3", baseUrl + "archives");

Bam! Done! Call it a night Frank.

This is actually a very simple and great solution to the problem I stated. In fact, it probably works better than the alternative I’m about to show you. If this works so well, why am I showing you the alternative?

Well, there’s something unsatisfying about that answer. Suppose a request comes in for /not-blog. Every one of those routes is going to be evaluated even though we already know none of them will match. If we could group them, we could reduce the check to just one check. Also, it’s just not as much fun as what I’m about to show you.

What I would like to be able to do is the following.

var blogRoutes = new RouteCollection();
blogRoutes.MapRoute("r1", "posts");
blogRoutes.MapRoute("r2", "posts/{id}");
blogRoutes.MapRoute("r3", "archives");

RouteTable.Routes.Add("blog-routes", new GroupRoute("~/blog", blogRoutes));

In this code snippet, I’ve declared a set of routes and added them to a proposed GroupRoute instance. That group route is then added to the route table. Note that the child routes are not themselves added to the route table and they have no idea what parent path they’ll end up responding to.

With this proposed route, I these child routes would then handle requests to /blog/posts and /blog/archives. But if I decide to place them under a different path, I can simply change a single route, the group route, and I don’t need to change each child route.


In this section, I’ll describe the implementation of such a group route in broad brush strokes. The goal here is to provide an under the hood look at how routing works and how it can be extended.

Implementing such a grouping route is not trivial. Routes in general work directly off of the current http request in order to determine if they match a request or not.

By themselves, those child routes I defined earlier would not match a request for /blog/posts. Note that the URL for the child routes don’t start with “blog”. Fortunately though, the request that is supplied to each route is an instance of HttpRequestBase, an abstract base class.

What this means is we can muck around with the request and even change it so that the child routes don’t even know the actual requests starts with /blog. That way, when a request comes in for /blog/posts, the group route matches it, but then rewrites the request only for its child routes so that they think they’re trying to match /posts.

Please note that what I’m about to show you here is based on internal knowledge of routing and is unsupported and may cause you to lose hair, get a rash, and suffer much grief if you depend on it. Use this approach at your own risk.

The first thing I did was implement my own wrapper classes for the http context class.

public class ChildHttpContextWrapper : HttpContextBase {
  private HttpContextBase _httpContext;
  private HttpRequestBase _request;

  public ChildHttpContextWrapper(HttpContextBase httpContext, 
      string parentVirtualPath, string parentPath) {
    _httpContext = httpContext;
    _request = new ChildHttpRequestWrapper(httpContext.Request, 
      parentVirtualPath, parentPath);

  public override HttpRequestBase Request {
    get {
      return _request;

  // ... All other properties/methods delegate to _httpContext

Note that all this does is delegate every method and property to the supplied HttpContextBase instance that it wraps except for the Request property, which returns an instance of my next wrapper class.

public class ChildHttpRequestWrapper : HttpRequestBase {
  HttpRequestBase _httpRequest;
  string _path;
  string _appRelativeCurrentExecutionFilePath;

  public ChildHttpRequestWrapper(HttpRequestBase httpRequest, 
    string parentVirtualPath, string parentPath) {
    if (!parentVirtualPath.StartsWith("~/")) {
      throw new InvalidOperationException("parentVirtualPath 
        must start with ~/");

    if (!httpRequest.AppRelativeCurrentExecutionFilePath
        .StartsWith(parentVirtualPath, StringComparison.OrdinalIgnoreCase)) {
      throw new InvalidOperationException("This request is not valid 
        for the current path.");

    _path = httpRequest.Path.Remove(0, parentPath.Length);
    _appRelativeCurrentExecutionFilePath = httpRequest.
      AppRelativeCurrentExecutionFilePath.Remove(1,       parentVirtualPath.Length - 1);
    _httpRequest = httpRequest;

  public override string Path { get { return _path; } }

  public override string AppRelativeCurrentExecutionFilePath {
    get { return _appRelativeCurrentExecutionFilePath; }

  // all other properties/method delegate to_httpRequest

What this child request does is strip off the portion of the request path that corresponds to its parent’s virtual path. That’s the “~/blog” part supplied by the group route.

It then makes sure that the Path and the AppRelativeCurrentExecutionFilePath properties return this updated URL. Current implementations of routing look at these two properties when matching an incoming request. However, that’s an internal implementation detail of routing that could change, hence my admonition earlier that this is voodoo magic.

The implementation of request matching for GroupRoute is fairly straightforward then.

public override RouteData GetRouteData(HttpContextBase httpContext) {
  if (!httpContext.Request.AppRelativeCurrentExecutionFilePath.
      StartsWith(VirtualPath, StringComparison.OrdinalIgnoreCase)) {
    return null;

  HttpContextBase childHttpContext = VirtualPath != ApplicationRootPath ? 
    new ChildHttpContextWrapper(httpContext, VirtualPath, _path) : null;

  return ChildRoutes.GetRouteData(childHttpContext ?? httpContext);

All we did here is to make sure that the group route matches the current request. If so, we then created a child http context which as we saw earlier, looks just like the current http context, only the /blog portion of the request is stripped off. We then pass that to our internal route collection to see if any child route matches. If so, we return the route data from that match and we’re done.

In Part 2 of this series, we’ll look at implementing URL generation. That’s where things get really tricky.

Found a typo or error? Suggest an edit! If accepted, your contribution is listed automatically here.



8 responses

  1. Avatar for Maarten Balliauw
    Maarten Balliauw December 2nd, 2010

    The "AppRelativeCurrentExecutionFilePath" is exactly the same headache we have in the MvcSiteMapProvider. We fixed it similarly, by creating a wrapper around HttpContextBase and rewriting the URL in an overloaded property.
    This makes two of us running against that wall, so maybe a small update in the route system can be handy :-p

  2. Avatar for Dan F
    Dan F December 2nd, 2010

    (I'll prefix this by saying it's mid Friday evening here in Aus, I've not used MVC in anger, and I might have had a beer or two :))
    Call me stupid, but why couldn't you just write an extension method to MapRoute that takes in the collection of routes like your second example, and just does a bit of a spin thru doing like what you did in your first example?
    Although, on further examination, this directly violates "Note that the child routes are not themselves added to the route table and they have no idea what parent path they’ll end up responding to", so I'm probably talking out my backside here. Carry on. Have a beer for this tired Aussie :)

  3. Avatar for John Ludlow
    John Ludlow December 2nd, 2010

    @Dan F: He could have done, but that would have meant that a request for ~/something_else would have to check against ~/blog/posts, ~/blog/posts/{id}, ~/blog/archives and so on. He was trying to group all those three ~/blog/* routes under ~/blog, so that a request for ~/something_else just checks once, against the group, and moves on.
    It's over-engineering in this case since 3 routes probably wouldn't cause an issue. For a larger site, something like this may be significant.
    However, like he says this is voodoo magic because you're not really supposed to play with the stuff he's playing with. Still, fun and interesting nonetheless.

  4. Avatar for jdubrownik
    jdubrownik December 2nd, 2010

    Thanks for touching this subject. It's far better than my solution with chained routes.

  5. Avatar for haacked
    haacked December 3rd, 2010

    @DanF Yeah, you could do that. I'm wary of manipulating existing routes though. But then again, I should probably be ware of what I'm doing. And as you pointed out, it still has that other, albeit minor, problem.
    @John Indeed. I probably should do some perf measurement to see what the actual impact really is with say 20 to 100 routes. Maybe it's so small as not to really be worth it.

  6. Avatar for vector
    vector December 4th, 2010

    Thanks for the rather useful article...

  7. Avatar for Dr. Bunson Honeydew
    Dr. Bunson Honeydew December 8th, 2010

    I've an issue that looks like your post may elude to. Say you create a second GroupRoute like "~/forum" that adds the BlogRoutes Collection. Is that possible? if a user hits, how would you send them to the right page? Could you include the GroupRoute as a filter? Am I way off here? I'm way off aren't I?

  8. Avatar for Skywalker
    Skywalker January 30th, 2011

    Just wanted to say I love this article and that I definitely would like to see support for GroupingRoutes baked in into the Routing system!