The routing engine will execute the first route corresponding to the specified URL and try to use the route values in this route.
The reason this happens is because RouteTable used as a switch-case statement. Imagine 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.
Classes
GetRouteData follow a similar pattern (in GetRouteData and GetVirtualPath ). They can return 2 states:
- A set of route values (or a
VirtualPath object in the case of GetVirtualPath ). This indicates that the route matches the request. null This indicates that the route does not match the request.
In the first case, MVC uses the route values that are created by the route to find the Action method. In this case, the RouteTable no longer parsed.
In the second case, MVC will check the next Route in the RouteTable to see if it matches the request (the built-in behavior matches the URL and restrictions, but technically you can match anything in the HTTP request). Again, this route can return a set of RouteValues or null depending on the result.
If you try to use the switch-case statement as described above, the program will not 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 not work correctly.
Mismatch Example
Here is a classic example that I often see in StackOverflow (or its variant):
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 has a length of 1, 2, or 3 segments (note that segment1 is required since it does not have a default value).Default will match any URL that is 0, 1, 2, or 3 segments long.
Therefore, if the URL \Home\About is passed to the application, CustomRoute will match, and put the following RouteValues in MVC:
segment1 = "Home"controller = "MyController"action = "About"id = {}
This causes MVC to look for an action called About on a controller named MyControllerController , which will not work if it does not exist. The Default route is an unattainable execution path in this case, because although it will correspond to a URL with two segments, the structure will not give it an opportunity, because the first match wins.
Commit configuration
There are several options for customizing the fix. But they all depend on the behavior that the first match wins , and then the routing will no longer look.
Option 1: add one or more literal segments
public class RouteConfig { public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.MapRoute( name: "CustomRoute", url: "Custom/{action}/{id}",
Option 2: Adding 1 or More RegEx Restrictions
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 } ); } }
Option 3: Add 1 or more custom restrictions
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 } ); } }
Option 4: Make the required segments + Make the number of segments not match existing routes
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, CustomRoute will only match a URL with 4 segments (note that these can be any values). The Default route still only matches URLs with 0, 1, 2, or 3 segments. Therefore, there is no unattainable execution path.
Option 5: Implement RouteBase (or Route) for User Behavior
Everything that routing is not supported out of the box (for example, matching by a specific domain or subdomain) can be performed using 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 information here is that most people think their routes should look like the Default route. Copy, paste, do, right? Wrong.
There are usually two problems with this approach:
- Virtually every other route should have at least one literal segment (or restriction if you are doing this).
- The most logical behavior usually is for the rest of the routes to have segments.
Another common misconception is that optional segments mean that you can leave a segment, but in fact you can leave only the rightmost segment or segments.
Microsoft has succeeded in creating protocol-based routing, extensibility, and power. They failed to understand intuition. Almost everyone fails the first time they try to do it (I know I did!). Fortunately, once you understand how it works, it is not very difficult.