Routing - Professional ASP.NET MVC 5 (2014)

Professional ASP.NET MVC 5 (2014)

Chapter 9
Routing

—by Phil Haack and David Matson

What's In This Chapter?

· Understanding URLs

· Introduction to Routing

· A peek under the Routing hood

· A look at advanced Routing

· Routing extensibility and magic

· Using Routing with Web Forms

When it comes to source code, software developers are notorious for fixating on little details to the point of obsessive compulsion. We'll fight fierce battles over code indentation styles and the placement of curly braces. In person, such arguments threaten to degenerate into all-out slap fights.

So, it comes as a bit of a surprise when you approach a majority of sites built using ASP.NET and encounter a URL that looks like this: http://example.com/albums/list.aspx?catid=17313&genreid=33723&page=3.

For all the attention we pay to code, why not pay the same amount of attention to the URL? It might not seem important, but the URL is a legitimate and widely used user interface for the Web.

This chapter helps you map logical URLs to action methods on controllers. It also covers the ASP.NET Routing feature, which is a separate API that the ASP.NET MVC framework makes heavy use of to map URLs to method calls. The chapter covers both traditional routing as well as the new attribute routing introduced in ASP.NET MVC 5. The chapter first covers how MVC uses Routing and then takes a peek under the hood at Routing as a standalone feature.

UNIFORM RESOURCE LOCATORS

Usability expert Jakob Nielsen (www.useit.com) urges developers to pay attention to URLs and provides the following guidelines for high-quality URLs. You should provide

· A domain name that is easy to remember and easy to spell

· Short URLs

· Easy-to-type URLs

· URLs that reflect the site structure

· URLs that are hackable to allow users to move to higher levels of the information architecture by hacking off the end of the URL

· Persistent URLs, which don't change

Traditionally, in many web frameworks such as Classic ASP, JSP, PHP, and ASP.NET, the URL represents a physical file on disk. For example, when you see a request for http://example.com/albums/list.aspx, you can bet your kid's tuition that the website has a directory structure that contains an albums folder and a List.aspx file within that folder.

In this case, a direct relationship exists between the URL and what physically exists on disk. A request for this URL is received by the web server, which executes some code associated with this file to produce a response.

This 1:1 relationship between URLs and the file system is not the case with most MVC-based web frameworks, such as ASP.NET MVC. These frameworks generally take a different approach by mapping the URL to a method call on a class, rather than some physical file.

As you saw in Chapter 2, these classes are generally called controllers because their purpose is to control the interaction between the user input and other components of the system. The methods that serve up the response are generally called actions. These represent the various actions the controller can process in response to user input requests.

This might feel unnatural to those who are accustomed to thinking of URLs as a means of accessing a file, but consider the acronym URL itself, Uniform Resource Locator. In this case, Resource is an abstract concept. It could certainly mean a file, but it can also be the result of a method call or something else entirely.

URI generally stands for Uniform Resource Identifier. A URI is a string that identifies a resource. All URLs are technically URIs. The W3C has said, at www.w3.org/TR/uri-clarification/#contemporary, that a “URL is a useful but informal concept: A URL is a type of URI that identifies a resource via a representation of its primary access mechanism.” In other words, a URI just identifies a resource, but a URL also tells you how to get it.

Arguably this is all just semantics, and most people will get your meaning regardless of which name you use. However, this discussion might be useful to you as you learn MVC because it acts as a reminder that a URL doesn't necessarily mean a physical location of a static file on a web server's hard drive somewhere; it most certainly doesn't in the case of ASP.NET MVC. All that said, we use the conventional term URL throughout the book.

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 it maps the requests to a controller action.

· It constructs outgoing URLs that correspond to controller actions.

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

Note

One constant area of confusion about Routing is its relationship to ASP.NET MVC. In its pre-beta days, Routing was an integrated feature of ASP.NET MVC. However, the team saw that it would have a useful future as a fundamental feature of ASP.NET that even Web Pages could build on, so it was extracted into its own assembly and made part of the core ASP.NET framework. The proper name for the feature is ASP.NET Routing, but everyone simply shortens it to Routing.

Putting this feature into ASP.NET meant that it became a part of the .NET Framework (and, by association, Windows). So, although new versions of ASP.NET MVC ship often, Routing is constrained by the schedule of the larger .NET Framework; hence, it hasn't changed much over the years.

ASP.NET Web API is hostable outside of ASP.NET, which means it can't use ASP.NET Routing directly. Instead, it introduces a clone of the Routing code. But when ASP.NET Web API is hosted on ASP.NET, it mirrors all the Web API routes into the core ASP.NET Routing's set of routes. Chapter 11 covers Routing as it applies to ASP.NET Web API.

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. Additionally, both techniques can be used to create prettyURLs 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 applies only to incoming requests 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. However, this comparison falls short because 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.

Routing Approaches

Now that you understand what routes do, you'll start looking at how to define your routes. MVC has always supported a centralized, imperative, code-based style of defining routes that we'll call traditional routing. This is a good option that is still fully supported. However, MVC 5 adds a second option using declarative attributes on your controller classes and action methods, which is called attribute routing. This new option is simpler and keeps your route URLs together with your controller code. Both options work well, and both are flexible enough to handle complex routing scenarios. Which option to choose is largely a matter of style and preference.

We'll start with the simplest kind of routes, attribute routes, and then build on that understanding to examine traditional routes. After describing both options, we'll give some guidance on when to use each one.

Defining Attribute Routes

Every ASP.NET MVC application needs routes to allow it to handle requests. Routes are the entry points into your MVC application. This section describes how to define routes and discusses how they map requests to executable code, starting with the simplest kind of routes, called attribute routes, which are new in ASP.NET MVC 5. After that, the discussion turns to the traditional routes that have been available since ASP.NET MVC 1.

Before you start looking at the details, here's a quick overview of the major concepts involved in defining attribute routes. A route definition starts with a URL template, which specifies the pattern that the route will match. Route definitions can appear as attributes on either the controller class or on an action method. Routes can also specify constraints and default values for the various parts of the URL, providing tight control over how and when the route matches incoming request URLs.

Routes can even have names associated with them, which comes into play for outgoing URL construction (the second main purpose of Routing). We'll cover named routes a bit later.

In the following sections, we'll 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 the RegisterRoutes method. This method is the central control point for your routes and is located in the ∼/App_Start/RouteConfig.cs file. Because you're starting with attribute routes, you'll clear out everything in the RegisterRoutes method for now and just have it enable attribute routing by calling the MapMvcAttributeRoutes registration method. When you're done, your RegisterRoutes method should look like this:

public static void RegisterRoutes(RouteCollection routes)

{

routes.MapMvcAttributeRoutes();

}

Now you're ready to write your first route. At its core, a route's job is to map a request to an action. The easiest way to do this is using an attribute directly on an action method:

public class HomeController : Controller

{

[Route("about")]

public ActionResult About()

{

return View();

}

}

This route attribute will run your About method any time a request comes in with /about as the URL. You tell MVC which URL you're using, and MVC runs your code. It doesn't get much simpler than this.

If you have more than one URL for your action, you use multiple route attributes. For example, you might want your home page to be accessible through the URLs /, /home, and /home/index. Those routes would look like this:

[Route("")]

[Route("home")]

[Route("home/index")]

public ActionResult Index()

{

return View();

}

The string you pass in to the route attribute is called the route template, which is a pattern-matching rule that determines whether this route applies to an incoming request. If the route matches, MVC will run its action method. In the preceding routes, you've used static values like about or home/index as your route templates, and your route will match only when the URL path has that exact string. They might look quite simple, but static routes like these can actually handle quite a bit of your application.

Route Values

The static routes you saw earlier are great for the simplest routes, but not every URL is static. For example, if your action shows the details for a person record, you might want to include the record ID in your URL. That's solved by adding a route parameter:

[Route("person/{id}")]

public ActionResult Details(int id)

{

// Do some work

return View();

}

Putting curly braces around id creates a placeholder for some text that you want to reference by name later. To be precise, you're capturing a path segment, which is one of part of a URL path separated by slashes (but not including the slashes). To see how that works, let's define a route like this:

[Route("{year}/{month}/{day}")]

public ActionResult Index(string year, string month, string day)

{

// Do some work

return View();

}

Table 9.1 shows how the route just defined in the preceding code parses certain URLs into route parameters.

Table 9.1 Route Parameter Value Mapping Examples

URL

Route Parameter Values

/2014/April/10

year = "2014"month = "April"day = "10"

/foo/bar/baz

year = "foo"month = "bar"day = "baz"

/a.b/c-d/e-f

year = "a.b"month = "c-d"day = "e-f"

In the preceding method, the attribute route will match any URL with three segments because a route 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 {year} route parameter, the value in the second segment of that URL corresponds to the {month} route parameter, and the value in the third segment corresponds to the {day} parameter.

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

When an attribute route matches and an action method runs, the route parameters from the route are used by model binding to populate values for any method parameters with the same name. Later, you'll learn about how route parameters are different from method parameters.

Controller Routes

So far, you've seen how to put route attributes directly on your action methods. But often, the methods in your controller class will follow a pattern with similar route templates. Consider the routes for a simple HomeController such as the one in a new MVC application:

public class HomeController : Controller

{

[Route("home/index")]

public ActionResult Index()

{

return View();

}

[Route("home/about")]

public ActionResult About()

{

return View();

}

[Route("home/contact")]

public ActionResult Contact()

{

return View();

}

}

These routes are all the same except for the last segment of the URL. Wouldn't it be nice to find some way to avoid repeating yourself and just say that each action method maps to a URL under home? Fortunately, you can:

[Route("home/{action}")]

public class HomeController : Controller

{

public ActionResult Index()

{

return View();

}

public ActionResult About()

{

return View();

}

public ActionResult Contact()

{

return View();

}

}

We've removed all the route attributes above each method and replaced them with one attribute on the controller class. When you define a route on the controller class, you can use a special route parameter named action, and it serves as a placeholder for any action name. It has the same effect as your putting separate routes on each action and typing in the action name statically; it's just a more convenient syntax. You can have multiple route attributes on your controller class just like you do on your action methods.

Often, some actions on a controller will have slightly different routes from all the others. In that case, you can put the most common routes on the controller and then override these defaults on the actions with different route patterns. For example, maybe you think/home/index is too verbose and you want to support /home as well. You could do that as follows:

[Route("home/{action}")]

public class HomeController : Controller

{

[Route("home")]

[Route("home/index")]

public ActionResult Index()

{

return View();

}

public ActionResult About()

{

return View();

}

public ActionResult Contact()

{

return View();

}

}

When you specify a route attribute at the action level, you're overriding anything specified at the controller level. In the preceding example, if the Index method only had the first route attribute (for home), it would not be accessible via home/index even though the controller has a default route for home/{action}. If you're customizing the routes for an action and you do want the default controller routes to apply, just list them again on your action.

The earlier class is still slightly repetitive. Every route begins with home/ (the class is named HomeController, after all). You can say that just once, using RoutePrefix:

[RoutePrefix("home")]

[Route("{action}")]

public class HomeController : Controller

{

[Route("")]

[Route("index")]

public ActionResult Index()

{

return View();

}

public ActionResult About()

{

return View();

}

public ActionResult Contact()

{

return View();

}

}

Now, all your route attributes can omit home/ because the prefix provides that automatically. The prefix is just a default, and you can escape from it if necessary. For example, you might want your home controller to support the URL / in addition to /home and/home/index. To do that, just begin the route template with ∼/, and the route prefix will be ignored. Here's how it looks when HomeController supports all three URLs for the Index method (/, /home, and /home/index):

[RoutePrefix("home")]

[Route("{action}")]

public class HomeController : Controller

{

[Route("∼/")]

[Route("")] // You can shorten this to [Route] if you prefer.

[Route("index")]

public ActionResult Index()

{

return View();

}

public ActionResult About()

{

return View();

}

public ActionResult Contact()

{

return View();

}

}

Route Constraints

Because your method parameter names are located right below your route attribute with its route parameter names, overlooking the differences between the two kinds of parameters can be easy to do. But when you're debugging, understanding the difference between a route parameter and a method parameter can be important. Recall the earlier example with a record ID:

[Route("person/{id}")]

public ActionResult Details(int id)

{

// Do some work

return View();

}

For this route, think about what happens when a request comes in for the URL /person/bob. What's the value of id? Well, that's a trick question: The answer depends on which id you're talking about, the route parameter or the action method parameter. As you saw earlier, a route parameter in a route will match any non-empty value. So, in Routing, the value of the route parameter id is bob, and the route matches. But later, when MVC tries to run the action, it sees that the action method declares its id method parameter to be an int, and the value bob from the Routing route parameter can't be converted to an int. So the method can't execute (and you never get to the point where there would be a value for id as a method parameter).

But what if you wanted to support both /person/bob and /person/1 and run different actions for each URL? You might try to add a method overload with a different attribute route like this:

[Route("person/{id}")]

public ActionResult Details(int id)

{

// Do some work

return View();

}

[Route("person/{name}")]

public ActionResult Details(string name)

{

// Do some work

return View();

}

If you look at the routes closely, you'll realize there's a problem. One route uses a parameter called id and the other uses a parameter called name. It might seem obvious to you that name should be a string and id should be a number, but to Routing they're both just route parameters, and, as you've seen, route parameters will match any string by default. So both routes match /person/bob and /person/1. The routes are ambiguous, and there's no good way to get the right action to run when these two different routes match.

What you want here is a way to define the person/{id} route so that it only matched if id was an int. Well, a way does exist, and that leads the discussion to something called route constraints. A route constraint is a condition that must be satisfied for the route to match. In this case, you just need a simple int constraint:

[Route("person/{id:int}")]

public ActionResult Details(int id)

{

// Do some work

return View();

}

[Route("person/{name}")]

public ActionResult Details(string name)

{

// Do some work

return View();

}

Note the key difference here: instead of defining the route parameter as just {id}, you've now defined it as {id:int}. Putting a constraint in the route template like this is called an inline constraint, and a number of them are available, as Table 9.2 shows.

Table 9.2 Inline Constraints

Name

Example Usage

Description

bool

{n:bool}

A Boolean value

datetime

{n:datetime}

A DateTime value

decimal

{n:decimal}

A Decimal value

double

{n:double}

A Double value

float

{n:float}

A Single value

guid

{n:guid}

A Guid value

int

{n:int}

An Int32 value

long

{n:long}

An Int64 value

minlength

{n:minlength(2)}

A String value with at least two characters

maxlength

{n:maxlength(2)}

A String value with no more than two characters

length

{n:length(2)} {n:length(2,4)}

A String value with exactly two characters A String value with two, three, or four characters

min

{n:min(1)}

An Int64 value that is greater than or equal to 1

max

{n:max(3)}

An Int64 value that is less than or equal to 3

range

{n:range(1,3)}

The Int64 value 1, 2, or 3

alpha

{n:alpha}

A String value containing only the A–Z and a–z characters

regex

{n:regex (^a+$)}

A String value containing only one or more ‘a' characters (a Regex match for the ^a+$ pattern)

Inline route constraints give you fine-grained control over when a route matches. If you have URLs that look similar but behave differently, route constraints give you the power to express the difference between these URLs and map them to the correct action.

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 and constraints are not the only factors taken into consideration when matching requests. Providing default values for a route parameter is also possible. For example, suppose that you have an Index action method that does not have a parameter:

[Route("home/{action}")]

public class HomeController : Controller

{

public ActionResult Index()

{

return View();

}

public ActionResult About()

{

return View();

}

public ActionResult Contact()

{

return View();

}

}

You might want to call this method via the URL:

/home

However, given the route template the class defines, home/{action}, this won't work because the route matches only URLs containing two segments, and /home contains only one segment.

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 one segment: home. However, that feels a bit repetitive. You might prefer keeping the original route and making Index the default action. The Routing API allows you to supply default values for parameters. For example, you can define the route like this:

[Route("home/{action=Index}")]

The {action=Index} snippet defines a default value for the {action} parameter. This default allows this route to match requests when the action parameter is missing. In other words, this route now matches any URL with one or two segments instead of matching only two-segment URLs. This now allows you to call the Index action method, using the URL /home, which satisfies your goal.

Instead of having default values, you can make a route parameter optional. Take a look at part of a controller for managing a table of records:

[RoutePrefix("contacts")]

public class ContactsController : Controller

{

[Route("index")]

public ActionResult Index()

{

// Show a list of contacts

return View();

}

[Route("details/{id}")]

public ActionResult Details(int id)

{

// Show the details for the contact with this id

return View();

}

[Route("update/{id}")]

public ActionResult Update(int id)

{

// Display a form to update the contact with this id

return View();

}

[Route("delete/{id}")]

public ActionResult Delete(int id)

{

// Delete the contact with this id

return View();

}

}

Most of the actions take an id parameter, but not all of them do. Instead of having separate routes for these actions, you can use one route and make id optional:

[RoutePrefix("contacts")]

[Route("{action}/{id?}")]

public class ContactsController : Controller

{

public ActionResult Index()

{

// Show a list of contacts

return View();

}

public ActionResult Details(int id)

{

// Show the details for the contact with this id

return View();

}

public ActionResult Update(int id)

{

// Display a form to update the contact with this id

return View();

}

public ActionResult Delete(int id)

{

// Delete the contact with this id

return View();

}

}

You can provide multiple default or optional values. The following snippet demonstrates providing a default value for the {action} parameter, as well:

[Route("{action=Index}/{id?}")]

This example supplies a default value for the {action} parameter within the URL. Typically the URL pattern of contacts/{action} requires 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 might now simply contain /contacts 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.

An optional route parameter is a special case of a default value. From a Routing standpoint, whether you mark a parameter as optional or list a default value doesn't make a lot of difference; in both cases, the route actually has a default value. An optional parameter just has the special default value UrlParameter.Optional.

Note

Instead of making id optional, you can also get the route to match by setting the default value of id to be an empty string: {id=}. What's the difference?

Remember earlier when we mentioned that route parameter values are parsed out of the URL and put into a dictionary? Well, when you mark a parameter as optional 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's being specified but left empty.

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

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

[Route("contacts/{action=Index}/{id}")]

[Route("contacts/{action}/{id?}")]

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

In this example, an ambiguity exists about which route the request should match. To avoid these types of ambiguities, the Routing engine only uses a particular default value when every subsequent parameter also has a default value defined (including an optional parameter, which uses the default value UrlParameter.Optional). In this example, if you have a default value for {action} you should also provide a default value for {id} (or make it optional).

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

[Route("{action}-{id?}")]

Notice the string literal (-) between the {action} and {id?} parameters. Clearly, a request for /details-1 will match this route, but should a request for /details- match? Probably not, because that makes for an awkward-looking URL.

It turns out that with Routing, any path segment (the portion of the URL between two slashes) with literal values must have a match for each of the route 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 “Inside Routing: How Routes Generate URLs.”

Defining Traditional Routes

Before you created your first attribute route, we briefly looked at a method called RegisterRoutes in the ∼/App_Start/RouteConfig.cs file. So far, you've only had one line in that method (to enable attribute routing). Now it's time to take a closer look at this method.RegisterRoutes is the central configuration point for Routing, and it's where traditional routes live.

Let's remove the reference to attribute routing while the discussion focuses on traditional routes. Later, you'll combine the two. But for now, clear out the RegisterRoutes method and put in a very simple traditional route. When you're done, your RegisterRoutesmethod should look like this:

public static void RegisterRoutes(RouteCollection routes)

{

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

}

Unit Testing Routes

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

var routes = new RouteCollection();

RouteConfig.RegisterRoutes(routes);

//Write tests to verify your routes here…

Unfortunately, this exact approach doesn't combine well with attribute routing. (Attribute routing needs to find controller classes and action methods to locate their route attributes, and that process is only designed to work when theMapMvcAttributeRoutes method is being called within an ASP.NET site.) To work around this limitation, you'll probably want to keep the MapMvcAttributeRoutes call out of the method you unit test. Instead, you might structure RegisterRoutes like this:

public static void RegisterRoutes(RouteCollection routes)

{

routes.MapMvcAttributeRoutes();

RegisterTraditionalRoutes(routes);

}

public static void RegisterTraditionalRoutes(RouteCollection routes)

{

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

}

and then have your unit tests call RouteConfig.RegisterTraditionalRoutes instead of RouteConfig.RegisterRoutes.

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

The simplest form of the MapRoute method takes in a name and a route template for the route. The name is discussed later. For now, focus on the route template.

Just like with attribute routing, the route template is a pattern-matching rule used to determine whether the route should handle an incoming request (based on the request's URL). The big difference between attribute routing and traditional routing is how a route is linked to an action method. Traditional routing relies on name strings rather than attributes to make this link.

With an attribute route on an action method, you didn't need any parameters at all for the route to work. The route attribute was placed directly on the action method, and MVC knew to run that action when the route matched. With an attribute route on a controller class, MVC still knew which class to use (because it had the attribute on it) but not which method, so you used the special action parameter to indicate the method by name.

If you try to make a request to a URL for the simple route above, (for example, /a/b/c), you'll receive a 500 error. This happens because with a traditional route, there's no automatic link with either a controller or an action. To specify the action, you need to use theaction parameter (just like you did with route attributes on controller classes). To specify the controller, you need to use a new parameter named controller. Without these parameters defined, MVC doesn't know what action method you want to run, so it lets you know about this problem by responding with a 500 error.

You can fix this problem by changing your simple route to include these required parameters:

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

Now, if you make a request to a URL such as /home/index, MVC sees it as a request for a {controller} named home and an {action} named index. By convention, MVC appends the suffix Controller to the value of the {controller} route parameter and attempts to locate a type of that name (case insensitively) that also implements the System.Web.Mvc.IController interface.

Note

The fact that attribute routes are directly tied to the method and controller, rather than just specifying a name, means that they are more precise. For example, with attribute routing, you can name your controller class anything you like as long as it ends with the Controller suffix (it doesn't need to be related to your URL). Having the attribute directly on the action method means that MVC knows exactly which overload to run, and doesn't need to pick one of the potentially multiple action methods that share the same name.

Route Values

The controller and action parameters are special because they are required and map to your controller and action name. But these two parameters aren't the only ones your route can use. Update your route to include a third parameter:

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

If you look back again at the examples in Table 9.1 and apply them to this updated route, you see that a request for /albums/display/123 is now a request for a {controller} named albums. 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.

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

Note that although 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();

}

}

A request for /albums/display/123 causes 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 route 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, just like you saw with attribute routes. 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}

This indicates that the 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.

Having path segments that mix literals with route parameters is even possible. The only restriction is that two consecutive route parameters are not allowed. Thus

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

{controller}.{action}.{id}

are valid route URLs, but

{controller}{action}/{id}

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.3) can help you see how the URL pattern corresponds to matching URLs.

Table 9.3 Route URL Patterns and Examples

Route URL Patterns

URLs That Match

{controller}/{action}/{genre}

/albums/list/rock

service/{action}-{format}

/service/display-xml

{report}/{year}/{month}/{day}

/sales/2008/1/23

Just remember that unless the route somehow provides both controller and action parameters, MVC won't know which code to run for the URL. (In the following discussion of default values, you'll see a way to provide these parameters to MVC without including them in the route template.)

Route Defaults

So far, your calls to MapRoute have focused on defining routes that contain a URL pattern for matching URLs. Just like with attribute routes, it turns out that the route URL is not the only factor taken into consideration when matching requests. Providing default values for a route parameter is also possible. For example, suppose that you have an action method that does not have a parameter:

public class AlbumsController : Controller

{

public ActionResult List()

{

//Do something

return View();

}

}

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

/albums/list

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.

With an attribute route, you would make the {id} parameter optional by changing it to {id?} inline in the route template. Traditional routing takes a different approach. Instead of putting this information inline as part of the route template, traditional routing puts it in a separate argument after the route template. To make {id} optional in traditional routing, you can define the route like this:

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

new {id = UrlParameter.Optional});

The third parameter to MapRoute is for default values. The {id = UrlParameter.Optional} snippet defines a default value for the {id} parameter. Unlike attribute routing, the relationship between optional and default values is obvious here. An optional parameter is simply a parameter with the special default value of UrlParameter.Optional, and that's exactly how it's specified in a traditional route definition.

This now allows you to call the List action method, using the URL /albums/list, which satisfies your goal. As in attribute routing, you can also provide multiple default values. The following snippet demonstrates adding a default value for the {action} parameter:

routes.MapRoute("simple",

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

new { id = UrlParameter.Optional, action = "index" });

Note

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 in this chapter. 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. Initially, it might feel unusual, but we think you'll soon grow to appreciate its terseness and clarity.

Attribute routing would have placed this default inline, using the syntax {action=Index}. Once again, traditional routing uses a different style. You specify default and optional values in a separate argument used just for this purpose.

The earlier 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. Though the syntax is different, the functionality provided by default values works exactly as it did with attribute routing.

Let's revisit the Table 9.3 on route URL patterns and what they match, and now throw defaults into the mix as shown in the following examples:

1. routes.MapRoute("defaults1",

2. "{controller}/{action}/{id}",

3. new {id = UrlParameter.Optional});

4. routes.MapRoute("defaults2",

5. "{controller}/{action}/{id}",

6. new {controller = "home",

7. action = "index",

8. id = UrlParameter.Optional});

The defaults1 route matches the following URLs:

1. /albums/display/123

2. /albums/display

The defaults2 route matches the following URLs:

1. /albums/display/123

2. /albums/display

3. /albums

4. /

Default values even allow you to map URLs that don't include controller or action parameters at all in the route template. For example, the following route has no parameters at all; instead, the controller and action parameters are provided to MVC by using defaults:

routes.MapRoute("static",

"welcome",

new { controller = "Home", action = "index" });

Just like with attribute routing, remember that the position of a default value relative to other route 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}. Unless both parameters have a default value, a potential ambiguity exists, so Routing will ignore the default value on the {action} parameter. When you specify a default value for one parameter, make sure you also specify default values for any parameters following it, or your default value will largely be ignored. In this case, the default value only comes into play when generating URLs, which is covered later in the section “Inside Routing: How Routes Generate URLs.”

Route Constraints

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

· http://example.com/2008/01/23/

· http://example.com/posts/categories/aspnetmvc/

Each URL contains three segments and would each match the simple traditional routes 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 you make that happen?

This situation is where constraints are useful. Constraints allow you to apply a regular expression to a path segment to restrict whether the route will match the request. With attribute routing, constraints were specified inline in the route template using a syntax such as {id:int}. Once again, traditional routing has a different approach. Instead of putting information like this inline, traditional routing uses a separate parameter. For example:

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

In the preceding snippet, the first route contains three route parameters, {year}, {month}, and {day}. Each of those parameters maps 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 route 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 on to the next route.

If you're familiar with regular expressions, you 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.

Note

Attribute routing has the opposite behavior for regular expression matching. Traditional routing always does an exact match, whereas the attribute routing regex inline constraint supports partial matches. The traditional routing constraint year = @"\d{4}" is the equivalent to {year:regex(^\d{4}$)} as an attribute routing inline constraint. In attribute routing, if you want to do an exact match, you need to include the ^ and $ characters explicitly. In traditional routing, those characters are always added for you, and partial matches are not supported without writing a custom constraint. Usually, you'll want an exact string match, so the traditional routing syntax means you won't accidentally forget this detail. Just be aware of the difference if you move a regex constraint between a traditional route and an attribute route.

Thus, the first route defined in the earlier code snippet 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

You put your new route before the default simple route because routes are evaluated in order. Because a request for /2008/06/07 would match both defined routes, you need to put the more specific route first.

By default, traditional route 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. Attribute routing provides a large number of built-in inline constraints, but it's limited to using the route template string. That means no easy way exists to provide custom constraint objects in attribute routing. Traditional routing treats constraints as regular expressions when they are strings, but it's easy to pass another constraint object instead when you want to use a different kind of constraint. You'll see how to take advantage of that in the “Custom Route Constraints” section.

Combining Attribute Routing with Traditional Routing

Now you've seen both attribute routes and traditional routes. Both support route templates, constraints, optional values, and defaults. The syntax is a little different, but the functionality they offer is largely equivalent, because under the hood both use the same Routing system.

You can use either attribute routing, traditional routing, or both. To use attribute routing, you need to have the following line in your RegisterRoutes method (where traditional routes live):

routes.MapMvcAttributeRoutes();

Think of this line as adding an über-route that contains all the route attributes inside it. Just like any other route, the position of this über-route compared to other routes makes a difference. Routing checks each route in order and chooses the first route that matches. If there's any overlap between a traditional route and an attribute route, the first route registered wins. In practice, we recommend putting the MapMvcAttributeRoutes call first. Attribute routes are usually more specific, and having attribute routes come first allows them to take precedence over traditional routes, which are usually more generic.

Suppose you have an existing application that uses traditional routing, and you want to add a new controller to it that uses attribute routing. That's pretty easy to do:

routes.MapMvcAttributeRoutes();

routes.MapRoute("simple",

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

new { action = "index", id = UrlParameter.Optional});

// Existing class

public class HomeController : Controller

{

public ActionResult Index()

{

return View();

}

public ActionResult About()

{

return View();

}

public ActionResult Contact()

{

return View();

}

}

[RoutePrefix("contacts")]

[Route("{action=Index}/{id?}")]

public class NewContactsController : Controller

{

public ActionResult Index()

{

// Do some work

return View();

}

public ActionResult Details(int id)

{

// Do some work

return View();

}

public ActionResult Update(int id)

{

// Do some work

return View();

}

public ActionResult Delete(int id)

{

// Delete the contact with this id

return View();

}

}

Choosing Attribute Routes or Traditional Routes

Should you use attribute routes or traditional routes? Either option is reasonable, but here are some suggestions on when to use each one.

Consider choosing traditional routes when:

· You want centralized configuration of all your routes.

· You use custom constraint objects.

· You have an existing working application you don't want to change.

Consider choosing attribute routes when:

· You want to keep your routes together with your action's code.

· You are creating a new application or making significant changes to an existing one.

The centralized configuration of traditional routes means there's one place to go to understand how a request maps to an action. Traditional routes also have some more flexibility than attribute routes. For example, adding a custom constraint object to a traditional route is easy. Attributes in C# only support certain kinds of arguments, and for attribute routing, that means everything is specified in the route template string.

On the other hand, attribute routing nicely keeps everything about your controllers together, including both the URLs they use and the actions that run. I tend to prefer attribute routing for that reason. Fortunately, you can use both and moving a route from one style to the other if you change your mind is not difficult.

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. However, as you'll see in this section, in some cases this can break down due to ambiguities about 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 traditional 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 write the following code:

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

@Html.RouteLink("to 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 in some situations this can bite you.

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. A request for /static/url would be matched by the default route and never make it through the list of routes to get to the new route. Therefore, you need to add this route to the beginning of the list of routes before the default route.

Note

This problem isn't specific to Routing with Web Forms. Many cases exist 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. However, 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:

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

and

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

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 route 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 route parameters, it matches every URL generation attempt because technically, “a route value is supplied for each route parameter.” It just so happens that there aren't any route 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 simple: Always use named routes 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 give it a name and use it. If you have a need to use non-named routes and are leaving the URL generation entirely up to Routing, we recommend writing unit tests that verify the expected behavior of the routes and URL generation within your application.

Specifying the name of the route not only avoids ambiguities, but it might 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.

For the previous example, where you generated two links, the following change fixes the issue. We changed the code to use named parameters to make it clear what the route 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}

)

For attribute routes, the name is specified as an optional argument on the attribute:

[Route("home/{action}", Name = "home")]

Generating links to attribute routes works the same way as it does for traditional routes.

For attribute routes, unlike traditional routes, the route name is optional. We recommend leaving it out unless you need to generate a link to the route. Under the hood, MVC does a small bit of extra work to support link generation for named attribute routes, and it skips that work if the attribute route is unnamed.

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

You configure area routes 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.RegisterAllAreaswithin the Application_Start method in Global.asax.

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 might 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 you use 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 might 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. The following code shows how to do that using a traditional route:

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.

To utilize areas with attribute routing, use the RouteArea attribute. In attribute routing, you don't need to specify the namespace, because MVC can figure that out for you (the attribute is on the controller, which knows its own namespace). Instead, you specify the name of the AreaRegistration in a RouteArea attribute.

[RouteArea("admin")]

[Route("users/{action}")]

public class UsersController : Controller

{

// Some action methods

}

By default, all attribute routes for this class use the area name as the route prefix. So the preceding route is for URLs like /admin/users/index. If you would rather have a different route prefix, you can use the optional AreaPrefix property:

[RouteArea("admin", AreaPrefix = "manage")]

[Route("users/{action}")]

This code would use URLs like /manage/users/index instead. Just like with prefixes defined by RoutePrefix, you can leave the RouteArea prefix out by starting the route template with the ∼/ characters.

Note

If you try to use both traditional routes and attribute routes together within an area, you'll need to be careful about route order. As we mentioned earlier, we recommend putting attribute routes in the route table before traditional routes. If you look at the default code in the Application_Start method in Global.asax, you'll notice that the call to AreaRegistration.RegisterAllAreas() comes before RegisterRoutes. That means any traditional routes you create in an area's RegisterArea method come before any routes you create in RegisterRoutes, including any attribute routes created by calling MapMvcAttributeRoutes. Having RegisterAllAreas come before RegisterRoutes makes sense because an area's traditional routes are more specific than the non-area routes in RegisterRoutes. However, attribute routes are even more specific, so in this case they need to be mapped even earlier than RegisterRoutes. In this scenario, we recommend moving the call to MapMvcAttributeRoutes outside of your RegisterRoutesmethod and instead making that the first call in Application_Start:

RouteTable.Routes.MapMvcAttributeRoutes();

AreaRegistration.RegisterAllAreas();

// Other registration calls, including RegisterRoutes

Catch-All Parameter

A catch-all parameter allows for a route to match part of a URL with an arbitrary number of segments. The value put in the parameter is the rest of the URL path (that is, it excludes the query string, if any). A catch-all parameter is permitted only as the last segment of the route template.

For example, the following traditional route below would handle requests like those shown in Table 9.4:

public static void RegisterRoutes(RouteCollection routes)

{

routes.MapRoute("catchallroute", "query/{query-name}/{*extrastuff}");

}

Table 9.4 Catch-All Route 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 = null (Route still matches. The catch-all just catches the null string in this case.)

Attribute routing uses the same syntax. Just add an asterisk (*) in front of the parameter's name to make it a catch-all parameter.

Multiple Route 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 a request comes in, Routing matches literals in the route URL exactly. Route parameters are matched greedily, which has the same connotations as it does with regular expressions. In other words, the route tries to match as much text as possible with each route parameter.

For example, how would the route {filename}.{ext} 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 route 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.

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{location}-{sublocation}

/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 its 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.

However, 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. The following example shows adding a route the manual way, by creating a route with a new StopRoutingHandler and adding the route to the RouteCollection:

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 is convenient, and using this new method along with MapRoutechanges the example to look like this:

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 can find a number of places in ASP.NET MVC where extension methods such as MapRoute and IgnoreRoute can make things a bit tidier.

Debugging Routes

Debugging problems with Routing used to be really frustrating 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 invokes either 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 might 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 Phil Haack wrote the Route Debugger.

When the Route 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 Route Debugger, simply use NuGet to install it via the following command in the Package Manager Console window in Visual Studio: Install-Package RouteDebugger. This package adds the Route Debugger assembly and then adds a setting to the appSettingssection of web.config used to turn Route Debugger on or off:

<add key="RouteDebugger:Enabled" value="true" />

As long as the Route Debugger is enabled, it will display the route data pulled from the current request URL 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 Route Debugger, so you can modify it to output any other data that you think is relevant. For example, Stephen Walther used the Route 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 you can use it without disabling the Routing system. You could even 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.

image

Figure 9.1

INSIDE ROUTING: 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.

Note

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 among not just the model, the view, and the controller, but also the powerful but silent fourth player, Routing.

In principle, developers supply a set of route values, and the Routing system uses them 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, first take a 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 code 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 RouteBase.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 the route does not match, 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.

A Detailed Look at URL Generation

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

A Simple Case

The logic most developers encounter when using Routing is detailed in the following steps:

1. Developer calls a method such as Html.ActionLink or Url.Action. That method, in turn, 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 route parameters of the route (route 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 might contain default values that do not have a corresponding route 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.image

Figure 9.2

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

Figure 9.3

5. The route is a match! Now the URL is generated by looking at each route 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 that you want to display a large list of tasks. Rather than dumping them all on the page at the same time, you might want to allow users to page through them via links. For example, Figure 9.4 shows a simple interface for paging through the list of tasks.

image

Figure 9.4

Table 9.6 shows the route data for this request.

Table 9.6 Route Data

Key

Value

Controller

Tasks

Action

List

Page

2

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

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

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 theRouteData for the current request. Explicitly supplied values for the controller and action would, of course, override the ambient values. To unset an ambient value when generating a URL, specify the key in the dictionary of parameters and have its value set to either null or an empty string.

Overflow Parameters

Overflow parameters are route values used in URL generation that are not specified in the route's definition. To be precise, we mean values in 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:

public static void RegisterRoutes(RouteCollection routes)

{

routes.MapRoute(

"Default",

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

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

);

}

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 parameter named “page.” In this example, instead of generating a link, you'll just render out the URL using theUrl.RouteUrl method.

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

The URL generated will be /Report/List?page=123. 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 extra parameters are specified.

More Examples of URL Generation with the Route Class

Assume that the following route is defined:

public static void RegisterRoutes(RouteCollection routes)

{

routes.MapRoute("report",

"{year}/{month}/{day}",

new { controller = "Reports", action = "View", day = 1 }

);

}

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

@Url.RouteUrl(new { param1 = value1, param2 = value2, ..., paramN = valueN })

Table 9.7 shows parameters and the resulting URL.

Table 9.7 Parameters and Resulting URL for GetVirtualPath

Parameters

Resulting URL

Reason

year=2007, month=1, day=12

/2007/1/12

Straightforward matching

year=2007, month=1

/2007/1

Default for day = 1

Year=2007, month=1, day=12, category=123

/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

INSIDE ROUTING: HOW ROUTES TIE YOUR URL TO AN ACTION

This section provides a peek under the hood to get a detailed understanding of how URLs and action methods tie together. This will give you a better picture of where the dividing line is between Routing and MVC.

One common misconception is that Routing is just a feature of ASP.NET MVC. During early previews of ASP.NET MVC 1.0 this was true, but it quickly became apparent that Routing was a useful feature in its own right beyond ASP.NET MVC. For example, the ASP.NET Dynamic Data team was also interested in using Routing. At that point, Routing became a more general-purpose feature that had neither internal knowledge of nor a dependency on MVC.

To better understand how Routing fits into the ASP.NET request pipeline, take a look at the steps involved in routing a request.

Note

The discussion here focuses on Routing for IIS 7 (and above) Integrated Mode. Some slight differences exist when using Routing with IIS 7 Classic Mode or IIS 6. When you use the Visual Studio built-in web server, the routing behavior is similar to IIS 7 Integrated Mode.

The High-Level Request Routing Pipeline

The Routing pipeline consists of the following high-level steps when a request is handled by ASP.NET:

1. The UrlRoutingModule attempts to match the current request with the routes registered in the RouteTable.

2. If one of the routes in the RouteTable matches, the Routing module grabs the IRouteHandler from that route.

3. The Routing module calls the GetHttpHandler method of the IRouteHandler, which returns the IHttpHandler that will be used to process the request.

4. ProcessRequest is called on the HTTP handler, thus handing off the request to be handled.

5. In the case of ASP.NET MVC, the IRouteHandler is an instance of MvcRouteHandler, which, in turn, returns an MvcHandler that implements IHttpHandler. The MvcHandler is responsible for instantiating the controller, which, in turn, calls the action method on that controller.

RouteData

Recall that when the GetRouteData method is called it returns an instance of RouteData, which contains information about the route that matched that request.

Earlier we showed a route with the following URL: {controller}/{action}/{id}. When a request for /albums/list/123 comes in, the route attempts to match the request. If it does match, it then creates a dictionary that contains information parsed from the URL. Specifically, it adds a key to the Values dictionary for each route parameter in the route URL.

In the case of the traditional route {controller}/{action}/{id}, the Values dictionary will contain at least three keys: "controller,” "action,” and "id.” In the case of /albums/list/123, the URL is parsed to supply values for these dictionary keys. In this case, controller = albums, action = list, and id = 123.

For attribute routing, MVC uses the DataTokens dictionary to store more precise information than just a string for an action name. Specifically, it contains a list of action descriptors that point directly to the possible action methods to use when the route matches. (In the case of a controller-level attribute route, there's more than one action in this list.)

The RouteData property of the RequestContext that is used throughout MVC is where the ambient route values are kept.

CUSTOM ROUTE CONSTRAINTS

The “Route Constraints” section earlier in this chapter covers how to use regular expressions to provide fine-grained control over route matching in traditional routes. As you might recall, we pointed out that the RouteValueDictionary class is a dictionary of string-object pairs. When you pass in a string as a constraint in a traditional route, the Route class interprets the string as a regular expression constraint. However, passing in constraints other than regular expression strings is possible.

Routing provides an IRouteConstraint interface with a single Match method. Here's a look at the interface definition:

public interface IRouteConstraint

{

bool Match(HttpContextBase httpContext, Route route, string parameterName,

RouteValueDictionary values, RouteDirection routeDirection);

}

When Routing evaluates route constraints, and a constraint value implements IRouteConstraint, it causes the route engine to call the IRouteConstraint.Match method on that route constraint to determine whether the constraint is satisfied for a given request.

Route constraints are run for both incoming URLs and while generating URLs. A custom route constraint will often need to inspect the routeDirection parameter of the Match method to apply different logic depending on when it is being called.

Routing itself provides one implementation of this interface in the form of the HttpMethodConstraint class. This constraint allows you to specify that a route should match only requests that use a specific set of HTTP methods (verbs).

For example, if you want a route to respond only to GET requests, but not POST, PUT, or DELETE requests, you could define the following route:

routes.MapRoute("name", "{controller}", null,

new { httpMethod = new HttpMethodConstraint("GET")} );

Note

Custom constraints don't have to correspond to a route parameter. Thus, providing a constraint that is based on some other piece of information, such as the request header (as in this case), or on multiple route parameters is possible.

MVC also provides a number of custom constraints in the System.Web.Mvc.Routing.Constraints namespace. These are where the inline constraints used by attribute routing live, and you can use them in traditional routing as well. For example, to use attribute routing's{id:int} inline constraint in a traditional route, you can do the following:

routes.MapRoute("sample", "{controller}/{action}/{id}", null,

new { id = new IntRouteConstraint() });

USING ROUTING WITH WEB FORMS

Although the main focus of this book is on ASP.NET MVC, Routing is a core feature of ASP.NET, so you can use it with Web Forms as well. This section looks at ASP.NET 4, because it includes full support for Routing with Web Forms.

In ASP.NET 4, you can add a reference to System.Web.Routing to your Global.asax and declare a Web Forms route in almost the exact same format as an ASP.NET MVC application:

void Application_Start(object sender, EventArgs e)

{

RegisterRoutes(RouteTable.Routes);

}

private void RegisterRoutes(RouteCollection routes)

{

routes.MapPageRoute(

"product-search",

"albums/search/{term}",

"∼/AlbumSearch.aspx");

}

The only real difference from an MVC route is the last parameter, in which you direct the route to a Web Forms page. You can then use Page.RouteData to access the route parameter values, like this:

protected void Page_Load(object sender, EventArgs e)

{

string term = RouteData.Values["term"] as string;

Label1.Text = "Search Results for: " + Server.HtmlEncode(term);

ListView1.DataSource = GetSearchResults(term);

ListView1.DataBind();

}

You can use Route values in your markup as well, using the <asp:RouteParameter> object to bind a segment value to a database query or command. For instance, using the preceding route, if you browsed to /albums/search/beck, you can query by the passed route value using the following SQL command:

<asp:SqlDataSource id="SqlDataSource1" runat="server"

ConnectionString="<%$ ConnectionStrings:Northwind %>"

SelectCommand="SELECT * FROM Albums WHERE Name LIKE @searchterm + "%'">

<SelectParameters>

<asp:RouteParameter name="searchterm" RouteKey="term" />

</SelectParameters>

</asp:SqlDataSource>

You can also use the RouteValueExpressionBuilder to write out a route parameter value a little more elegantly than just writing out Page.RouteValue["key"]. If you want to write out the search term in a label, you can do the following:

<asp:Label ID="Label1" runat="server" Text="<%$RouteValue:Term%>" />

You can generate outgoing URLs for using the Page.GetRouteUrl() in code-behind logic method:

string url = Page.GetRouteUrl(

"product-search",

new { term = "chai" });

The corresponding RouteUrlExpressionBuilder allows you to construct an outgoing URL using Routing:

<asp:HyperLink ID="HyperLink1"

runat="server"

NavigateUrl="<%$RouteUrl:Term=Chai%>">

Search for Chai

</asp:HyperLink>

SUMMARY

Routing is much like the Chinese game of Go: It's simple to learn but takes a lifetime to master. Well, maybe not a lifetime, but certainly a few days at least. The concepts are basic, but in this chapter you've seen how Routing can enable several sophisticated scenarios in your ASP.NET MVC (and Web Forms) applications.