The Sitecore Default MVC Route is NOT a Catchall Route

This blog post explains how the Sitecore default MVC route in solutions using the ASP.NET web Content Management System (CMS) can match all URLs without applying to every URL.

First, a few words about routing. ASP.NET provides a route table, which is basically an ordered list of routes.

Each route can contain a variety of details, but typically most importantly, defines a pattern for URLs to match, default values, and possibly some constraints. For each HTTP request, ASP.NET applies the first route in the table that matches the requested URL. If no route matches, then somehow you end up in Web Forms world.

Below are the default routes that an Visual Studio defines for a default MVC 4 project. Note that the current release of Sitecore CMS 6.6 supports MVC 3 but I am working on MVC 4 support. In this context the specific version doesn't matter though MVC 4 names the arguments to the MapRoute() method, apparently to make the code easier to read (which is useful in this case), where MVC 3 did not include argument names:

  name: "Default"
  url: "{controller}/{action}/{id}"
  defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }

The pattern in each route can include some number of segments indicated by curly braces ({}). The asterisk (*) matches any number of segments. Each segment defines something like a variable that gets passed through the route. In the first route, the {resource}.axd/{*pathInfo} pattern matches a URL containing a single segment (such as /trace.axd) followed by any number of segments (such as /trace.axd/whatever). Because this route uses the IgnoreRoute() method, if the URL matches the pattern, ASP.NET stops evaluating the route table and processes the request using the specified Web Handler .axd (I am not sure if or how this works if segments appear before the .axd, such as /subdirectory/something.axd). If this had used MapRoute instead of IgnoreRoute, ASP.NET would have defined something named resource containing trace and something named pathInfo containing whatever.

The pattern in the second route matches all URLs with three segments, such as /hr/jobs/123. Because it does not include an asterisk, it does not match URLs with more than four segments (/hr/jobs/123/requirements). Because it provides default values for the first two segments and indicates that the third segment is optional, it also matches URLs with zero segments (/, the home page), one segment (/hr), or two segments (/hr/jobs). This is not a catchall route because it matches only a given number of segments.

A catchall route matches all URLs. For example, {*pathInfo} and {controller}/{action}/{*pathInfo} (assuming controller and action are optional or define defaults) would be catchall routes, the difference being that the second would defined controller and action.

The Initialize processor that Sitecore MVC adds to the initialize pipeline defines a route that looks like a catchall route, something like this:

  new { scIsFallThrough = true },  
  new { isContent = new Sitecore.Mvc.Presentation.IsContentUrlRestraint() });

The reason this is not a true catchall route is the fourth argument to the MapRoute() method, which defines a constraint. When ASP.NET determines whether a route applies, it invokes the Match() method of each constraint. If that method returns false, then the route does not apply.

The Match() method of the IsContentUrlRestraint class checks for a value named sc::IsContentUrl in the Items property of the System.Web.HttpContextBase passed as the first argument to the method. This value is absent by default. The TransferControllerRequest and TransferMvcLayout processors that Sitecore MVC adds to the httpRequestBegin pipeline set this value if the context item indicates a controller or contains layout details that indicate an MVC layout.

Because the TransferRoutedRequest processor appears in the httpRequestBegin pipeline before the TransferControllerRequest and TransferMvcLayout pipelines, sc::IsContentUrl is absent when it runs and the Sitecore default route does not apply. If the route table included the {controller}/{action}/{id} route and the URL consisted of three or fewer segments, then that route would apply, whether it appeared before or after the Sitecore default route in the table. If the TransferRoutedRequest processor determines that no route applies, then the httpRequestBegin pipeline continues and the TransferControllerRequest or the  TransferMvcLayout processors can set sc:IsContentUrl and abort the pipeline, at which point the Sitecore default route would apply (though unless you take further steps, the {controller}/{action}/{id} route would apply if it appears before the Sitecore default route and the URL consists of three or fewer segments).