More Flexible Routing For ASP.NET Web Pages

The built-in Web Pages routing system offers a fair degree of freedom in terms of how friendly URLs can be constructed and managed. However, the major limitation with the standard routing mechanism is that it relies on matching segments of the URL to files on disk. ASP.NET MVC and Web Forms enjoy a much more flexible routing system. This article looks at a Package that brings full ASP.NET routing control to Web Pages and examines how to use it.

By default, ASP.NET will always try to match the URL in a request to a file on disk. If no match is found, ASP.NET attempts to see if a match for the URL's pattern can be found in a RouteCollection object. The RouteCollection is a collection of Route objects (sounds obvious when you say it like that...), and each Route object holds the definition of a URL pattern, and instructions on how ASP.NET should handle requests that match each pattern. However, a RouteCollection needs to be populated and a means of handling the request defined. ASP.NET Web Forms and ASP.NET MVC include mechanisms to manage this as part of their standard framework. If you want to add full routing control to your Web Pages application, you need to acquire the Routing For Web Pages package.

The package will install a file called WebPagesRouteHandler.cs into your App_Code folder. It will create an App_Code folder if one doesn't already exist. The file contains three classes: WebPagesRouteHandler, whose job is to locate the correct end point (page) to process the request for a given URL match; RouteCollectionExtension; and ContextExtensions (more of which a bit later). The RouteCollectionExtension class contains one method: MapWebPageRoute() which is used to define a Route and then add it to the RouteCollection.

Defining a Route

A Route has five elements to it:

  • A URL
  • A physical file
  • Default values for route parameters
  • Constraints that must be met for this Route to be a match for a particular URL
  • A name for the Route

Of these, only the first two elements are required. The URL is the one that you want to use to point to resources in your site. They are most likely to be SEO-friendly, for example List/Beverages/ to display all products in the Beverages category rather than, say, ListProducts.cshtml?category=beverages. URL definitions can also be composed of parameters which specify a pattern to match. The List/Beverages URL may be represented as List/{category}/, where {category} is a parameter, or placeholder for a variable. That means that List/Beverages and List/Condiments or indeed List/Anything will all result in matches for this pattern.

The second part of a route is the name of the file that will actually process the request. Note that if the file name without the extension is used as the first segment in any URL, default Web Pages routing will kick in. For that reason, you should avoid naming files the same as a vital part of your URL schema. Continuing with the previous example, if a file called List.cshtml was added to the site, none of the route examples: List/Beverages/, List/{category}/, List/Anything/ would be invoked because a file-on-disk match will be made with List.cshtml.

You can provide default values for parameters. These are stored in a special kind of Dictionary - a RouteValueDictionary. In addition, you can constrain the acceptable values for a parameter through an expression. Route constraints are also stored in a RouteValueDictionary. Finally, you can provide a name to identify each Route by. If you don't provide one, the URL is used instead.

Adding Routes

Routes need to be set up for the lifetime of the application, so the best place to do this is in the _AppStart.cshtml file. A simple Route definition looks like this:

RouteTable.Routes.MapWebPageRoute("List/", "~/ListProducts.cshtml");

This will result in any request to List/ being mapped to the ListProduct.cshtml file. The following example adds a parameter:

RouteTable.Routes.MapWebPageRoute("List/{category}/", "~/ListProducts.cshtml");

No default value for the category parameter has been provided, so a URL must include a value for this segment for it to be considered a match to this Route. List/Beverages/ is a match, as is List/Anything/ or List/1/. If a default value for the category parameter is specified, that segment does not need to be included in the URL for a match to be achieved. In that case, List/ will match. Default parameter values are added to a RouteValueDictionary, with the name of the parameter acting as an entry's key. An anonymous object is used in the MapWebPageRoute method to populate the RouteValueDictionary:

RouteTable.Routes.MapWebPageRoute("List/{category}", "~/ListProducts.cshtml", new { category = "Beverages" });

You can have multiple parameters. In fact, a URL pattern can be composed entirely of parameters. Take a look at the following:

RouteTable.Routes.MapWebPageRoute("{product}/{action}", "~/Catchall.cshtml", new { product = "Beer", action = "Drink" }); 

This will match any one or two part URL because there are default values for both parameters. For that reason, you should be careful when designing URL patterns. Furthermore, when the RouteCollection object is inspected for matches, the first match is returned. If this was added as the first route, all requests consisting of one or two URL segments will result in Catchall.cshtml being requested. A URL with no segments specified (eg www.domain.com) will result in the default document being returned through simple file-on-disk matching, and three ore more segments exceeds the number specified in the pattern. Having said that, a match for three or more segments can be achieved through using a wildcard in the pattern specification. The wildcard character is * and it is applied as follows:

RouteTable.Routes.MapWebPageRoute("{product}/{*action}", "~/Catchall.cshtml", new { product = "Beer", action = "Drink" }); 

Any value can be provided to the parameters in the examples so far. No restrictions or "constraints" have been placed on what is acceptable for a match to be made. You constrain parameter values by providing regular expression patterns that valid values must match. If the category parameter in the first route example expects a word, the regular expression pattern "[a-zA-Z]" will restrict valid values to just letters:

RouteTable.Routes.MapWebPageRoute("List/{category}", "~/ListProducts.cshtml", constraints: new { category = "[a-zA-Z]" });

RouteValues

In the previous example, you need to know what value was passed to the category parameter if you are going to do anything meaningful in ListProducts.cshtml. You acquire the value via the GetRouteValue method, which is the only member of the ContextExtensions class mentioned earlier. The GetRouteValue method requires a valid key:

var category = Context.GetRouteValue("category");

From that point, you can use the value as a parameter in a database call, or in any way that suits your application.

Summary

The built-in Web Pages routing system is pretty flexible, but it is based on matching files on disk. You can achieve some pretty good URLs with this mechanism although you may need to employ a pretty extensive directory structure to give you the right file path to map to a URL. Or you will probably have to contend with some complexity in your pages as you unpick several layers of UrlData. The Web Pages Routing package helps you do away with all that by entirely divorcing the URL from your files. Additionally, the ability to set default values for parameters and constrain valid values offers a very fine level of control.