Skip to content

ASP.NET Routing and ASP.NET MVC

.NET 3.5 SP1 introduced ASP.NET Routing. This feature is primarily used by ASP.NET MVC and Dynamic Data. Routing is actually a very simple feature; here is what it does:

  • Stores a list of routes.
  • Translates an incoming URI into lists of values and tokens, and passes those lists to the handler specified on the route. In the MVC framework, this feature is used to call the appropriate Controller/Action.
  • Translates a list of values into a URI suitable for inclusion within an HTML page. In the MVC framework, this feature is most commonly used in the Html.RouteLink/ActionLink methods you call to generate a link in your Views.

Let’s examine in more detail how MVC uses Routing, and distinguish between the features of Routing and the MVC framework.

Components of a URI

Consider this sample URI:

http://www.example.com:80/SiteRoot/Controller/Action?foo=A&bar=B#anchor

The individual parts of the URI are:

  • The scheme (protocol), authority (host), and port ("http://www.example.com:80" in this sample URI). I will mostly ignore this part in the rest of the post, since Routing doesn’t deal with it, except to pass it along when necessary.
  • The path ("/SiteRoot/Controller/Action" in this example). Per the URI RFC, this part "serves to identify a resource…" I’ll talk about what that means in a second.
  • The query ("?foo=A&bar=B" in this example). My example uses name/value pairs is the query, because that seems to be the most common case. However, the RFC says that the query can be almost anything, and the purpose of the query is to provide a non-hierarchical means of identifying the resource requested by the user.
  • The fragment ("#anchor" in this example). I’m going to ignore this, since Routing doesn’t deal with it.
  • I’ve omitted some stuff like username/password which is almost never used today.

MapRoute

Now lets examine a simple case of adding a route to the routing system. This is from the default MVC application template:

routes.MapRoute(
    "Default",                                              // Route name
    "{controller}/{action}/{id}",                           // URL with parameters
    new { controller = "Home", action = "Index", id = "" }  // Parameter defaults
);

Note that MapRoute is actually not part of the routing system; it’s an extension method added to RouteCollection by ASP.NET MVC. Calling MapRoute will add a single route to the RouteCollection. Most web applications will have only one RouteCollection, stored in System.Web.Routing.RouteTable.Routes. The most important thing that MapRoute does is to create and pass an MvcRouteHandler instance when adding the route to the RouteCollection. This ensures that when ASP.NET handles an incoming URI which matches this route, control will be passed to the MVC framework for handling the request.

The second argument to MapRoute is named "url", but it’s not really a complete URL. Instead, it is a template for a fragment of a URL containing a portion of the path component of a URI. I say "a portion of the path" because it omits the site root, which I’ve called "/SiteRoot" in the example above. The site root is often empty, but it doesn’t have to be. The URIs produced by the routing system include quite a bit more than the path, however. They also include the scheme/authority (in other words, the protocol and host) and, sometimes, a query.

There are a number of overloads to MapRoute. In addition to the values passed in this simple example, you can also pass constraints (which I’ll discuss in a moment) and namespaces. The namespaces argument is for MVC only (it’s not used by routing) and is useful when you want to put a Controller in a place where MVC wouldn’t otherwise find it.

A route can also contain DataTokens, which are not used by the routing system, but are passed to the route handler. MapRoute doesn’t have an overload which includes these as an argument, and I’m not going to discuss them in this post. ASP.NET MVC uses them only to store the namespaces argument, discussed above.

Parsing a URI

Let’s examine what happens when the routing system is asked to interpret a URI submitted by the user. Importantly, the RouteCollection is ordered. So the routing system will attempt to match the routes in the collection in the order you added them. The first matching route "wins" even if a later route is a "better" match. The incoming URI is compared with the url portion of the route. If it matches, the entire URI, including the query, is tokenized. If there are any constraints on the route, these are considered.

In practice, this means that if you have a lot of routes, then you are almost guaranteed to encounter a bug, sooner or later, where the wrong route is matched to an incoming URI. The only good way to prevent this that I know of is to write unit tests for your routes. Unfortunately, the name of the route is not actually part of the route itself. It is stored in a private, internal dictionary in the RouteCollection. So there is no way to write a unit test which examines the name of the route returned.

Let’s use the sample URI and route from earlier in this post as an example. Out of the entire URI, http://www.example.com:80/SiteRoot/Controller/Action?foo=A&bar=B#anchor, the routing system is only going to examine this bit when processing an incoming URI:

/Controller/Action?foo=A&bar=B#anchor

Does this match the url portion of the route defined above? Recall that it is defined as:

"{controller}/{action}/{id}"

Yes, it matches, because "Controller" can be interpreted as the "{controller}" portion of the url, "Action" for the "{action}" part, and a default value exists for the "{id}" portions (empty string). Note that this means that having default values in a route makes that route much more likely to "match" an incoming URI. If there are defaults for every token in a URL pattern and there are no constraints, then that route will match every incoming URI. The route shown in this post is created by the MVC framework as a "catch all" route for a new application, which contains only that route. That is why it has defaults for all of its values and no constraints; it is intended to match every incoming URI. If, on the other hand, you write a special-purpose route intended only for one particular controller or action, and you provide default values for all of the tokens, then you need to implement constraints to prevent this route for matching other controllers or actions.

Because the url matches, we have five values, so far, to pass as the route data. "controller" will have the value Controller, and "action" will have the value "Action". "id" will have the default value. Also, from the query, "foo" will have the value "A", and "bar" will have the value "B". Now the routing system considers any constraints on the route, but in this case, there aren’t any. Therefore, this list of values is handed to the MvcRouteHandler which was created by MapRoute.

Importantly, note that all of these values are strings. The routing system only deals with URIs, which are always strings. Now, your actions on your controllers can have arguments which are not strings, and the MVC framework will attempt to coerce the strings supplied by the routing system into whatever type is specified in the method signature. But that’s a feature of the MVC framework, not of the routing system. (Yes, the Values property returns type object, but, when matching an incoming URI, the segments are always added as strings.)

Not all URI patterns are as simple as the default route from the standard MVC application template. You can use delimiters other than /, for example. If you have hyphens, parentheses, etc. in your url template, these will be matched against the incoming URI to determine if the route is a match. The following route, for example, would not match the sample URI at the top of the post, even though it contains all of the necessary tokens and has no constraints:

routes.MapRoute(
    "Parenthesized",                                        // Route name
    "{controller}({action})({id})",                         // URL with parameters
    new { id = "" }                                         // Parameter defaults
);

The incoming URI contains no parentheses, so this route can’t match, because the parentheses are needed to to delimit the values for controller and action, and there are no defaults for this values.

Matching a Route With an Action

After a route is matched to an incoming URI, and a list of route values is created, control goes to the MvcRouteHandler. Now the MVC framework can invoke the action on the controller specified by the "action" and "controller" route values. Note that there is nothing special about these names/values to the routing system; they’re just two more values in the list from the routing system’s point of view. But these two names, "action" and "controller" are very important to the MVC framework, for obvious reasons.

MvcRouteHandler (indirectly) calls MvcHandler.ProcessRequest, which constructs the controller using the ControllerFactory, and then calls Controller.Execute, which, in turn, invokes the action using the ControllerActionInvoker. It is at this point that the values in the route are matched against the arguments for the action. In other words, tokenizing the URI and matching those values with action arguments are two entirely separate operations. The tokenizing is done by the routing system, and the matching route values with controller action arguments is done by the MVC framework.

In order to invoke the action, the MVC framework needs to look at the individual arguments to the action, and match them with route values. This is performed by the model binding system.

Note that the name of the argument does not have to correspond to a name in the route values if the model binding system can instantiate an instance of the type of the argument from the values which do exist. For example, if the type of the argument is FormCollection, then this argument can always be passed, no matter what names are present in the route values, and no matter what the name of the argument. The model binding system knows that it can always create a FormCollection by rolling up any values in the form in the incoming POST, if there are any. If there are not, it will simply return an empty FormCollection.

If the action takes an argument which cannot be instantiated based on the data in the route values, that is fine, so long as the argument is nullable. If the action requires a non-nullable argument which is not in the route values, or if the route values contain a value for an argument which cannot be bound into the type of that argument, the MVC framework will throw an exception. Otherwise, the action will be invoked.

Creating a URI With RouteLink

Creating an "a href=" tag with Html.RouteLink is trivial. The routing system looks up the route by name, and populates the URI with values from the RouteCollection you passed. Any values in the RouteCollection not included in the string in the url argument to MapRoute will be added to the resulting link as query string parameters. In my view, you should always prefer RouteLink over ActionLink, whenever there is any ambiguity as to which route will be returned.

Again, let’s consider an example, with the route defined in the MapRoute call earlier in this post:

<%= Html.RouteLink("Link text to display on page", "Default",
    new RouteValueDictionary(new {controller = "Foo", action = "Bar", foo = "A", bar = "B" })) %>

With this example, the order of the routes in the RouteCollection, the constraints, etc., are not needed to find the matching route, because we have specified ("Default") directly. Since there are no tokens called "foo" and "bar" in the url argument we passed to MapRoute, these will be appended to the generated URI as query string parameters. So we end up with the sample URI at the top of this post, less the anchor, provided that the site is configured to live in /SiteRoot.

Creating a URI With ActionLink

ActionLink is more complicated, because the routing system must first find an appropriate route, and then use that route to generate the URI. In a way, the method of finding the route is similar to the method used when parsing a URI, insofar as the first matching route "wins." But in another way, it is very different, because there is no incoming URI to use as a pattern for matching the partial template passed as the url argument to MapRoute. The only thing that the url argument is used for in this scenario is determining which tokens must be included, either directly, or via defaults, in order for a route to match. Let’s re-examine the same scenario considered in the RouteLink section, above, only this time using ActionLink. The call looks like this:

<%= Html.ActionLink("Link text to display on page", "Bar", "Foo",
    new RouteValueDictionary(new {foo = "A", bar = "B" })) %>

Does this match the route defined at the top of this post? Yes it does, and here’s why: That route contains three tokens in its url argument. They are controller, action, and id. I have supplied values for two of these tokens in the call to ActionLink, namely controller and action, via the controller and action arguments in the overload of ActionLink I am using here. The remaining required token, id, is not supplied in the ActionLink call at all. However, there was a default for it (empty string) in the original call to MapRoute, so the route matches even without an explicit value for id. Because foo and bar are present in the RouteValueDictionary, but the url template in the route does not contain {foo} or {bar}, these will be added to the generated URI as query string parameters. Again, the generated URI will be the sample URI at the top of this post, less the anchor, provided that the site is configured to live in /SiteRoot.

Conclusions

I hope this long explanation has taken some of the mystery out of what routing does. Since there’s a lot of detail above, I’m going to summarize some of the information which is important for day-to-day work.

  • Routing and the MVC framework are distinct features. MVC knows about Routing, but Routing does not know about MVC. Routing produces a list of values, but does not assign special meaning to them. MVC signs special meaning to the controller and action values.
  • Routes include information about a portion of the path in the URI and also the query, since any token not in the path is treated as part of the query.
  • The order of routes is very important, both for parsing incoming URIs and for generating URIs with ActionLink.
  • Default values in a route will cause the route to match more incoming URIs or sets of values in a RouteValueDictionary. Constraints in a route will cause the route to match fewer incoming URI this or sets of values in a RouteValueDictionary. If there are defaults for every token in a URL pattern and there are no constraints, then that route will match every incoming URI or sets of values in a RouteValueDictionary. For a "catch all" route like the one in the default MVC application, it is fine for all of the tokens to have default values in for there to be no constraints. If you write a special-purpose route for a particular controller or action, however, then you should probably use constraints to prevent it from ever matching any other controller or action.
  • If you have more than one route, then you simply must write unit tests to ensure that the correct route is selected for all incoming URIs or values used in your calls to ActionLink.

{ 5 } Comments

  1. David Glassborow | March 20, 2009 at 4:24 am | Permalink

    Craig have you had any experience using routing with normal asp.net as opposed to MVC ?

    I’m wondering if I could use it to at least tidy up the URL’s on an existing project.

  2. Khaja Minhajuddin | April 2, 2009 at 5:23 am | Permalink

    Thanks for this wonderful post, It definitely removes a lot of mystery from routing which is a cause of confusion for most guys new to MVC.

  3. Zakir Hossain | June 2, 2010 at 9:34 am | Permalink

    Wonderful!
    It realy helps me a lot learn inside of routing as a new MVC developer

  4. Carlton Fletcher | November 4, 2010 at 9:44 am | Permalink

    Hi - great post, and really helpful in understanding the way this works. I do have one question though please - where does the string for the "SiteRoot". Does it use the application name, or the directory name - the reason I ask is that RouteLink capitalises the first initial and it is driving me mad!

  5. Carlton Fletcher | November 4, 2010 at 10:11 am | Permalink

    Cheers Craig - I’ve just done some playing around renaming the application name for the virtual directory. It does get it from there thanks, but also frustratingly ignores the case! Thanks for getting back so quick - appreciate it.

Post a Comment

Your email is never published nor shared. Required fields are marked *

Bad Behavior has blocked 713 access attempts in the last 7 days.

Close