Sponsored By Aspose - File Format APIs for .NET

Aspose are the market leader of .NET APIs for file business formats – natively work with DOCX, XLSX, PPT, PDF, MSG, MPP, images formats and many more!

An investigation into routes

This post would probably be more entertaining if I dictated it because then you could all have a good chuckle on my Canadian pronunciation on "route". For the record, it is supposed to be pronounced "root", which is clean, terse, and polite. Not "r-OW-t", which, if done correctly (like in Texas), requires your jaw to be double-jointed.

To date, routes have always been something I’ve kinda, sorta had figured out. I use the default ones as much as possible and when I stray from that, I end up plugging away until they work for my current situation. Sometimes this involves creating hard-coded ones, other times simply re-arranging the order has the desired effect.

Oh sorry, did I just make you throw up a little?

But now it’s time to tackle these little beasties head-on, thanks in no small part to Ben Scheirman’s recent addition to MvcContrib, wherein you can test your routing strategy with a call as simple as:

"~/Still/AddIngredient/Dandelion".Route().ShouldMapTo( c => c.AddIngredient( "Dandelion" ) );

First, let’s take a look at a concrete example to define the problem space. Here is a sample route table:

routes.Add(new Route("{controller}/{action}", new MvcRouteHandler()) 
   { 
       Defaults = new RouteValueDictionary(new {action = "Index"}), 
   }); 

routes.Add(new Route("{controller}/{id}/{action}", new MvcRouteHandler()) 
   { 
       Defaults = new RouteValueDictionary(new {action = "Index", id = ""}), 
   }); 

With something like this, and in fact, with routes in general, order is important. More so than I’d like, quite frankly. A URL like ~/Still/Mix will match the first one but one like ~/Still/101/Dismantle matches the second. Note that in this scenario, the default values in the second one seem meaningless. If no action or id are provided, the first route will be used so there is no need for defaults. We’ll come back to that.

It should also be noted that the routes come into play not only when parsing URLs but when generating them. This is actually what led to this post. In one of our controllers, we used the RedirectToRoute result. For those of you that can’t parse out Pascal casing, this just allows you to redirect to another route from within an action.

The final line of that action was: return RedirectToRouteResult( "Ferment", "Hooch", new { id = 123 } );

Given the above route table, what do you suppose would be the URL that this generates? If you say ~/Hooch/123/Ferment, give yourself a pat on the back. But it’ll be just because you’re a nice guy/gal because it’s actually wrong. The correct answer is: ~/Hooch/Ferment?id=123.

Here’s the thought process as I imagine it to be. The route generator checks the first route in the dictionary, which is "{controller}/{action}" and says "Can I make these values match that route?" Well, of course it can. It’s got a controller and an action. Everything else can be tacked on in the query string.

This URL turned out to be a bit of an issue for me because of another problem that this uncovered. At some point in our controller, we reference ViewContext.RouteData.Values["id"]. And with the id as part of the query string, this bit of code returns null. The debugger shows that for a URL of ~/Hooch/Ferment?id=123, the RouteData collection contains two values: controller = Hooch and action = Ferment. The id is nowhere to be found, at least by the RouteData.

So how *do* we get the RedirectToRouteResult to generate a shinier URL? Why, we fiddle with the routes until they work, of course! It’s the principle that bug fixing is based upon.

I jest, of course. But only partly. We do need to fiddle with the routes but we should really write a test to verify that the ViewContext.RouteData is being set properly. Unfortunately, this isn’t easy without some funky mocking.

One would think that you could just examine the RedirectToRouteResult coming back from the controller action but alas! It appears correct. I.e. It has all three components (controller, action, and id) set properly. I suppose the ViewContext.RouteData is set somewhere else in the bowels of the framework. So I’ll skip the test because I’m already over my allotted time. Plus we’re using Preview 5 still and this could very well be different in the beta.

Let’s reverse the order of the routes in the table and see what happens. Here’s what that looks like:

routes.Add(new Route("{controller}/{id}/{action}", new MvcRouteHandler()) 
   { 
       Defaults = new RouteValueDictionary(new {action = "Index", id = ""}), 
   }); 

routes.Add(new Route("{controller}/{action}", new MvcRouteHandler()) 
   { 
       Defaults = new RouteValueDictionary(new {action = "Index"}), 
   }); 

This immediately caused problems. The URL ~/Still/Mix now maps to the first route. That is, it is invoking the Index action on the Still controller with an id of Mix. Again, the defaults are screwing us over like some sort of mot–…actually, let’s leave the simile out of that one. Part of the reason for our problem is because the {action} is in a different place in each route. But with the defaults in place, this set up will essentially ensure the second route will never be used.

Instead, we need to take out the defaults altogether, like so:

routes.Add(new Route("{controller}/{id}/{action}", new MvcRouteHandler()) 
   { 
       Defaults = new RouteValueDictionary(new {}), 
   }); 

routes.Add(new Route("{controller}/{action}", new MvcRouteHandler()) 
   { 
       Defaults = new RouteValueDictionary(new {action = "Index"}), 
   }); 

Now we’re cooking. The URL ~/Still/Mix no longer matches the first route because we haven’t provided an id. But ~/Still/101/Dismantle does meet the requirements. Furthermore, generating a RedirectToRouteResult( "Ferment", "Hooch", new { id = 123 } ) now gives us a URL in the format: ~/Hooch/123/Ferment. Again, this is because it examines the first route in the table and finds a match.

I’ve omitted quite a bit of context here, like why we have {id} second in the route and why we’re referencing ViewContext.RouteData.Values["id"] in the first place. There is a good reason for all of that. At least as far as you know…

Kyle the Evasive

This entry was posted in ASP.NET MVC. Bookmark the permalink. Follow any comments here with the RSS feed for this post.
  • http://www.blogcoward.com jdn

    So, it is:

    “so let’s route, route, route for the home team, if they don’t win it’s a shame”

  • http://www.honestillusion.com James Curran

    Ok, so you are all right & all wrong.

    Route (“root”) is a noun.
    Route (“r-OW-t”) is a verb.

    Hence one might say “/root/ 80 was backed up, so I /rowted/ him around on /root/ 46″.

  • http://wekempf.spaces.live.com wekempf

    According to all the dictionaries I could find, both pronunciations are correct. So basically, you’re participating in a religious debate. Then again, as techies, that seems to be the norm.

    Poe-TAY-toe, Poe-TAH-toe, Toe-MAY-toe, Toe-MAH-toe. Let’s call the whole thing off.

  • http://www.alvinashcraft.com Alvin Ashcraft

    Gotta agree with Chad on the pronunciation issue. Sorry, your highness…

  • http://liz@buckinghampalace.gov.uk Queen Elizabeth II

    Now listen to me you rebellious colonials. It’s my language and I hearby decree that the correct pronunciation (that is pro-NUN-ciation BTW) of route is root.

    BTW you now owe us 232 years of back-dated tea taxes.

    HRH

  • http://hadihariri.com Hadi Hariri

    Now I don’t feel awkward when I say “root”. And it’s a “rooter” not a “rawter”

  • Steve Sheldon

    I was just reading something similar today.

    http://weblogs.asp.net/leftslipper/archive/2008/12/17/optimizing-your-route-collection-for-url-generation-in-asp-net-mvc-and-more.aspx

    He suggests using named routes, and I know from my playing I have had to do that on occasion to have it use the right route. I wonder if it might also not be faster as it’s not going down the list of routes trying to match them?

  • McOz

    Australians say ‘root’. This is also a colloquialism for engaging in sexual intercourse as popularised by the classic hit ‘Rooting in the Back of the Ute” by Kevin Bloody Wilson. (‘ute’ being slang for ‘Utility Vehicle’, a two seater car with an open back tray for carrying goods).

    “Wanna root?” is considered a typical Australian mating call, usually (but not always) uttered by the male of the species after consuming a minimum 8 cans of larger. Refusal of such a proposition does not offend as after all, there’s usually plenty more been to be drunk.

  • Kyle Baley

    Lord Tunderin’ Jayzus, it figures the first two commenters here would be a couple o’ Texans.

    Ben: I did know about it but it didn’t occur to me to refactor to it. Hard to see the forest for the trees when you’re neck deep in it. Thanks.

  • http://chadmyers.lostechies.com Chad Myers

    The correct pronunciation is actually “r-OW-t”, not “root”. Roots are roots, routes are “r-OW-ts”. Unless you live in the UK, but they screw up S’s and Z’s, and eat human placentas, so I’m not sure they’re qualified to be the authority on anything.

    Curiously, native Texans generally say “root” or “rut”, so they actually have MORE in common with the British in this regard. British have silly words like ‘loo’ and ‘poppycock’, similar to how Texans say things like “tump” and “ya’ll”.

    In summary, the civilized world uses the “r-OW-t” pronunciation while cross-eyed, mouth-breathing, elbows-on-table barbarians use “root”.

  • http://flux88.com Ben Scheirman

    You do know about the routes.MapRoute(..) helper method right? it makes your routes a tad easier to build.

    Also, I modified the code in MvcContrib to take out the Route() method. It’s actually still there if you want the raw route data from the string, but now you can just say “~/candiens/tawk/funneh”.ShouldRouteTo(x=>x.Yeehaw());