niico
niico

Reputation: 12749

Why does the "Default" site route to the site root in ASP.NET MVC work?

ASP.NET MVC sites come with the following default root that executes the Index action of the Home controller when the root of the site is visited (e.g. http://localhost:12345)

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

So it's a kind of fallback when no route is found then?

Not so fast. If we try and navigate to http://localhost/a/b (where no 'a' controller exists) it will not execute the Index action of the Home controller - an error will be returned.

Why does that fail to execute Home/Index in that scenario - but entering absolutely nothing in the path does execute Home/Index?

What is the logic here - and why is this route called 'defaults'?

I've seen a lot of articles cover routing - but not one where this is explained.

Related Question

In other situations what is in 'defaults' seems more like a mapping of a route. For example

url: "abc/def", defaults: new { controller = "bongo", action = "bingo" }

This just executes the bingo action on the bongo controller - whenever the exact url "abc/def" is entered. Why is it called 'defaults' - that term doesn't seem to fit really. (does removing 'defaults' have any effect, I have seen it omitted).

In the Default route it seems more like a fallback, in the latter example more like a mapping?

I feel like there is something at the conceptual level I am missing here.

thx.

Upvotes: 1

Views: 1284

Answers (1)

NightOwl888
NightOwl888

Reputation: 56909

The logic, while not very intuitive at first, is actually pretty simple.

In general, there are 2 different things going on when an incoming request happens.

  1. Attempt to match the route
  2. Supply MVC with a set of route values (and optionally, route metadata), which it uses to lookup the action method

When an incoming request happens, MVC executes the GetRouteData method of the first route in the route table. If it doesn't match, it will attempt the second, third, and so on until a match is found.

If ultimately there is no match in the route table, RouteCollection.GetRouteData (the method that calls GetRouteData of each route) will return null. If a match is found, the route values from the matching route will be returned to MVC where it uses them to lookup the controller and action to execute. Do note that in this case no other routes in the route table are checked for a match. In other words, the first match always wins.

The matching process relies on 3 things:

  1. Placeholders
  2. Literal segments
  3. Constraints

Placeholders

The part you are asking about are placeholders and why they match when no value is present. Placeholders (i.e. {controller}) act like variables. They will accept any value. They can be initialized to a default value. If they are not initialized to a default value, they are required to be in the URL in order to match.

Consider the defaults in the route definition:

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

These are the values that will be used (as both the match and the output in the RouteValues) if the placeholder is not supplied in the URL.

By this same logic, by default all of these URLs will reach the HomeController.Index action method.

  • /
  • /Home
  • /Home/Index
  • /Home/Index/Foo
  • /Home/Index/123

URL: /

If you pass the URL / to the framework, it will match the Default route and send it to the HomeController.Index method because the default values are Home and Index if none are supplied. In this case, the route values are:

| Key         | Value       |
|-------------|-------------|
| controller  | Home        |
| action      | Index       |
| id          | {}          |

URL /Home

Note that you could also just pass the controller name /Home. The route table looks exactly the same.

| Key         | Value       |
|-------------|-------------|
| controller  | Home        |
| action      | Index       |
| id          | {}          |

However, in this case the controller value is being passed in through the placeholder in the URL. It is not considering the default controller value of the route anymore because a value has been supplied in the URL.

URL: /Test

Following this same logic, the URL /Test will result in the following route table.

| Key         | Value       |
|-------------|-------------|
| controller  | Test        |
| action      | Index       |
| id          | {}          |

Routing doesn't check automatically whether the controller actually exists. It just supplies the values. If there is no controller in your application named TestController with an Index action, this will result in an error.

This is why the URL /a/b you supplied above doesn't work - there is no controller in your project named AController with an action named B.

If placeholders are not initialized to a default value, they are required to be in the URL in order for the route to match.

routes.MapRoute(
    name: "Default",
    url: "{controller}/{action}/{id}",
    defaults: new { action = "Index", id = UrlParameter.Optional }
);

So, given the above route, it will match the URL /Home:

| Key         | Value       |
|-------------|-------------|
| controller  | Home        |
| action      | Index       |
| id          | {}          |

But it won't match the URL / because there is no default value for controller.

Literal Segments

routes.MapRoute(
    name: "Foo",
    url: "abc/def",
    defaults: new { controller = "bongo", action = "bingo" }
);

The above route uses literal segments in the URL. Literal segments require an exact match (case insensitive) in order for the route to be considered a match. Therefore, the only URL that matches is abc/def or any uppercase/lowercase combination of these 2 segments.

However, this scenario differs in one regard. There is no way to pass a value through the URL. Therefore you must set default values (at the very least for controller and action) in order for any route values to be passed on to MVC.

| Key         | Value       |
|-------------|-------------|
| controller  | bongo       |
| action      | bingo       |

The MVC framework requires there to be a BongoController with an action named Bingo or this route will fail miserably.

Constraints

Constraints are extra criteria that is required in order for the route to match. Each constraint returns a boolean (match/no match) response. You can have 0 to many constraints per route.

RegEx constraints

routes.MapRoute(
    name: "CustomRoute",
    url: "{placeholder1}/{action}/{id}",
    defaults: new { controller = "MyController" },
    constraints: new { placeholder1 = @"^house$|^car$|^bus$" }
);

Matches

  • /house/details/123
  • /car/foo/bar
  • /car/bar/foo

Doesn't Match

  • /house/details
  • /bank/details/123
  • /bus/foo
  • /car
  • /

Custom constraints

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);
    }
}

routes.MapRoute(
    name: "CustomRoute",
    url: "{year}/{month}/{day}/{article}",
    defaults: new { controller = "News", action = "ArticleDetails" },
    constraints: new { year = new CorrectDateConstraint() }
);

Note: The value that the constraint is assigned to (in the above case year =) is the same value that is passed into the custom constraint. However, the custom constraint has no obligation to use this value. Sometimes it doesn't make sense to use any value, in which case you can set the constraint on controller.

Matches

  • /2012/06/20/some-great-article
  • /2016/12/25/all-about-christmas

Doesn't Match

  • /2012/06/33/some-great-article
  • /2012/06/20
  • /99999/09/09/the-foo-article

In most cases, you should use constraints whenever there are placeholders (such as {controller} or {something}) in the URL to keep them from matching values that they shouldn't.

Literal segments (or partial literal segments with placeholders), constraints, and required values are all generally very good things to use in your routing setup. They help to ensure that your routes don't match in such a wide scope that they block execution of routes registered after them in the route table.

Placeholders match any value, so it is generally not recommended to use only placeholders in any route but the Default route unless done in conjunction with constraints. Many folks here on StackOverflow recommend to remove the Default route entirely to ensure that unintended routes don't function, and I don't necessarily disagree with this sentiment.

Further Reading

Upvotes: 3

Related Questions