Introduction to Routing

Routing within the ASP.NET MVC framework serves two main purposes:

  • It matches incoming requests that would not otherwise match a file on the file system and maps the requests to a controller action.
  • It constructs outgoing URLs that correspond to controller actions.

The above two items only describe what Routing does in the context of an ASP.NET MVC application. Later in this chapter we'll dig deeper and uncover additional Routing features available for ASP.NET.

Comparing Routing to URL Rewriting

To better understand Routing, many developers compare it to URL Rewriting. After all, both approaches are useful in creating a separation between the incoming URL and what ends up handling the request and both of these techniques can be used to create pretty URLs for Search Engine Optimization (SEO) purposes.

The key difference is that URL Rewriting is focused on mapping one URL to another URL. For example, URL Rewriting is often used for mapping old sets of URLs to a new set of URLs. Contrast that to routing which is focused on mapping a URL to a resource.

You might say that routing embodies a resource-centric view of URLs. In this case, the URL represents a resource (not necessarily a page) on the Web. With ASP.NET Routing, this resource is a piece of code that executes when the incoming request matches the route. The route determines how the request is dispatched based on the characteristics of the URL—it doesn't rewrite the URL.

Another key difference is that Routing also helps generate URLs using the same mapping rules that it uses to match incoming URLs. URL rewriting only applies to incoming requests URLs and does not help in generating the original URL.

Another way to look at it is that ASP.NET Routing is more like bidirectional URL Rewriting. Where this comparison falls short is that ASP.NET Routing never actually rewrites your URL. The request URL that the user makes in the browser is the same URL your application sees throughout the entire request life cycle.

Defining Routes

Every ASP.NET MVC application needs at least one route to define how the application should handle requests but usually will end up with a handful or more. It's conceivable that a very complex application could have dozens of routes or more.

In this section, you'll see how to define routes. Route definitions start with the URL pattern, which specifies the pattern that the route will match. Along with the route URL, routes can also specify default values and constraints for the various parts of the URL, providing tight control over how and when the route matches incoming request URLs.

Routes can also have names which are associated with the route when that route is added to a route collection. We'll cover named routes a bit later.

In the following sections, you start with an extremely simple route and build up from there.

Route URLs

After you create a new ASP.NET MVC Web Application project, take a quick look at the code in Global.asax.cs. You'll notice that the Application_Start method contains a call to a method named the RegisterRoutes method. This method is where all routes for the application are registered.

Product Team Aside

UnFigure

Rather than adding routes to the RouteTable directly in the Application_Start method, we moved the code to add routes into a separate static method named RegisterRoutes to make writing unit tests of your routes easier. That way, it is very easy to populate a local instance of a RouteCollection with the same routes that you defined in Global.asax.cs simply by writing the following code within a unit test method:

download
var routes = new RouteCollection();
MvcApplication.RegisterRoutes(routes);

//Write tests to verify your routes here…

Code snippet 9-1.txt

For more details on unit testing routes, see the section “Testing Routes” in Chapter 12.

Let's clear out the routes in there for now and replace them with a very simple route. When you're done, your RegisterRoutes method should look like:

public static void RegisterRoutes(RouteCollection routes)
{
    routes.MapRoute("simple", "{first}/{second}/{third}");
}

Code snippet 9-2.txt

The simplest form of the MapRoute method takes in a name for the route and the URL pattern for the route. The name is discussed later. For now, let's focus on the URL pattern.

Table 9.1 shows how the route we just defined in Code Snippet 9-2 will parse certain URLs into a dictionary of keys and values stored in an instance of a RouteValueDictionary to give you an idea of how URLs are decomposed by routes into important pieces of information used later in the request pipeline.

Table 9.1: URL Parameter Value Mapping Examples

URL URL Parameter Values
/albums/display/123 first = “albums”
second = “display”
third = “123”
/foo/bar/baz first = “foo”
second = “bar”
third = “baz”
/a.b/c-d/e-f first = “a.b”
second = “c-d”
third = “e-f”

Notice that the route URL in Code Snippet 9-2 consists of several URL segments (a segment is everything between slashes but not including the slashes), each of which contains a parameter delimited using curly braces. These parameters are referred to as URL parameters.

This is a pattern-matching rule used to determine if this route applies to an incoming request. In this example, this rule will match any URL with three segments because a URL parameter, by default, matches any nonempty value. When this route matches a URL with three segments, the text in the first segment of that URL corresponds to the {first} URL parameter, the value in the second segment of that URL corresponds to the {second} URL parameter, and the value in the third segment corresponds to the {third} parameter.

You can name these parameters almost anything you'd like (alphanumeric characters are allowed as well as a few other characters), as we did in this case. When a request comes in, Routing parses the request URL and places the route parameter values into a dictionary (specifically a RouteValueDictionary accessible via the RequestContext), using the URL parameter names as the keys and the corresponding subsections of the URL (based on position) as the values.

Later you'll learn that when using routes in the context of an MVC application, certain parameter names carry a special purpose. Table 9.1 displays how the route just defined will convert certain URLs into a RouteValueDictionary.

Route Values

If you actually make a request to the URLs listed in Table 9.1, you'll notice that a request for your application ends up returning a 404 File Not Found error. Although you can define a route with any parameter names you'd like, certain special parameter names are required by ASP.NET MVC in order to function correctly—{controller} and {action}.

The value of the {controller} parameter is used to instantiate a controller class to handle the request. By convention, MVC appends the suffix Controller to the value of the {controller} URL parameter and attempts to locate a type of that name (case insensitively) that also implements the System.Web.Mvc.IController interface.

Going back to the simple route example, let's change it from:

routes.MapRoute("simple", "{first}/{second}/{third}");

to:

routes.MapRoute("simple", "{controller}/{action}/{id}");

Code snippet 9-3.txt

so that it contains the MVC-specific URL parameter names.

If we look again at the first example in the Table 9.1 and apply it to this updated route, you see that the request for /albums/display/123 is now a request for a {controller} named albums. ASP.NET MVC takes that value and appends the Controller suffix to get a type name, AlbumsController. If a type with that name exists and implements the IController interface, it is instantiated and used to handle the request.

The {action} parameter value is used to indicate which method of the controller to call in order to handle the current request. Note that this method invocation applies only to controller classes that inherit from the System.Web.Mvc.Controller base class. Classes that directly implement IController can implement their own conventions for handling mapping code to handle the request.

Continuing with the example of /albums/display/123, the method of AlbumsController that MVC will invoke is named Display.

Note that while the third URL in Table 9.1 is a valid route URL, it will not match any controller and action because it attempts to instantiate a controller named a.bController and calls the method named c-d, which is of course not a valid method name!

Any route parameters other than {controller} and {action} can be passed as parameters to the action method, if they exist. For example, assuming the following controller:

public class AlbumsController : Controller 
{
  public ActionResult Display(int id)
  {
    //Do something
    return View();
  }
}

Code snippet 9-4.txt

a request for /albums/display/123 will cause MVC to instantiate this class and call the Display method, passing in 123 for the id.

In the previous example with the route URL {controller}/{action}/{id}, each segment contains a URL parameter that takes up the entire segment. This doesn't have to be the case. Route URLs do allow for literal values within the segments. For example, you might be integrating MVC into an existing site and want all your MVC requests to be prefaced with the word site; you could do this as follows:

site/{controller}/{action}/{id}

Code snippet 9-5.txt

This indicates that first segment of a URL must start with “site” in order to match this request. Thus, /site/albums/display/123 matches this route, but /albums/display/123 does not match.

It is even possible to have URL segments that mix literals with parameters. The only restriction is that two consecutive URL parameters are not allowed. Thus:

{language}-{country}/{controller}/{action}
{controller}.{action}.{id}

are valid route URLs, but:

{controller}{action}/{id}

Code snippet 9-6.txt

is not a valid route. There is no way for the route to know when the controller part of the incoming request URL ends and when the action part should begin.

Looking at some other samples (shown in Table 9.2) will help you see how the URL pattern corresponds to matching URLs.

Table 9.2: Route URL Patterns and Examples

Route URL Pattern Examples of URLs That Match
{controller}/{action}/{genre} /albums/list/rock
service/{action}-{format} /service/display-xml
{report}/{year}/{month}/{day} /sales/2008/1/23

Route Defaults

So far, the chapter has covered defining routes that contain a URL pattern for matching URLs. It turns out that the route URL is not the only factor taken into consideration when matching requests. It's also possible to provide default values for a route URL parameter. For example, suppose that you have an action method that does not have a parameter:

download
public class AlbumsController : Controller 
{
  public ActionResult List()
  {
    //Do something
    return View();
  }
}

Code snippet 9-7.txt

Naturally, you might want to call this method via the URL:

/albums/list

Code snippet 9-8.txt

However, given the route URL defined in the previous snippet, {controller}/{action}/{id}, this won't work because this route matches only URLs containing three segments and /albums/list contains only two segments.

At this point, it would seem you need to define a new route that looks like the route defined in the previous snippet, but with only two segments: {controller}/{action}. Wouldn't it be nice if you didn't have to define another route and could instead indicate to the route that the third segment is optional when matching a request URL?

Fortunately, you can! The routing API allows you to supply default values for parameter segments. For example, you can define the route like this:

routes.MapRoute("simple", "{controller}/{action}/{id}", 
  new {id = UrlParameter.Optional});

Code snippet 9-9.txt

The {id = UrlParameter.Optional} snippet defines a default value for the {id} parameter. This default allows this route to match requests for which the id parameter is missing. In other words, this route now matches any URL with two or three segments instead of matching only three-segment URLs.

note

Note that the same thing can also be accomplished by setting the id to be an empty string: {id = “”}. This seems a lot more concise, so why not use this? What's the difference?

Remember earlier when we mentioned that URL parameter values are parsed out of the URL and put into a dictionary? Well when you use UrlParameter.Optional as a default value and no value is supplied in the URL, routing doesn't even add an entry to the dictionary. If the default value is set to an empty string, the route value dictionary will contain a value with the key “id” and the value as an empty string. In some cases, this distinction is important. It lets you know the difference between the id not being specified, and it being specified but left empty.

This now allows you to call the List action method, using the URL /albums/list, which satisfies our goal, but let's see what else we can do with defaults.

Multiple default values can be provided. The following snippet demonstrates providing a default value for the {action} parameter as well:

routes.MapRoute("simple"
  , "{controller}/{action}/{id}"
  , new {id = UrlParameter.Optional, action="index"});

Code snippet 9-10.txt

Product Team Aside

UnFigure

We're using shorthand syntax here for defining a dictionary. Under the hood, the MapRoute method converts the new {id=UrlParameter.Optional, action=“index”} into an instance of RouteValueDictionary, which we'll talk more about later. The keys of the dictionary are “id” and “action” with the respective values being UrlParameter.Optional and “index”. This syntax is a neat way for turning an object into a dictionary by using its property names as the keys to the dictionary and the property values as the values of the dictionary. The specific syntax we use here creates an anonymous type using the object initializer syntax. It may feel unusual initially, but we think you'll soon grow to appreciate its terseness and clarity.

This example supplies a default value for the {action} parameter within the URL via the Defaults dictionary property of the Route class. Typically the URL pattern of {controller}/{action} would require a two-segment URL in order to be a match. But by supplying a default value for the second parameter, this route no longer requires that the URL contain two segments to be a match. The URL may now simply contain the {controller} parameter and omit the {action} parameter to match this route. In that case, the {action} value is supplied via the default value rather than the incoming URL.

Let's revisit the previous table on route URL patterns and what they match, and now throw defaults into the mix, shown in Table 9.3.

Table 9.3: URL Patterns and What They Match

Route URL Pattern Defaults Examples of URLs That Match
{controller}/{action}/{id} new {id = URLParameter
.Optional}
/albums/display/123
/albums/display
{controller}/{action}/{id} new {controller =
“home”,
action = “index”, id =
UrlParameter.Optional}
/albums/display/123
/albums/display
/albums
/

One thing to understand is that the position of a default value relative to other URL parameters is important. For example, given the URL pattern {controller}/{action}/{id}, providing a default value for {action} without specifying a default for {id} is effectively the same as not having a default value for {action}. Routing will allow such a route, but it's not particularly useful. Why is that, you ask?

A quick example will make the answer to this question clear. Suppose you had the following two routes defined, the first one containing a default value for the middle {action} parameter:

routes.MapRoute("simple", "{controller}/{action}/{id}", new {action="index "});
routes.MapRoute("simple2", "{controller}/{action}");

Now if a request comes in for /albums/rock, which route should it match? Should it match the first because you provide a default value for {action}, and thus {id} should be “rock”? Or should it match the second route, with the {action} parameter set to “rock”?

In this example, there is an ambiguity about which route the request should match. To avoid these type of ambiguities the routing engine only uses a particular default value when every subsequent parameter also has a default value defined. In this example, if we have a default value for {action} we should also provide a default value for {id}.

Routing interprets default values slightly differently when there are literal values within a URL segment. Suppose that you have the following route defined:

routes.MapRoute("simple", "{controller}-{action}", new {action = "index"});

Code snippet 9-11.txt

Notice that there is a string literal “-” between the {controller} and {action} parameters. It is clear that a request for /albums-list will match this route, but should a request for /albums- match? Probably not, because that makes for an awkward-looking URL.

It turns out that with Routing, any URL segment (the portion of the URL between two slashes) with literal values must not leave out any of the parameter values when matching the request URL. The default values in this case come into play when generating URLs, which is covered later in the section, “Under the Hood: How Routes Generate URLs.”

Route Constraints

Sometimes, you need more control over your URLs than specifying the number of URL segments. For example, take a look at the following two URLs:

Each of these URLs contains three segments and would each match the default route you've been looking at in this chapter thus far. If you're not careful you'll have the system looking for a controller called 2008Controller and a method called 01! However, just by looking at these URLs you can tell they should map to different things. How can we make that happen?

This is where constraints are useful. Constraints allow you to apply a regular expression to a URL segment to restrict whether or not the route will match the request. For example:

download
routes.MapRoute("blog", "{year}/{month}/{day}"
  , new {controller="blog", action="index"}
  , new {year=@"d{4}", month=@"d{2}", day=@"d{2}"});

routes.MapRoute("simple", "{controller}/{action}/{id}");

Code snippet 9-12.txt

In the preceding snippet, the first route contains three URL parameters, {year}, {month}, and {day}. Each of those parameters map to a constraint in the constraints dictionary specified using an anonymous object initializer, {year=@“d{4}”, month=@“d{2}”, day=@“d{2}”}. As you can see, the keys of the constraints dictionary map to the route's URL parameters. Thus the constraint for the {year} segment is d{4}, a regular expression that only matches strings containing exactly four digits.

The format of this regular expression string is the same as that used by the .NET Framework's Regex class (in fact, the Regex class is used under the hood). If any of the constraints do not match, the route is not a match for the request, and routing moves onto the next route.

If you're familiar with regular expressions, you'd know that the regular expression d{4} actually matches any string containing four consecutive digits such as “abc1234def.”

Routing automatically wraps the specified constraint expression with and $ characters to ensure that the value exactly matches the expression. In other words, the actual regular expression used in this case is “∧d{4}$” and not d{4} to make sure that “1234” is a match, but “abc1234def” is not.

Thus the first route defined in Snippet 9-12 matches /2008/05/25 but doesn't match /08/05/25 because 08 is not a match for the regular expression d{4} and thus the year constraint is not satisfied.

note

Note that we put our new route before the default simple route. Note that routes are evaluated in order. Because a request for /2008/06/07 would match both defined routes, we need to put the more specific route first.

By default, constraints use regular expression strings to perform matching on a request URL, but if you look carefully, you'll notice that the constraints dictionary is of type RouteValueDictionary, which implements IDictionary<string, object>. This means the values of that dictionary are of type object, not of type string. This provides flexibility in what you pass as a constraint value. You'll see how to take advantage of that in the “Custom Route Constraints” section.

Named Routes

Routing in ASP.NET doesn't require that you name your routes, and in many cases it seems to work just fine without using names. To generate a URL, simply grab a set of route values you have lying around, hand it to the routing engine, and let the routing engine sort it all out. But as we'll see in this section, there are cases where this can break down due to ambiguities between which route should be chosen to generate a URL. Named routes solve this problem by giving precise control over route selection when generating URLs.

For example, suppose an application has the following two routes defined:

public static void RegisterRoutes(RouteCollection routes)
{
    routes.MapRoute(
        name: "Test",
        url: "code/p/{action}/{id}",
        defaults: new { controller = "Section", action = "Index", id = "" }
    );

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

To generate a hyperlink to each route from within a view, you'd write the following code.

@Html.RouteLink("Test", new {controller="section", action="Index", id=123})
@Html.RouteLink("Default", new {controller="Home", action="Index", id=123})

Notice that these two method calls don't specify which route to use to generate the links. They simply supply some route values and let the ASP.NET Routing engine figure it all out. In this example, the first method generates a link to the URL /code/p/Index/123 and the second to /Home/Index/123, which should match your expectations.

This is fine for these simple cases, but there are situations where this can bite you.

Let's suppose you add the following page route at the beginning of your list of routes so that the URL /static/url is handled by the page /aspx/SomePage.aspx:

routes.MapPageRoute("new", "static/url", "∼/aspx/SomePage.aspx");

Note that you can't put this route at the end of the list of routes within the RegisterRoutes method because it would never match incoming requests. Why wouldn't it? Well a request for /static/url would be matched by the default route. Therefore you need to add this route to the beginning of the list of routes before the default route.

note

Note this problem isn't specific to routing with Web Forms; there are many cases where you might route to a non ASP.NET MVC route handler.

Moving this route to the beginning of the defined list of routes seems like an innocent enough change, right? For incoming requests, this route will match only requests that exactly match /static/url but will not match any other requests. This is exactly what you want. But what about generated URLs? If you go back and look at the result of the two calls to Url.RouteLink, you'll find that both URLs are broken:

/url?controller=section&action=Index&id=123

and

/static/url?controller=Home&action=Index&id=123

Huh?!

This goes into a subtle behavior of routing, which is admittedly somewhat of an edge case, but is something that people run into from time to time.

Typically, when you generate a URL using routing, the route values you supply are used to “fill in” the URL parameters as discussed earlier in this chapter.

When you have a route with the URL {controller}/{action}/{id}, you're expected to supply values for controller, action, and id when generating a URL. In this case, because the new route doesn't have any URL parameters, it matches every URL generation attempt because technically, “a route value is supplied for each URL parameter.” It just so happens that there aren't any URL parameters. That's why all the existing URLs are broken because every attempt to generate a URL now matches this new route.

This might seem like a big problem, but the fix is very simple. Use names for all your routes and always use the route name when generating URLs. Most of the time, letting Routing sort out which route you want to use to generate a URL is really leaving it to chance, which is not something that sits well with the obsessive-compulsive control freak developer. When generating a URL, you generally know exactly which route you want to link to, so you might as well specify it by name.

Specifying the name of the route not only avoids ambiguities, but it may even eke out a bit of a performance improvement because the routing engine can go directly to the named route and attempt to use it for URL generation.

In the previous example where you generated two links, the following change fixes the issue (I changed the code to use named parameters to make it clear what the change was):

@Html.RouteLink(
    linkText: "route: Test", 
    routeName: "test", 
    routeValues: new {controller="section", action="Index", id=123}
)

@Html.RouteLink(
    linkText: "route: Default", 
    routeName: "default", 
    routeValues: new {controller="Home", action="Index", id=123}
)

As Elias Canetti, the famous Bulgarian novelist noted, “People's fates are simplified by their names.” The same is true for URL generation with Routing.

MVC Areas

Areas, introduced in ASP.NET MVC 2, allow you to divide your models, views, and controllers into separate functional sections. This means you can separate larger or more complex sites into sections, which can make them a lot easier to manage.

Area Route Registration

Area routes are configured by creating classes for each area that derive from the AreaRegistration class, overriding AreaName and RegisterArea members. In the default project templates for ASP.NET MVC, there's a call to the method AreaRegistration.RegisterAllAreas within the Application_Start method in Global.asax.

You'll see a complete example in Chapter 13, but it's good to know what that AreaRegistration.RegisterAllAreas call is about when you're working with routes.

Area Route Conflicts

If you have two controllers with the same name, one within an area and one in the root of your application, you may run into an exception with a rather verbose error message when a request matches the route without a namespace:

Multiple types were found that match the controller named ‘Home’. This can happen if the route that services this request (‘{controller}/{action}/{id}’) does not specify namespaces to search for a controller that matches the request. If this is the case, register this route by calling an overload of the ‘MapRoute’ method that takes a ‘namespaces’ parameter.

The request for ‘Home’ has found the following matching controllers:

AreasDemoWeb.Controllers.HomeController

AreasDemoWeb.Areas.MyArea.Controllers.HomeController

When using the Add Area dialog to add an area, a route is registered for that area with a namespace for that area. This ensures that only controllers within that area match the route for the area.

Namespaces are used to narrow down the set of controllers that are considered when matching a route. When a route has a namespace defined, only controllers that exist within that namespace are valid as a match. But in the case of a route that doesn't have a namespace defined, all controllers are valid.

That leads to this ambiguity where two controllers of the same name are a match for the route without a namespace.

One way to prevent that exception is to use unique controller names within a project. However, you may have good reasons to use the same controller name (for example, you don't want to affect your generated route URLs). In that case, you can specify a set of namespaces to use for locating controller classes for a particular route. Listing 9.1 shows how you'd do that:

download
Listing 9.1: Listing 9-1.txt
routes.MapRoute(
    "Default",                                             
    "{controller}/{action}/{id}",                          
    new { controller = "Home", action = "Index", id = "" },
    new [] { "AreasDemoWeb.Controllers" }
);

The preceding code uses a fourth parameter that is an array of namespace names. The controllers for the example project live in a namespace called AreasDemoWeb.Controllers.

Catch-All Parameter

A catch-all parameter allows for a route to match a URL with an arbitrary number of segments. The value put in the parameter is the rest of the URL sans query string.

For example, the route in Listing 9.2 would handle requests like the ones shown in Table 9.4.

download
Listing 9.2: Listing 9-2.txt
public static void RegisterRoutes(RouteCollection routes)
{
    routes.MapRoute("catchallroute", "query/{query-name}/{*extrastuff}");
}

Table 9.4: Listing 9-2 Requests

URL Parameter value
/query/select/a/b/c extrastuff = “a/b/c”
/query/select/a/b/c/ extrastuff = “a/b/c”
/query/select/ extrastuff = “” (Route still matches. The catch- all just catches the empty string in this case.)

Multiple URL Parameters in a Segment

As mentioned earlier, a route URL may have multiple parameters per segment. For example, all the following are valid route URLs:

  • {title}-{artist}
  • Album{title}and{artist}
  • {filename}.{ext}

To avoid ambiguity, parameters cannot be adjacent. For example, the following are invalid:

  • {title}{artist}
  • Download{filename}{ext}

When matching incoming requests, literals within the route URL are matched exactly. URL parameters are matched greedily, which has the same connotations as it does with regular expressions. In other terms, the route tries to match as much text as possible with each URL parameter.

For example, looking at the route {filename}.{ext}, how would it match a request for /asp.net.mvc.xml? If {filename} were not greedy, it would match only “asp” and the {ext} parameter would match “net.mvc.xml”. But because URL parameters are greedy, the {filename} parameter matches everything it can, “asp.net.mvc”. It cannot match any more because it must leave room for the .{ext} portion to match the rest of the URL, “xml.

Table 9.5 demonstrates how various route URLs with multiple parameters would match. Note that you use the shorthand for {foo=bar} to indicate that the URL parameter {foo} has a default value “bar.

Table 9.5: Matching Route URLs with Multiple Parameters

Route URL Request URL Route Data Result
{filename}.{ext} /Foo.xml.aspx filename=“Foo.xml”
ext=“aspx”
My{title}-{cat} /MyHouse-dwelling location=“House”
sublocation=“dwelling”
{foo}xyz{bar} /xyzxyzxyzblah foo=“xyzxyz”
bar=“blah”

Note that in the first example, when matching the URL /Foo.xml.aspx, the {filename} parameter did not stop at the first literal “.” character, which would result in it only matching the string “foo.” Instead, it was greedy and matched “Foo.xml.”

StopRoutingHandler and IgnoreRoute

By default, routing ignores requests that map to physical files on disk. That's why requests for files such as CSS, JPG, and JS files are ignored by routing and handled in the normal manner.

But in some situations, there are requests that don't map to a file on disk that you don't want routing to handle. For example, requests for ASP.NET's web resource handlers, WebResource.axd, are handled by an http handler and don't correspond to a file on disk.

One way to ensure that routing ignores such requests is to use the StopRoutingHandler. Listing 9.3 shows adding a route the manual way, by creating a route with a new StopRoutingHandler and adding the route to the RouteCollection.

download
Listing 9.3: Listing 9-3.txt
public static void RegisterRoutes(RouteCollection routes)
{
    routes.Add(new Route
    (
         "{resource}.axd/{*pathInfo}", 
         new StopRoutingHandler()
    ));

    routes.Add(new Route
    (
        "reports/{year}/{month}"
        , new SomeRouteHandler()
    ));
}

If a request for /WebResource.axd comes in, it will match that first route. Because the first route returns a StopRoutingHandler, the routing system will pass the request on to normal ASP.NET processing, which in this case falls back to the normal HTTP handler mapped to handle the .axd extension.

There's an even easier way to tell routing to ignore a route, and it's aptly named IgnoreRoute. It's an extension method that's added to the RouteCollection type just like MapRoute, which you've seen before. It's a convenience, and using this new method along with MapRoute changes Listing 9.3 to look like Listing 9.4.

download
Listing 9.4: Listing 9-4.txt
public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
    routes.MapRoute("report-route", "reports/{year}/{month}");
}

Isn't that cleaner and easier to look at? You'll find a number of places in ASP.NET MVC where extension methods like MapRoute and IgnoreRoute can make things a bit tidier.

Debugging Routes

It used to be really frustrating to debug problems with routing because routes are resolved by ASP.NET's internal route processing logic, beyond the reach of Visual Studio breakpoints. A bug in your routes can break your application because it either invokes an incorrect controller action or none at all. Things can be even more confusing because routes are evaluated in order, with the first matching route taking effect, so your routing bug may not be in the route definition at all, but in its position in the list. All this used to make for frustrating debugging sessions, that is, before I wrote the Routing Debugger.

When the Routing Debugger is enabled it replaces all of your routes' route handlers with a DebugRouteHandler. This route handler traps all incoming requests and queries every route in the route table to display diagnostic data on the routes and their route parameters at the bottom of the page.

To use the RouteDebugger, simply use NuGet to install it via the following command, Install-Package RouteDebugger. This package adds the RouteDebugger assembly and adds a setting to the appSettings section of web.config used to turn route debugging on or off:

UnFigure
Listing 9.5: Listing 9-5.txt
<add key="RouteDebugger:Enabled" value="true" />

As long as the Route Debugger is enabled, it will display the route data pulled from the request of the current request in the address bar (see Figure 9.1). This enables you to type in various URLs in the address bar to see which route matches. At the bottom, it shows a list of all defined routes in your application. This allows you to see which of your routes would match the current URL.

note

I provided the full source for the Routing Debugger, so you can modify it to output any other data that you think is relevant. For example, Stephen Walther used the Routing Debugger as the basis of a Route Debugger Controller. Because it hooks in at the Controller level, it's only able to handle matching routes, which makes it less powerful from a pure debugging aspect, but it does offer a benefit in that it can be used without disabling the routing system. Although it's debatable whether you should be unit-testing routes, you could use this Route Debugger Controller to perform automated tests on known routes. Stephen's Route Debugger Controller is available from his blog at http://tinyurl.com/RouteDebuggerController.

Under the Hood: How Routes Generate URLs

So far, this chapter has focused mostly on how routes match incoming request URLs, which is the primary responsibility for routes. Another responsibility of the routing system is to construct a URL that corresponds to a specific route. When generating a URL, a request for that generated URL should match the route that was selected to generate the URL in the first place. This allows routing to be a complete two-way system for handling both outgoing and incoming URLs.

Product Team Aside

UnFigure

Let's take a moment and examine those two sentences. “When generating a URL, a request for that generated URL should match the route that was selected to generate the URL in the first place. This allows routing to be a complete two-way system for handling both outgoing and incoming URLs.” This is the point where the difference between routing and URL rewriting becomes clear. Letting the routing system generate URLs also separates concerns between not just the model, view, and the controller, but also the powerful but silent fourth player, Routing.

In principle, developers supply a set of route values that the routing system uses to select the first route that is capable of matching the URL.

High-Level View of URL Generation

At its core, the routing system employs a very simple algorithm over a simple abstraction consisting of the RouteCollection and RouteBase classes. Before digging into how routing interacts with the more complex Route class, let's first look at how routing works with these classes.

A variety of methods are used to generate URLs, but they all end up calling one of the two overloads of the RouteCollection.GetVirtualPath method. The following listing shows the method signatures for the two overloads:

public VirtualPathData GetVirtualPath(RequestContext requestContext,
  RouteValueDictionary values)
public VirtualPathData GetVirtualPath(RequestContext requestContext, string name,
  RouteValueDictionary values)

The first method receives the current RequestContext and user-specified route values (dictionary) used to select the desired route.

1. The route collection loops through each route and asks, “Can you generate a URL given these parameters?” via the Route.GetVirtualPath method. This is similar to the matching logic that applies when matching routes to an incoming request.

2. If a route answers that question (that is, it matches), it returns a VirtualPathData instance containing the URL as well as other information about the match. If not, it returns null, and the routing system moves on to the next route in the list.

The second method accepts a third argument, the route name. Route names are unique within the route collection—no two routes can have the same name. When the route name is specified, the route collection doesn't need to loop through each route. Instead, it immediately finds the route with the specified route name and moves to step 2. If that route doesn't match the specified parameters, then the method returns null and no other routes are evaluated.

Detailed Look at URL Generation

The Route class provides a specific implementation of the preceding high-level algorithm.

Simple Case

This is the logic most developers encounter when using routing and is detailed in the following steps.

1. User calls RouteCollection.GetVirtualPath, passing in a RequestContext, a dictionary of values, and an optional route name used to select the correct route to generate the URL.

2. Routing looks at the required URL parameters of the route (URL parameters that do not have default values supplied) and makes sure that a value exists in the supplied dictionary of route values for each required parameter. If any required parameter does not have a value, URL generation stops immediately and returns null.

3. Some routes may contain default values that do not have a corresponding URL parameter. For example, a route might have a default value of “pastries” for a key named category, but category is not a parameter in the route URL. In this case, if the user-supplied dictionary of values contains a value for category, that value must match the default value for category. Figure 9.2 shows a flowchart example.

4. Routing then applies the route's constraints, if any. Refer to Figure 9.3 for each constraint.

5. The route is a match! Now the URL is generated by looking at each URL parameter and attempting to fill it with the corresponding value from the supplied dictionary.

Ambient Route Values

In some scenarios URL generation makes use of values that were not explicitly supplied to the GetVirtualPath method by the caller. Let's look at a scenario for an example of this.

Simple Case

Suppose you want to display a large list of tasks. Rather than dumping them all on the page at the same time, you may want to allow users to page through them via links.

For example, Figure 9.4 shows a very simple interface for paging through the list of tasks.

The Previous and Next buttons are used to navigate to the previous and next pages of data, but all these requests are handled by the same controller and action.

The following route handles these requests:

download
public static void RegisterRoutes(RouteCollection routes)
{
   routes.MapRoute("tasks", "{controller}/{action}/{page}",
       new {controller="tasks", action="list", page=0 });
}

Code snippet 9-13.txt

In order to generate links to the previous and next page, we'd typically need to specify all the URL parameters in the route. So to generate a link to page 2, we might use the following code in the view:

@Html.ActionLink("Page 2", "List", 
  new {controller="tasks", action="List", page = 2})

However we can shorten this by taking advantage of ambient route values. The following is the URL for page 2 of our list of tasks.

/tasks/list/2

The route data for this request looks like this (Table 9.6):

Table 9.6: Route Data

Key Value
Controller tasks
Action List
Page 2

To generate the URL for the next page, we only need to specify the route data that will change in the new request.

@Html.ActionLink("Page 2", "List", new { page  2})

Code snippet 9-14.txt

Even though the call to ActionLink supplied only the page parameter, the routing system used the ambient route data values for the controller and action when performing the route lookup. The ambient values are the current values for those parameters within the RouteData for the current request. Explicitly supplied values for the controller and action would, of course, override the ambient values.

Overflow Parameters

Overflow parameters are route values used in URL generation that are not specified in the route's definition. By definition we mean the route's URL, its defaults dictionary, and its constraints dictionary. Note that ambient values are never used as overflow parameters.

Overflow parameters used in route generation are appended to the generated URL as query string parameters.

Again, an example is most instructive in this case. Assume that the following default route is defined:

download
public static void RegisterRoutes(RouteCollection routes) 
{
    routes.MapRoute(
      "Default",
      "{controller}/{action}/{id}",
      new { controller = "Home", action = "Index", id = UrlParameter.Optional }
    );
} 

Code snippet 9-15.txt

Now suppose you're generating a URL using this route and you pass in an extra route value, page = 2. Notice that the route definition doesn't contain a URL parameter named “page.” In this example, instead of generating a link, you'll just render out the URL using the Url.RouteUrl method.

@Url.RouteUrl(new {controller="Report", action="List", page="123"})

Code snippet 9-16.txt

The URL generated will be /Report/List?page=2. As you can see, the parameters we specified are enough to match the default route. In fact, we've specified more parameters than needed. In those cases, those extra parameters are appended as query string parameters. The important thing to note is that routing is not looking for an exact match when determining which route is a match. It's looking for a sufficient match. In other words, as long as the specified parameters meet the route's expectations, it doesn't matter if there are extra parameters specified.

More Examples of URL Generation with the Route Class

Let's assume that the following route is defined:

download
void Application_Start(object sender, EventArgs e) 
{
    routes.MapRoute("report",
        "reports/{year}/{month}/{day}",
        new {day = 1}
    );    
}

Code snippet 9-17.txt

Here are some results of some Url.RouteUrl calls that take the following general form:

@Url.RoutUrl(new {param1 = value1, parm2 = value2, ..., parmN, valueN})

Code snippet 9-18.txt

Parameters and the resulting URL are shown in Table 9.7.

Table 9.7: Parameters and Resulting URL for GetVirtualPath

Parameters Resulting URL Reason
year=2007, month=1, day=12 /reports/2007/1/12 Straightforward matching
year=2007, month=1 /reports/2007/1 Default for day = 1
Year=2007, month=1, day=12, category=123 /reports/2007/1/12?category=123 “Overflow” parameters go into query string in generated URL.
Year=2007 Returns null. Not enough parameters supplied for a match
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.221.136.142