Chapter 4. Routes and URLs

Software developers are well known for paying close attention to the little details, especially when it comes to the quality and structure of their source code. We'll often fight long battles over code indentation styles and where curly braces should go. 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/products/list.aspx?id=17313&catid=33723&page=3

For all the attention we pay to code, why not pay the same amount of attention to the URL? It may not seem all that important, but the URL is a legitimate and widely used web user interface. Usability expert Jakob Nielsen (www.useit.com) urges developers to pay attention to URLs and provides the following guidelines for high-quality URLs:

  • 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, ASP.NET, and the like, the URL represents a physical file on disk. For example, when you see a request for:

http://example.com/products/list.aspx

you could bet your kid's tuition that the web site has a directory structure that contains a products folder and a List.aspx file within that folder. In this case, there is a direct relationship between the URL and what physically exists on disk. When such a request is received by the web server, the web framework executes code associated with this file to respond to the request. In many cases, this code contains or is associated with a template that intermixes server-side declarations with HTML markup to generate the resulting markup sent back to the browser via the response.

As you might guess from the section, "Serving Methods, Not Files," in Chapter 2, this 1:1 relationship between URLs and the file system is not the case with most MVC Web Frameworks like ASP.NET MVC. These frameworks generally take a different approach by mapping the URL to a method call on a class.

These classes are generally called Controllers because their purpose is to control the interaction between the user input and other components of the system. If an application is a symphony, the Controller is the conductor — the Controller orchestrates the handling of user input, executes appropriate application and data logic in response to the request, and selects the appropriate view to send back in the response.

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," while URL means "Uniform Resource Locator." 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." One way that Ryan McDonough (www.damnhandy.com) put it is that "a URI is an identifier for some resource, but a URL gives you specific information as to obtain that resource." That specific information might be http:// or ftp://.

Arguably this is all just semantics, and most people will get your meaning regardless of which name you use. However, this discussion may 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. This chapter will help you map logical URLs/URIs to methods on controllers. All that said, we'll use the conventional term URL throughout the book. This chapter also covers the ASP.NET Routing feature, which is a separate API that the MVC framework makes heavy use of in order to map URLs to method calls. The chapter first covers how MVC uses Routing and then takes a peek under the hood a bit at Routing as a standalone feature.

INTRODUCTION TO ROUTING

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

  • It matches incoming requests and maps them to a controller action.

  • It constructs outgoing URLs that correspond to controller actions.

Later, when we dig deeper, you'll see that the above two items only describe what Routing does in the context of MVC. You'll look at how routing does much more, and is used by other parts of ASP.NET, in a later section.

Compared 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 URL and the code that handles the URL, which can help create pretty URLs for Search Engine Optimization (SEO) purposes. One key difference, though, is that URL Rewriting represents a page-centric view of URLs. Most rewriting schemes with ASP.NET rewrite a URL for one page to be handled by another. For example, you might see:

/product/bolts.aspx

rewritten as:

/product/display.aspx?productid=111

Routing, on the other hand takes 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. 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 at least a handful. It's conceivable that a very complex application could have dozens of routes or more.

In this section, we'll look at how to define routes. Route definitions start with the URL, which specifies a 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 the route matches incoming request URLs.

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.

Let's clear out the routes in there for now and replace them with this very simple route:

routes.MapRoute("simple", "{first}/{second}/{third}");
Code snippet 4-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, focus on the URL pattern.

Notice that the route URL consists of several URL segments (a segment is everything between slashes but not including the slashes), each of which contains a placeholder delimited using curly braces. These placeholders 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 it 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.

We can name these parameters anything we'd like, as we did in this case. When a request comes in, Routing parses the request URL into a dictionary (specifically a RouteValueDictionary accessible via the RequestContext), using the URL parameter names as the keys and subsections of the URL in the corresponding position as the values. Later you'll learn that when using routes in the context of an MVC application, there are certain parameter names that carry a special purpose. Table 4-1 displays how the route we just defined will convert certain URLs into a RouteValueDictionary:

Table 4.1. URL Parameter Value Mapping Examples

URL

URL PARAMETER VALUES

/products/display/123

{first} = products{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"

If you actually make a request to the URLs listed above, you'll notice that your ASP.NET MVC application will appear to be broken. While you can define a route with any parameter names you'd like, there are certain special parameter names 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 {controller} value and attempts to locate a type of that name (case insensitively) that also inherits from 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 4-3.txt

so that it contains the special URL parameter names.

Now looking again at the first example in the previous table, you see that the request for /products/list/123 is a request for a {controller} named Products. ASP.NET MVC takes that value and appends the Controller suffix to get a type name, ProductsController. If a type of that name that implements the IController interface exists, 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 only applies to controller classes that inherit from the System.Web.Mvc.Controller base class. Continuing with the example of /products/list/123, the method of ProductsController that MVC will invoke is List.

Note that the third URL in the preceding table, while it is a valid route URL, will probably not match any real Controller and action, as it would attempt to instantiate a Controller named a.bController and call the method named c-d, which are not valid method names.

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

public class ProductsController : Controller
{
  public ActionResult Display(int id)
  {
    //Do something
    return View();
  }
}
Code snippet 4-4.txt

a request for /products/display/123 would cause MVC to instantiate this class and call the Display method, passing in 123 for the id. You'll get more into the details of how Controllers work in Chapter 5 after you've mastered Routing.

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 4-5.txt

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

It is even possible to have URL segments that intermix 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 4-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 4-2) will help you see how the URL pattern corresponds to matching URLs.

Table 4.2. Route URL Patterns and Examples

ROUTE URL PATTERN

EXAMPLES OF URLS THAT MATCH

{controller}/{action}/{category}

/products/list/beverages/blog/posts/123

service/{action}-{format}

/service/display-xml

{reporttype}/{year}/{month}/{date}

/sales/2008/1/23

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:

public class ProductsController : Controller
{
  public ActionResult List()
  {
    //Do something
    return View();
  }
}
Code snippet 4-7.txt

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

/products/list
Code snippet 4-8.txt

However, given the route URL defined above, {controller}/{action}/{id}, this won't work, as this route only matches URLs containing three segments and /products/list only contains two segments.

At this point, it would seem you need to define a new route that looks like the above route, but only defines two segments like {controller}/{action}. Wouldn't it be nicer 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 has the notion of default values for parameter segments. For example, you can define the route like this:

routes.MapRoute("simple", "{controller}/{action}/{id}", new {id = ""});
Code snippet 4-9.txt

The new {id = ""} defines a default value for the {id} parameter. This allows this route to match requests for which the id parameter is missing (supplying an empty string as the value for {id} in those cases). In other words, this route now matches any two or three segment URLs, as opposed to only matching three segment URLs before you tacked on the defaults.

This now allows you to call the List action method, using the URL /products/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 = "" , action="index"});
Code snippet 4-10.txt

This example supplies a default value for the {action} parameter within the URL via the Defaults dictionary property of the Route class. While the URL pattern of {controller}/{action} would normally only match a two-segment URL, by supplying a default value for one of the parameters, this route no longer requires that the URL contain two segments. It may now simply contain the {controller} parameter in order to match this route. In that case, the {action} value is supplied via the default value.

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

Table 4.3. URL Patterns and What They Match

ROUTE URL PATTERN

DEFAULTS

EXAMPLES OF URLS THAT MATCH

{controller}/{action}/{id

new {id=""}

/products/display/beverages/products/list

{controller}/{action}/{id

new {controller="home", action="index", id=""}

/products/display/beverages/products/list/products/

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} like new{action="index"} is effectively the same as not having a default value for {action} because there is no default value for the {id} parameter.

Why is this the case?

A quick example will make the answer to this question clear. Suppose that Routing allowed a middle parameter to have a default value and you had the following two routes defined:

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

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

The problem here is which route the request should match is ambiguous and difficult to keep track of when defining routes. Thus, default values only work when every URL parameter after the one with the default also has a default value assigned. Thus, in the previous route, if you have a default value for {action}, you must also have a default value for {id}, which is defined after {action}.

Routing treats how it handles default values slightly different 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 4-11.txt

Notice that there is a string literal "-" between the {controller} and {action} parameters. It is clear that a request for /products-list will match this route, but should a request for /products- match? Probably not, as 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."

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 request URLs:

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

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

Both of these URLs contain three segments and would both 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, it seems clear that they should map to different things. So 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:

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 4-12.txt

In the above snippet, you create a route with three segments, {year}, {month}, and {day}, and you constrain each of these three segments to be digits via a constraints dictionary. The dictionary is specified again using an anonymous object initializer as a shortcut. The constraint for the {year} segment is

year = @"d{4}"

Note

Note that we use the @ character here to make this a verbatim string literal so that we don't have to escape the backslashes. If you omit the @ character, you would need to change this string to "\d{4}".

The keys for the constraint dictionary map to the URL parameter that they constrain. The regular expression value for the key specifies what the value of that URL parameter must match in order for this route to match. 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 does not match, the route is not a match for the request, and routing moves onto the next route.

So, in this case, the year must be a four-digit string. Thus this route matches /2008/05/25 but doesn't match /08/05/25 because 08 is not a match for the regular expression @"d{4}".

Note

Note that we put our new route before the default simple route. Recall that routes are evaluated in order. Since 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 from 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 a later section.

Named Routes

When constructing a URL, it's quite possible that more than one route matches the information provided to the RouteCollection in order to construct that route URL. In this case, the first match wins. In order to specify that a specific route should construct the URL, you can specify a name for the route. The name of a route is not actually a property of RouteBase or Route. It is only used when constructing a route (not when matching routes); therefore, the mappings of names to routes is managed by the RouteCollection internally. When adding a route to the collection, the developer can specify the name using an overload.

Example of adding a named route:

public static void RegisterRoutes(RouteCollection routes)
{
    routes.MapRoute("MyRoute",
         "reports/{year}/{month}", new ReportRouteHandler()));
}
Code snippet 4-13.txt

The name of the route is not stored with the route but managed by the route table.

Areas

Areas, newly introduced with 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 subsections, which can make them a lot easier to manage.

We'll talk about how to set up a project with multiple areas in Chapter 13, but you should be aware that handling routes in a multi-project solution requires some additional considerations.

Area Route Registration

Area routes are configured by creating classes for each area that derive from the AreaRegistration class, overriding AreaName and RegisterArea. To register these area routes, you'll call AreaRegistration.RegisterAllAreas from RegisterRoutes in your main application.

public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
    AreaRegistration.RegisterAllAreas();
    routes.MapRoute(
        "Default",
        "{controller}/{action}/{id}",
        new { controller = "Home", action = "Index", id = "" }
    );
}
Code snippet 4-14.txt

We'll look at 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

What happens when you have two routes with the same name in different areas of the same MVC solution? You'll get an exception stating that "The controller name 'Home' is ambiguous between the following types: AreasDemoWeb.Controllers.HomeController AreasDemoWeb.Areas.Blogs.Controllers.HomeController."

One way to prevent that exception is to use unique Controller names across your projects. However, you might not want to change your Controller names for a variety of reasons (e.g., you don't want to affect your generated route URLs). In that case, you can specify a namespace for a controller action when creating a named URL, which will give you a unique way to refer to each specific controller action regardless of area. Listing 4-1 shows how you'd do that:

Example 4.1. Listing 4-1.txt

routes.MapRoute(
    "Default",
    "{controller}/{action}/{id}",
    new { controller = "Home", action = "Index", id = "" },
new [] { "AreasDemoWeb.Controllers" }
);

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

Catch-All Parameter

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

For example, the route in Listing 4-2 ...

Example 4.2. Listing 4-2.txt

public static void RegisterRoutes(RouteCollection routes)
{
    routes.MapRoute("catchallroute", "query/{query-name}/{*extrastuff}",
        new QueryRouteHandler));
}

... would handle requests like the following:

Table 4.4. Listing 4-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.)

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

  • {title}-{author}

  • Book{title}and{foo}

  • {filename}.{ext}

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

  • {foo}{bar}

  • Xyz{foo}{bar}blah

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, we try to match as much 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 only match "asp". But because URL parameters are greedy, it 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.

Table 4-5 demonstrates how various route URLs with multiple parameters would match. Note that we use the shorthand for {foo=bar} to indicate that the URL parameter {foo} has a default value "bar".

Table 4.5. Matching Route URLs with Multiple Parameters

ROUTE URL

REQUEST URL

ROUTE DATA RESULT

NOTES

{filename}.{ext}

/Foo.xml.aspx

filename="Foo.xml"ext="aspx"

The {filename} parameter did not stop at the first literal "." character, but matched greedily instead.

My{location}-{sublocation}

/MyHouse-LivingRoom

location="House"sublocation="LivingRoom"

 

{foo}xyz{bar}

/xyzxyzxyzblah

foo="xyzxyz"bar="blah"

Again, greedy matching

StopRoutingHandler and IgnoreRoute

There are situations in which the developer may wish to exclude certain URLs from being routed. One way to do this is to use the StopRoutingHandler. Listing 4-3 shows adding a route the manual way, by creating a route with a new StopRoutingHandlerStopRoutingHandler and adding the route to the RouteCollection.

Example 4.3. Listing 4-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 object just like MapRoute, which you've seen before. It's a convenience, and using this new method along with MapRoute changes Listing 4-3 to look like Listing 4-4.

Example 4.4. Listing 4-4.txt

public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
    routes.MapRoute(null, "reports/{year}/{month}", new MvcRouteHandler());
}

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, since routes are resolved by the ASP.NET MVC run time, beyond the reach of Visual Studio breakpoints. A routing bug will completely 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 Phil wrote the Routing Debugger.

The Routing Debugger replaces your route handler with a DebugRouteHandler, which attempts to match all of your routes in turn and displays diagnostic data on all routes and route parameters.

There are two steps to use it:

  1. Download the RouteDebug.dll file from http://code.haacked.com/mvc-1.0/RouteDebug-Binary.zip and place it in your /bin folder. You can leave it in your /bin folder in your development environment, since it has no effect when it's not enabled.

  2. To enable Route Debugging, temporarily change your Application_Start event to add a call to RouteDebug.RouteDebugger.RewriteRoutesForTesting(RouteTable.Routes) as shown in Listing 4-5.

    Example 4.5. Listing 4-5.txt

    protected void Application_Start()
    {
        RegisterRoutes(RouteTable.Routes);
        RouteTable.Routes.RouteExistingFiles = false;
        RouteDebug.RouteDebugger
    .RewriteRoutesForTesting(RouteTable.Routes);
    }

As long as the Route Debugger is enabled, it will handle all routing requests instead of the standard routing engine. Instead of invoking controller actions, it displays the route data pulled from the request of the current request in the address bar (see Figure 4-1). This allows 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.

FIGURE 4-1

Figure 4.1. FIGURE 4-1

Note

Phil 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. While 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 one of the primary responsibilities for routes. The other primary responsibility of the routing system is to construct a URL that corresponds to a specific route. When generating a URL, a request for that URL should match the route that was selected to generate the URL. This allows routing to be a complete two-way system for handling both outgoing and incoming URLs.

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 is a simple abstraction based on the RouteCollection and RouteBase classes. It's instructive to first look at how routing works with these classes before digging into the interaction of the more complex Route class with routing. URL construction starts with a method call, RouteCollection.GetVirtualPath, passing in the RequestContext and user-specified route values (dictionary) used to select the desired route in the form of a dictionary of parameter values.

  1. The route collection loops through every route and asks each one, "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 can answer that question (i.e., it matches), it returns a VirtualPathData instance containing the URL. If not, it returns null, and the routing system moves onto the next route in the list, in the order they were declared.

Detailed Look at URL Generation

The Route class provides a specific more powerful default implementation of the above high-level algorithm. This is the logic most developers will use for routing.

Here is how the URL generation algorithm works, expressed as an outlined use case.

Named Routes

For named routes, you can pass the name for the route to the GetVirtualPath method. In this case, you don't iterate through all routes looking for a match; you simply grab the route corresponding to the specified name and apply the route matching rules to that route to see if it can generate a URL.

Ambient Values

There are scenarios in which 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.

Ambient Values and Default Values without Corresponding URL Parameter

Yeah, this title is a mouthful. This is a unique scenario that bears explanation. Suppose that you have the following routes defined. Notice the first route has no {controller} parameter in the URL, but there is a default value for the controller URL parameter.

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

    routes.MapRoute("another-route", "{controller}/{action}",
new {controller="home", action="list", page=0});
}
Code snippet 4-17.txt

Also suppose that the current request looks like this:

/home/list

The route data for the current request (ambient values) looks like Table 4-7.

Table 4.7. Route Data for the Current Request

KEY

VALUE

Controller

home

Action

list

Now suppose that you want to generate a URL pointing to the TodoController like this:

VirtualPathData vp = routes.GetVirtualPath(null,
  "todo-route", new RouteValueDictionary());

  if(vp != null)
  {
    return vp.VirtualPath;
  }
  return null;
}
Code snippet 4-18.txt

According to the rules listed in the first "Simple Case" above, because there is a default value for {controller} but {controller} is not within the URL, any user-supplied value for {controller} must match the default value.

In this case, you only look at the user-supplied value and not the ambient value. So this call would match the first route.

Overflow Parameters

Overflow parameters are route values explicitly passed into the GetVirtualPath that are not specified in the route URL. Note that ambient values are not used as overflow parameters. Overflow parameters used in route generation are appended to the generated URL as query string parameters unless the overflow parameter is specified in the route's Defaults dictionary or in the route's Constraints dictionary. In that case, the parameter value must match the corresponding default value.

Again, an example is most instructive in this case. Assume that the following routes are defined: Note that in this case, we're not defining ASP.NET MVC routes, so we switch from calling MapRoute to explicitly adding routes to our RouteCollection. When explicitly adding routes, it is necessary to specify a route handler (an instance of a type that implements IRouteHandler). The MapRoute method specifies the MvcRouteHandler as the route handler. In this example, we specify a ReportRouteHandler, which we made up for the sake of discussion.

public static void RegisterRoutes(RouteCollection routes)
{
    routes.Add(new Route
     (
        "blog/{user}/{action}"
        , new ReportRouteHandler()
    )
    {
        Defaults = new RouteValueDictionary{
             {"controller", "blog"},
             {"user", "admin"}}
    });

    routes.Add(new Route
    (
        "forum/{user}/{action}"
        , new ReportRouteHandler()
    )
    {
        Defaults = new RouteValueDictionary{
               {"controller", "forum"},
               {"user", "admin"}}
    });
}
Code snippet 4-19.txt

In this example, there is no route URL parameter for controller, but the route does define a default. Any requests that match the first route will have the value blog for the key controller in the RouteData. Any requests that match the second route will have the value forum.

Now suppose that the developer wants to generate a URL:

string url1 = RouteCollection.GetVirtualPath(
            context,
            new {action="Index", controller="forum"}).VirtualPath;
            //Should check for null, but this is an example.

VirtualPathData vpd2 = RouteCollection.GetVirtualPath(
            context,
            new {action="Index", controller="blah"});
            //returns null.
Code snippet 4-20.txt

The first URL generated will be /forum/admin/Index. Because the call to GetVirtualPath specifies the overflow parameter controller, but the routes don't specify controller as a parameter, the defaults of the route are checked to see if there's an exact match.

The default of user has a corresponding parameter in the route URL, so its value is used in URL generation and doesn't need to be specified in the call to GetVirtualPath.

The second URL will return null because the call to GetVirtualPath doesn't match any of the routes. Even though the controller value specified is an overflow parameter, these routes define a default with the same name and the values do not match.

More Examples of URL Generation with the Route Class

Let's assume that the following route is defined:

void Application_Start(object sender, EventArgs e)
{
    RouteTable.Routes.Add(new Route
    (
        "reports/{year}/{month}/{day}"
        , new ReportRouteHandler()

    )
    {
        Defaults = new RouteValueDictionary{{"day",1}}
    });
}
Code snippet 4-21.txt

Here are some results of some GetVirtualPath calls that take the following general form:

RouteCollection.GetVirtualPath(
    context,
    new RouteValueDictionary {
      {param1, value1},
      {param2,value2},
      ...,
      {paramN,valueN}
     });
Code snippet 4-22.txt

Parameters and the resulting URL are shown in Table 4-8.

Table 4.8. 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

Note

RouteCollection.GetVirtualPath will automatically prepend the ApplicationPath, so subclasses of RouteBase should not do this.

UNDER THE HOOD: HOW ROUTES TIE YOUR URL TO AN ACTION

In the last section, you walked through how routes map to controller actions within the MVC framework. In this section, you take a look under the hood to get a better look at how this happens. 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 the early stages of ASP.NET MVC 1.0 implementation, this was true, but after a while, it became apparent that this was a more generally useful feature. The ASP.NET Dynamic Data team, in particular, was also interested in using it in their feature. At that point, Routing became a more general-purpose feature that has neither internal knowledge of nor dependency on MVC.

One very outward bit of proof that Routing is separate is not just that it's a separate assembly but that it lives in the System.Web.Routing namespace, and not a theoretical System.Web.Mvc.Routing. You can glean a lot reading into namespaces.

Note

The discussion here focuses on routing for IIS 7 Integrated mode. There are some slight differences when using routing with IIS 7 Classic mode or IIS 6. When using the Visual Studio built-in web server, the behavior is very similar to the IIS 7 Integrated mode.

The High-Level Request Routing Pipeline

The routing pipeline consists of the following high-level steps:

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

  2. If a route matches, then the Routing module grabs the IRouteHandler from that route.

  3. The Routing module calls GetHandler from the IRouteHandler, which returns an IHttpHandler. Recall that a typical ASP.NET Page (aka System.Web.UI.Page) is nothing more than an IHttpHandler.

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

  5. In the case of MVC, the IRouteHandler is by default an instance of MvcRouteHandler, which, in turn, returns an MvcHandler (implement IHttpHandler). The MvcHandler is responsible for instantiating the correct controller and calling the action method on that controller.

Route Matching

At its core, routing is simply matching requests and extracting route data from that request and passing it to an IRouteHandler. The algorithm for route matching is very simple from a high-level perspective. When a request comes in, the UrlRoutingModule iterates through each route in the RouteCollection accessed via RouteTable.Routes in order. It then asks each route, "Can you handle this request?" If the route answers "Yes I can!" then the route lookup is done and that route gets to handle the request.

The question of whether a route can handle a request is asked by calling the method GetRouteData. The method returns null if the current request is not a match for the route. (In other words, there's no real conversation going on between the module and routes.)

RouteData

Recall that when we call GetRouteData, it returns an instance of RouteData. What exactly is RouteData? RouteData contains information about the route that matched a particular request, including context information for the specific request that matched.

Recall in the previous section that we showed a route with the following URL: {foo}/{bar}/{baz}. When a request for /products/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 dictionary for each url parameter in the route URL.

So in the case of {foo}/{bar}/{baz}, you would expect the dictionary to contain at least three keys: "foo", "bar", and "baz". In the case of /products/list/123, the URL is used to supply values for these dictionary keys. In this case, foo = products, bar = list, and baz = 123.

ADVANCED ROUTING WITH CUSTOM CONSTRAINTS

Earlier, we covered how to use regular expression constraints to provide fine-grained control over route matching. 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, the Route class interprets the string as a regular expression constraint. However, it is possible to pass in constraints other than regular expression strings.

Routing provides an IRouteConstraint class 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);
}
Code snippet 4-23.txt

When defining the constraints dictionary for a route, supplying a dictionary value that is an instance of a type that implements IRouteConstraint, instead of a string, will cause the route engine to call the Match method on that route constraint to determine whether or not the constraint is satisfied for a given request.

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 only match a specific set of HTTP methods (verbs).

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

routes.MapRoute("name", "{controller}", null
  , new {httpMethod = new HttpMethodConstraint("GET")} );
Code snippet 4-24.txt

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

ROUTE EXTENSIBILITY

Most of the time, you will find little need to write a custom route. However, in those rare cases that you do, the Routing API is quite flexible. At one extreme, you can toss out everything in the Route class and inherit directly from RouteBase instead. That would require that you implement GetRouteData and GetVirtualPath yourself. In general, I wouldn't recommend that approach except in the most extreme scenarios. Most of the time, you'll want to inherit from Route and add some extra scenario-specific behavior.

Having said that, let's look at an example where we might implement RouteBase directly instead of inheriting from Route. In this section, we'll look at implementing a RestRoute class. This single route will match a set of conventional URLs that corresponds to a "Resource", in our case, a Controller.

For example, when you're done, you'll be able to define the following route using a new extension method you will create:

routes.MapResource("Products");

and that will add a RestRoute to the RouteTable.Routes collection. That route will match the set of URLs in Table 4-9.

Table 4.9. URLs that Match the Route

URL

DESCRIPTION

/products

Displays all products.

/product/new

Renders a form to enter a new product.

/product/1

Where 1 is the ID

/product/1/edit

Renders a form to edit a product.

However, what happens when one of these URLs is requested depends on the HTTP method of the request. For example, a PUT request to /product/1 will update that product, whereas a DELETE request to that URL will delete the product.

Note that at the time of this writing, browsers do not support creating a form with a method of PUT or DELETE, so these URLS would require writing a custom client to call everything appropriately. We could implement a "cheat" to enable this for our routes, but that is left as an exercise for the reader.

The first step is to create a new class that implements RouteBase. RouteBase is an abstract method with two methods we'll need to overload:

public class RestRoute : RouteBase
{
    public override RouteData GetRouteData(HttpContextBase httpContext)
    {
        //...
    }

    public override VirtualPathData GetVirtualPath(RequestContext requestContext,
           RouteValueDictionary values)
    {
        //...
    }
}
Code snippet 4-25.txt

We need to add a constructor that takes in the name of the resource. This will end up corresponding to our Controller class name. The strategy we'll take here is to have this route actually encapsulate an internal set of routes that correspond to the various URLs we will match so the constructor will instantiate those routes and put them in an internal List. Along with the constructor, we will implement a simple helper method for adding routes, a Resource property, and an internal list for the routes:

List<Route> _internalRoutes = new List<Route>();

public string Resource { get; private set; }

public RestRoute(string resource)
{
    this.Resource = resource;
    MapRoute(resource, "index", "GET", null);
    MapRoute(resource, "create", "POST", null);
    MapRoute(resource + "/new", "newitem", "GET", null);
    MapRoute(resource + "/{id}", "show", "GET", new { id = @"d+" });
    MapRoute(resource + "/{id}", "update", "PUT", new { id = @"d+" });
    MapRoute(resource + "/{id}", "delete", "DELETE", new { id = @"d+" });
    MapRoute(resource + "/{id}/edit", "edit", "GET", new { id = @"d+" });
}

public void MapRoute(string url, string actionName, string httpMethod,
                     object constraints)
{
    RouteValueDictionary constraintsDictionary;
if (constraints != null)
    {
        constraintsDictionary = new RouteValueDictionary(constraints);
    }
    else
    {
        constraintsDictionary = new RouteValueDictionary();
    }
    constraintsDictionary.Add("httpMethod", new HttpMethodConstraint(httpMethod));

    _internalRoutes.Add(new Route(url, new MvcRouteHandler())
    {
        Defaults = new RouteValueDictionary(new
              { controller = Resource, action = actionName }),
        Constraints = constraintsDictionary
    });
}
Code snippet 4-26.txt

Finally, we need to implement the methods of RouteBase. These are fairly straightforward. For each method, we iterate through our internal list of routes and call the corresponding method on each route, returning the first one that returns something that isn't null.

public override RouteData GetRouteData(HttpContextBase httpContext)
{
    foreach (var route in this._internalRoutes)
    {
        var rvd = route.GetRouteData(httpContext);
        if (rvd != null) return rvd;
    }
    return null;
}

public override VirtualPathData GetVirtualPath(RequestContext requestContext,
  RouteValueDictionary values)
{
    foreach (var route in this._internalRoutes)
    {
        VirtualPathData vpd = route.GetVirtualPath(requestContext, values);
        if (vpd != null) return vpd;
    }
    return null;
}
Code snippet 4-27.txt

To use this route, we simply do the following in the RegisterRoutes method of Global.asax.cs:

routes.Add(new RestRoute("Products"));
Code snippet 4-28.txt

Of course, we should make sure to have a Controller with the corresponding methods. The following is the outline of such an implementation:

public class ProductsController : Controller
{
    public ActionResult Index()
    {
        return View();
    }

    public ActionResult New()
    {
        return View();
    }

    public ActionResult Show(int id)
    {
        return View();
    }

    public ActionResult Edit(int id)
    {
        return View();
    }

    public ActionResult Update(int id)
    {
        //Create Logic then...
        return RedirectToAction("Show", new { id = id });
    }

    public ActionResult Create()
    {
        //Create Logic then...
        return RedirectToAction("Index");
    }

    public ActionResult Destroy(int id)
    {
        //Delete it then...
        return RedirectToAction("Index");
    }
}
Code snippet 4-29.txt

As you can see in this example, it is possible to extend Routing in ways not anticipated by the product team to provide very custom control over the URL structure of your application.

Editable Routes

Let's look at another example of route extensibility in which we'll set up editable routes. This allows the routes to be changed without having to recompile the application.

In this implementation, we'll need to place the routes in a Config folder in your web root, as shown in Figure 4-5.

Note that you're also using Visual Studio's Properties dialog to mark the file's Build Action as "Content" so that it's not compiled into the application, as illustrated in Figure 4-6.

FIGURE 4-5

Figure 4.5. FIGURE 4-5

FIGURE 4-6

Figure 4.6. FIGURE 4-6

The authors have intentionally excluded the Route.cs file from build-time compilation because they want it to be compiled dynamically at run time. The code for Route.cs is shown in Listing 4-6.

Example 4.6. Listing 4-6.txt

using System.Web.Mvc;
using System.Web.Routing;
using EditableRoutesWeb;

// Use this one for Full Trust
public class Routes : IRouteRegistrar
{
    public void RegisterRoutes(RouteCollection routes)
    {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

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

One thing you'll notice is that this class implements an interface named IRouteRegistrar. This is an interface we created and added to our web application (although it could be defined in another assembly).

The code in Global.asax.cs for this application simply calls an extension method shown in Listing 4-7.

Example 4.7. Listing 4-7.txt

protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();
    RouteTable.Routes.RegisterRoutes("~/Config/Routes.cs");
}

This all seems simple enough, but that's because we've hidden all the magic in that extension method. We're using two tricks that will allow us to dynamically generate the routing code in medium trust, without causing an application restart:

  1. We use the BuildManager to dynamically create an assembly from the Routes.cs file. From that assembly, we can create an instance of the type Routes and cast it to IRouteHandler.

  2. We use an ASP.NET Cache to get a notification of when the Routes.cs file changes, so we'll know it needs to be rebuilt. The ASP.NET Cache allows us to set a cache dependency on a file and a method to call when the file changes (invalidating the cache).

With those two tricks, we can add a cache dependency pointing to Routes.cs and a callback method that will reload the routes when Routes.cs is changed, as shown in Listing 4-8.

Example 4.8. Listing 4-8.txt

using System;
using System.Web.Compilation;
using System.Web.Routing;

namespace EditableRoutesWeb
{
public static class RouteRegistrationExtensions
{
    public static void RegisterRoutes(this RouteCollection routes,
                                string virtualPath)
    {
        ConfigFileChangeNotifier.Listen(
                                virtualPath, vp => routes.ReloadRoutes(vp));
    }

    static void ReloadRoutes(this RouteCollection routes, string virtualPath)
    {
        var assembly = BuildManager.GetCompiledAssembly(virtualPath);
        var registrar = assembly.CreateInstance("Routes") as IRouteRegistrar;
        using(routes.GetWriteLock())
        {
            routes.Clear();
            registrar.RegisterRoutes(routes);
        }
}
}
}

This makes use of a ConfigFileChangeNotifier from David Ebbo's work on Dynamic Data, shown in Listing 4-9.

Example 4.9. Listing 4-9.txt

using System;
using System.Collections.Generic;
using System.Web;
using System.Web.Caching;
using System.Web.Hosting;

namespace EditableRoutesWeb
{
    public class ConfigFileChangeNotifier
    {
        private ConfigFileChangeNotifier(Action<string> changeCallback)
            : this(HostingEnvironment.VirtualPathProvider, changeCallback)
        {
        }

        private ConfigFileChangeNotifier(VirtualPathProvider vpp,

       Action<string> changeCallback) {
            _vpp = vpp;
            _changeCallback = changeCallback;
        }

        VirtualPathProvider _vpp;
        Action<string> _changeCallback;

        // When the file at the given path changes, we'll call the supplied action.
        public static void Listen(string virtualPath, Action<string> action) {
            var notifier = new ConfigFileChangeNotifier(action);
            notifier.ListenForChanges(virtualPath);
        }

        void ListenForChanges(string virtualPath) {
            // Get a CacheDependency from the BuildProvider, so that we know
                    // anytime something changes
            var virtualPathDependencies = new List<string>();
            virtualPathDependencies.Add(virtualPath);
            CacheDependency cacheDependency = _vpp.GetCacheDependency(
                virtualPath, virtualPathDependencies, DateTime.UtcNow);

            HttpRuntime.Cache.Insert(virtualPath /*key*/,
                    virtualPath /*value*/,
                    cacheDependency,
                    Cache.NoAbsoluteExpiration,
                    Cache.NoSlidingExpiration,
CacheItemPriority.NotRemovable,
                    new CacheItemRemovedCallback(OnConfigFileChanged));
        }

        void OnConfigFileChanged(string key, object value,

       CacheItemRemovedReason reason) {
            // We only care about dependency changes
            if (reason != CacheItemRemovedReason.DependencyChanged)
                return;

            _changeCallback(key);

            // Need to listen for the next change
            ListenForChanges(key);
        }
    }
}

With this in place, we can now change routes within the Routes.cs file in the Config directory after we've deployed the application.

Note

Technically, a recompilation is happening, but it's happening dynamically at run time when the file changes and there's no need to restart our entire App Domain, which is one benefit of this approach over using the code in App_Code.

USING ROUTING WITH WEB FORMS

While the main focus of this book is on ASP.NET MVC, Routing is now a core feature of ASP.NET, so it can be used with Web Forms as well. We'll first look at the easy case, ASP.NET 4, since it includes full support for Routing with Web Forms. Next we'll look at how you can use routing in ASP.NET 3.5 SP1.

Routing With Web Forms in ASP.NET 4

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",
"products/search/{term}",
             "~/ProductSearch.aspx");
}
Code snippet 4-30.txt

The only real difference from an MVC route is the last parameter, in which we 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 = Page.RouteData.Values["term"] as string;

    Label1.Text = "Search Results for: " + Server.HtmlEncode(term);
    ListView1.DataSource = GetSearchResults(term);
    ListView1.DataBind();
}
Code snippet 4-31.txt

You can use Route values in your markup as well, using the new <asp:RouteParameter> object to bind a segment value to a database query or command. For instance, using the above route, if we browsed to /products/search/chai, we could query by the passed route value using the following SQL Command:

<asp:SqlDataSource id="sqldatasource1" runat="server"
    ConnectionString="<%$ ConnectionStrings:Northwind %>"
    SelectCommand="SELECT Products FROM WHERE Name LIKE @searchterm + '%'">
  <SelectParameters>
    <asp:RouteParameter name="searchterm" RouteKey="term"  />
  </SelectParameters>
</asp:SqlDataSource>
Code snippet 4-32.txt

You can also use the RouteValue ExpressionBuilder to write out a Route parameter value a little more elegantly than just writing out Page.RouteValue["key"]. If we wanted to write out the search term in a label, we could do the following:

<asp:Label ID="Label1" runat="server" Text="<%$RouteValue:Term%>" />
Code snippet 4-33.txt

We can generate outgoing URLs for using the Page.GetRouteUrl() in codebehind logic method:

string url = Page.GetRouteUrl(
    "product-search",
    new { term = "chai" });
Code snippet 4-34.txt

The corresponding RouteUrl ExpressionBuilder allows us to construct outgoing Route in our markup:

<asp:HyperLink ID="HyperLink1"
       runat="server"
       NavigateUrl="<%$RouteUrl:SearchTerm=Chai%>">
             Search for Chai
</asp:HyperLink>
Code snippet 4-35.txt

Routing with Web Forms in ASP.NET 3.5

In this section, we'll walk through building a WebFormRouteHandler, which is a custom implementation of IRouteHandler. The WebFormRouteHandler will allow us to define routes that correspond to an ASPX Page in our application, using code like this (defined within the Application_Start method of a normal Web Forms project):

RouteTable.Routes.Add(new Route("somepage",
    new WebFormRouteHandler("~/webforms/somepage.aspx"));
Code snippet 4-36.txt

Before we dive into the code, there is one subtle potential security issue to be aware of when using routing with URL Authorization. Let me give an example.

Suppose that you have a web site and you wish to block unauthenticated access to the admin folder. With a standard site, one way to do so would be to drop the following web.config file in the admin folder:

<?xml version="1.0"?>
<configuration>
    <system.web>

        <authorization>
            <deny users="*" />
        </authorization>

    </system.web>
</configuration>
Code snippet 4-37.txt

OK, I am being a bit draconian here. I decided to block access to the admin directory for all users. Attempt to navigate to the admin directory, and you get an access-denied error. However, suppose that you use a naive implementation of WebFormRouteHandler to map the URL /fizzbin to the admin directory like this:

RouteTable.Routes.Add(new Route("fizzbin",
    new WebFormRouteHandler("~/admin/secretpage.aspx"));

Now, a request for the URL /fizzbin will display secretpage.aspx in the admin directory. This might be what you want all along. Then again, it might not be.

In general, I believe that users of routing and Web Forms will want to secure the physical directory structure in which Web Forms are placed using URL authorization. One way to do this is to call UrlAuthorizationModule.CheckUrlAccessForPrincipal on the actual physical virtual path for the Web Form.

As mentioned before in this chapter, this is one key difference between routing and URL rewriting. Routing doesn't actually rewrite the URL. Another key difference is that routing provides a means to generate URLs as well and is thus bidirectional.

The following code is an implementation of WebFormRouteHandler, which addresses this security issue. This class has a Boolean property on it that allows you to not apply URL authorization to the physical path if you'd like. (In following the principle of "secure by default," the default value for this property is true, which means that it will always apply URL authorization.)

public class WebFormRouteHandler : IRouteHandler
{
  public WebFormRouteHandler(string virtualPath) : this(virtualPath, true)
  {
  }

  public WebFormRouteHandler(string virtualPath, bool checkPhysicalUrlAccess)
  {
    this.VirtualPath = virtualPath;
    this.CheckPhysicalUrlAccess = checkPhysicalUrlAccess;
  }

  public string VirtualPath { get; private set; }

  public bool CheckPhysicalUrlAccess { get; set; }

  public IHttpHandler GetHttpHandler(RequestContext requestContext)
  {
    if (this.CheckPhysicalUrlAccess
      && !UrlAuthorizationModule.CheckUrlAccessForPrincipal(this.VirtualPath
              ,  requestContext.HttpContext.User
              , requestContext.HttpContext.Request.HttpMethod))
      throw new SecurityException();

    var page = BuildManager
      .CreateInstanceFromVirtualPath(this.VirtualPath
        , typeof(Page)) as IHttpHandler;

    if (page != null)
    {
var routablePage = page as IRoutablePage;
      if (routablePage != null)
        routablePage.RequestContext = requestContext;
    }
    return page;
  }
}
Code snippet 4-38.txt

You'll notice that the code here checks to see if the page implements an IRoutablePage interface. If your Web Forms page implements this interface, the WebFromRouteHandler class can pass it the RequestContext. In the MVC world, you generally get the RequestContext via the ControllerContext property of Controller, which itself inherits from RequestContext.

The RequestContext is needed in order to call the various API methods for URL generation. We'll provide that via an IRoutablePage interface (shown in Code Listing 4-10) a RoutablePage abstract base class that inherits from Page (shown in Code Listing 4-11).

Example 4.10. Listing 4-10.txt

using System;
using System.Web;
using System.Web.Routing;

namespace WebFormRouting
{
    /// <summary>
    /// Pages don't have to implement this interface, but the ones that
    /// do will be able to generate outgoing routing URLs.
    /// </summary>
    public interface IRoutablePage : IHttpHandler
    {
        RequestContext RequestContext { get; set; }
        HtmlHelper Html { get; }
        UrlHelper Url { get; }
    }
}

Example 4.11. Listing 4-11.txt

using System;
using System.Web.UI;
using System.Web.Routing;
using System.Web;

namespace WebFormRouting
{
    /// <summary>
    /// Handy base class for routable pages.
    /// </summary>
public abstract class RoutablePage : Page, IRoutablePage
    {
        public RoutablePage() {
            this.Html = new HtmlHelper(this, RouteTable.Routes);
            this.Url = new UrlHelper(this, RouteTable.Routes);
        }

        public RequestContext RequestContext {get; set; }

        public RouteData RouteData
        {
            get
            {
                if (RequestContext == null)
                {
                    //Try to manafacture one.
                    var context = new HttpContextWrapper(
                                        HttpContext.Current);
                    var requestContext = new RequestContext(
                                        context, new RouteData());
                    this.RequestContext = requestContext;
                }

                return this.RequestContext.RouteData;
            }
        }

        public HtmlHelper Html {get; private set; }
        public UrlHelper Url { get; private set; }
    }
}

The following code provides an example for how Web Forms routes might be registered within the Global.asax.cs file:

public static void RegisterRoutes(RouteCollection routes)
{
    //first one is a named route.
    routes.MapWebFormRoute("General",
      "haha/{filename}.aspx", "~/forms/haha.aspx");
    routes.MapWebFormRoute ("backdoor", "~/admin/secret.aspx");
}
Code snippet 4-39.txt

The idea is that the route URL on the left maps to the WebForm virtual path to the right.

The code that comes with this book contains a solution that contains all this code you've seen and more, including:

  • WebFormRouting: The class library with the WebFormRouteHandler and helpers

  • WebFormRoutingDemoWebApp: A web site that demonstrates how to use WebFormRouting and also shows off URL generation

  • WebFormRoutingTests: A few non-comprehensive unit tests of the WebFormRouting library Using techniques like WebFormRouting can also enable you to have successful hybrid Web Forms/MVC applications, which can ultimately enable you to use the best of both worlds — MVC when it's appropriate and Web Forms when they are appropriate.

SUMMARY

Routing is much like the Chinese game of Go, simple to learn and a lifetime to master. Well, 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 very sophisticated scenarios in your ASP.NET MVC (and Web Forms) applications.

..................Content has been hidden....................

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