Saturday, March 31, 2012

ASP.NET MVC and overlapping routes

ASP.NET routing in MVC allows us to define how different URLs are mapped to the controllers, actions, action parameters etc. It is quite simple and in some cases it seems even too simple. In our MVC application we had a requirement that we should accept these two path patterns:

{controller}/{action}
{controller}.aspx/{action}

The first one is the default, we want our generated links to go this route. The second one is legacy, but it's required to work correctly. We could've set up some kind of redirects from old route to the default one, but we've thought it'll be easier to define a separate route in our application that will map to the same controllers as the default route.

Easier said than done. The first attempt looked like that:

routes.MapRoute("Default", "{controller}/{action}", 
new { controller = "Home", action = "Index" });
routes.MapRoute("Legacy", "{controller}.aspx/{action}",
new { controller = "Home", action = "Index" });

Seems trivial, but doesn't work. When requested Home.aspx, the application failed to find the controller named "Home.aspx". Well, the default route eagerly matched the controller variable and missed the fact that the second route seems to be better. Now I remember, the docs clearly state that finding the route stops on first match and we should arrange our routes from most specific to most generic ones.

OK, let's then change the order of our routes, so that we'll catch legacy ones first:

routes.MapRoute("Legacy", "{controller}.aspx/{action}", 
new { controller = "Home", action = "Index" });
routes.MapRoute("Default", "{controller}/{action}",
new { controller = "Home", action = "Index" });

Looks like working, both "Home.aspx" and "Home" are mapped to HomeController. But now all our links generated by MVC helpers (like Html.ActionLink) have .aspx extension. We don't want to expose this route as it is for backwards compatibility only. I've found the explanation and the insight I needed in Craig Stuntz's article. Generally, when building an URL, the helpers' behavior is similiar to parsing scenario. The first route that can be satisfied with route values given is chosen. We're passing controller and action values to Html.ActionLink, so the first route matches.

But Craig's article got me on the right track:

routes.MapRoute("Legacy", "{controller}.{extension}/{action}", 
new { controller = "Home", action = "Index" }, new { extension = "aspx" });
routes.MapRoute("Default", "{controller}/{action}",
new { controller = "Home", action = "Index" });

I've modified the first, legacy route, introduced the extension variable in place of "aspx" and defined a constraint in the fourth parameter, stating that extension variable have to be equal to aspx. This way only URLs like Home.aspx match the first route and ActionLink helpers don't use it (unless a route value named extension and equal to "aspx" is passed).

1 comment: