Update: Phil Haack has now blogged on this under Routing Regression With Two Consecutive Optional Parameters.

When upgrading the Labs and Demo code for a course I run from MVC 2 to MVC 3 I discovered some odd behaviour with one of the routes. We  have a very simple route to handle an “archive” page for a blog engine;

   1:  routes.MapRoute(
   2:      "Archive",
   3:      "archive/{year}/{month}/{day}",
   4:      new
   5:      {
   6:          controller = "Other",
   7:          action = "Archive",
   8:          month = UrlParameter.Optional,
   9:          day = UrlParameter.Optional
  10:      });

This maps to an action that looks a little like this;

   1:  public ActionResult Archive(
   2:      int year, 
   3:      int? month, 
   4:      int? day)
   5:  {
   6:      return Content("Thanks");
   7:  }

In the real solution it obviously does a bit more work than that, but you get the point! The problem is that in a view I had three links to this action, something like these;

   1:  @Html.ActionLink(
   2:      "12th", 
   3:      "Archive", 
   4:      "Home",
   5:      new { year = 2011, month = 11, day = 12 }, 
   6:      null)
   7:  @Html.ActionLink(
   8:      "November", 
   9:      "Archive", 
  10:      "Home", 
  11:      new { year = 2011, month = 11 }, 
  12:      null)
  13:  @Html.ActionLink(
  14:      "2011", 
  15:      "Archive", 
  16:      "Other", 
  17:      new { year = 2011 }, 
  18:      null)

They’re designed to output the date with different sections of it leading to different ranges of the archive. It turns out, however, that the URLs these ActionLinks output are;

http://localhost:60000/archive/2011/11/12

http://localhost:60000/archive/2011/11

http://localhost:60000/Home/Archive?year=2011

Notice the last of these links – it doesn’t follow my routing pattern! Instead it has fallen back to the default routing pattern. If I had deleted the default route (which I would usually) it would have created a link to the root of the web site (i.e. “localhost:60000/”).

Workaround

After a quick bit of mail tennis with Phil Haack it seems the best workaround for this is to add a new route, just below my existing Archive route, that handles the failing case;

   1:  routes.MapRoute(
   2:      "Archive_Year",
   3:      "archive/{year}",
   4:      new
   5:      {
   6:          controller = "Home",
   7:          action = "Archive"
   8:      });

This works because the problem occurs when you have two consecutive optional parameters in your routing pattern. Therefore, we deal with the case that won’t match by adding a pattern that excludes the optional parameters.  I’ll let you know if I hear of any updates to this.

If only I had unit tests… Oh wait, I have!

By sheer coincidence my last blog post was on Unit Testing ASP.NET MVC Routes. If you are unit testing your routes, you will see this failure pop up when you upgrade to MVC 3. If you are not testing your routes, you might not notice it! I hope this is a good example of how unit testing your routing table can highlight issues with the navigation through your site. I’ve also attached an example solution that repro’s the problem and has some (fairly basic, and nowhere near complete I’m sure) unit tests for the routing table, pulling together some of the resources in my previous blog post to make them testable. Let me encourage you once more to thoroughly unit test your routes, and to run those tests as part of a Continuous Integration scheme, nightly build, or pre-release checklist.