From the www:
...The routing engine will take the first route that matches the supplied URL and attempt to use the route values in that route. Therefo
The routing engine will take the first route that matches the supplied URL and attempt to use the route values in that route.
The reason why this happens is because the RouteTable
is used like a switch-case statement. Picture the following:
int caseSwitch = 1;
switch (caseSwitch)
{
case 1:
Console.WriteLine("Case 1");
break;
case 1:
Console.WriteLine("Second Case 1");
break;
default:
Console.WriteLine("Default case");
break;
}
If caseSwitch
is 1
, the second block is never reached because the first block catches it.
Route
classes follow a similar pattern (in both the GetRouteData and GetVirtualPath methods). They can return 2 states:
VirtualPath
object in the case of GetVirtualPath
). This indicates the route matched the request.null
. This indicates the route did not match the request.In the first case, MVC uses the route values that are produced by the route to lookup the Action
method. In this case, the RouteTable
is not analyzed any further.
In the second case, MVC will check the next Route
in the RouteTable
to see if it matches with the request (the built in behavior matches the URL and constraints, but technically you can match anything in the HTTP request). And once again, that route can return a set of RouteValues
or null
depending on the result.
If you try to use a switch-case statement as above, the program won't compile. However, if you configure a route that never returns null
or returns a RouteValues
object in more cases than it should, the program will compile, but will misbehave.
Here is the classic example that I frequently see posted on StackOverflow (or some variant of it):
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "CustomRoute",
url: "{segment1}/{action}/{id}",
defaults: new { controller = "MyController", action = "Index", id = UrlParameter.Optional }
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
}
In this example:
CustomRoute
will match any URL that is either 1, 2, or 3 segments in length (note that segment1
is required because it has no default value).Default
will match any URL that is 0, 1, 2, or 3 segments in length.Therefore, if the application is passed the URL \Home\About
, the CustomRoute
will match, and supply the following RouteValues
to MVC:
segment1 = "Home"
controller = "MyController"
action = "About"
id = {}
This will make MVC look for an action named About
on a controller named MyControllerController
, which will fail if it doesn't exist. The Default
route is an unreachable execution path in this case because even though it will match a 2-segment URL, the framework will not give it the opportunity to because the first match wins.
There are several options on how to proceed to fix the configuration. But all of them depend on the behavior that the first match wins and then routing won't look any further.
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "CustomRoute",
url: "Custom/{action}/{id}",
// Note, leaving `action` and `id` out of the defaults
// makes them required, so the URL will only match if 3
// segments are supplied begining with Custom or custom.
// Example: Custom/Details/343
defaults: new { controller = "MyController" }
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
}
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "CustomRoute",
url: "{segment1}/{action}/{id}",
defaults: new { controller = "MyController", action = "Index", id = UrlParameter.Optional },
constraints: new { segment1 = @"house|car|bus" }
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
}
public class CorrectDateConstraint : IRouteConstraint
{
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
var year = values["year"] as string;
var month = values["month"] as string;
var day = values["day"] as string;
DateTime theDate;
return DateTime.TryParse(year + "-" + month + "-" + day, System.Globalization.CultureInfo.InvariantCulture, DateTimeStyles.None, out theDate);
}
}
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "CustomRoute",
url: "{year}/{month}/{day}/{article}",
defaults: new { controller = "News", action = "ArticleDetails" },
constraints: new { year = new CorrectDateConstraint() }
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
}
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "CustomRoute",
url: "{segment1}/{segment2}/{action}/{id}",
defaults: new { controller = "MyController" }
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
}
In the above case, the CustomRoute
will only match a URL with 4 segments (note these can be any values). The Default
route as before only matches URLs with 0, 1, 2, or 3 segments. Therefore there is no unreachable execution path.
Anything that routing doesn't support out of the box (such as matching on a specific domain or subdomain) can be done by implementing your own RouteBase subclass or Route subclass. It is also the best way to understand how/why routing works the way it does.
public class SubdomainRoute : Route
{
public SubdomainRoute(string url) : base(url, new MvcRouteHandler()) {}
public override RouteData GetRouteData(HttpContextBase httpContext)
{
var routeData = base.GetRouteData(httpContext);
if (routeData == null) return null; // Only look at the subdomain if this route matches in the first place.
string subdomain = httpContext.Request.Params["subdomain"]; // A subdomain specified as a query parameter takes precedence over the hostname.
if (subdomain == null) {
string host = httpContext.Request.Headers["Host"];
int index = host.IndexOf('.');
if (index >= 0)
subdomain = host.Substring(0, index);
}
if (subdomain != null)
routeData.Values["subdomain"] = subdomain;
return routeData;
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
object subdomainParam = requestContext.HttpContext.Request.Params["subdomain"];
if (subdomainParam != null)
values["subdomain"] = subdomainParam;
return base.GetVirtualPath(requestContext, values);
}
}
This class was borrowed from: Is it possible to make an ASP.NET MVC route based on a subdomain?
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.Add(new SubdomainRoute(url: "somewhere/unique"));
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
}
NOTE: The real gotcha here is that most people assume that their routes should all look like the
Default
route. Copy, paste, done, right? Wrong.There are 2 problems that commonly arise with this approach:
- Pretty much every other route should have at least one literal segment (or a constraint if you are into that sort of thing).
- The most logical behavior is usually to make the rest of the routes have required segments.
Another common misconception is that optional segments mean you can leave out any segment, but in reality you can only leave off the right-most segment or segments.
Microsoft succeeded in making routing convention-based, extensible, and powerful. They failed in making it intuitive to understand. Virtually everyone fails the first time they try it (I know I did!). Fortunately, once you understand how it works it is not very difficult.