Using ASP.NET Routing Without ASP.NET MVC
By Scott Mitchell
Introduction ASP.NET MVC is a Microsoft-supported framework for creating ASP.NET applications using a
Model-View-Controller pattern. In a nutshell, ASP.NET MVC allows developers much finer
control over the markup rendered by their web pages, a greater and clearer separation of concerns, better
testability, and cleaner, more SEO-friendly URLs. This article is not about ASP.NET MVC, but rather focuses
on ASP.NET Routing, which is the technology by ASP.NET MVC to allow for intuitive and "hackable" URLs.
There is typically a one-to-one correspondence between the files on the website and the URLs through which visitors interface with the site. For instance, if you worked for an
eCommerce company and were tasked with creating a web page that displayed a list of products for a particular category you'd likely create a new page - say,
ShowProductsByCategory.aspx - and add markup and code so that it displays the products for the category specified via the querystring. Once deployed to a
production environment, visitors would reach this page via the URL www.yoursite.com/ShowProductsByCategory.aspx?CategoryID=categoryID and would
see the products for the category categoryID. The URL entered into the user's browser (less the querystring) - ShowProductsByCategory.aspx - is the same
as the name of the ASP.NET web page file sitting on the web server's file system.
ASP.NET Routing is a library that was introduced in the .NET Framework 3.5 SP1 that decouples the URL from a physical file; it is used extensively in ASP.NET MVC web applications.
With ASP.NET Routing you, the developer, define routing rules that indicate what route patterns map to what physical files. For example, you might indicate that the
URL Categories/CategoryName maps to the ShowProductsByCategory.aspx ASP.NET page, passing along the CategoryName portion of the URL. The
ASP.NET page could then display the products for that category. With such a mapping, users could view products for the Beverages category by visiting
www.yoursite.com/Categories/Beverages rather than visiting the more verbose and less readable www.yoursite.com/ShowProductsByCategory.aspx?CategoryID=1.
While ASP.NET MVC is a great way to get started with ASP.NET Routing, the good news is that these two systems are independent of one another. It's quite possible to use ASP.NET
Routing in a traditional ASP.NET Web Forms application. This article shows how to get ASP.NET Routing up and running in a Web Forms application. Read on to learn more!
This article explores ASP.NET Routing when used with ASP.NET version 3.5 SP1. The ASP.NET Routing system has been enhanced in ASP.NET
version 4.0 and includes a number of new features that make implementing ASP.NET Routing in a Web Forms application much easier and
straightforward. For a look at these new features, check out URL Routing in ASP.NET 4.0.
The URL will continue to be part of the Web user interface for several more years, so a usable site requires:
a domain name that is easy to remember and easy to spell
short URLs
easy-to-type URLs
URLs that visualize 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 that don't change
... Edward Cutrell and Zhiwei Guan from Microsoft Research have conducted an eyetracking study of search engine use that found that people spend 24%
of their gaze time looking at the URLs in the search results. ... We found that searchers are particularly interested in the URL when they are assessing
the credibility of a destination. If the URL looks like garbage, people are less likely to click on that search hit. On the other hand, if the URL looks
like the page will address the user's question, they are more likely to click.
Traditionally, there is a tight coupling between URLs and the names of the files on the web server's file system that handle a particular URL request. Such a coupling has
a number of downsides, one of the most apparent one being that it makes it hard to adhere to the third through fifth items above. It is especially difficult for data-driven
websites to adhere to these above suggestions as data-driven websites typically have a small subset of web pages that display a variety of data based on querystring parameters.
This results in ugly and unreadable URLs like ViewProduct.aspx?ProductID=45134&SKU=128 or worse, ViewProduct.aspx?ProductID=C519EE44-3515-11DE-B118-559256D89593.
The classical way of creating ideal URLs has been to implement URL rewriting, which is where the web server inspects the incoming URL (such as /Categories/Beverages)
and points it to the appropriate physical file (such as /ViewProductsByCategory.aspx?CategoryID=1). URL rewriting can be implemented at the web server level
(see ISAPI_Rewrite) or from the ASP.NET layer via static mapping rules in Web.config or dynamic rules through an HTTP
Module. Implementing dynamic URL rewriting in ASP.NET involves using the HttpContext.RewritePath
method, which internally nudges the request from the custom URL to the actual web page file for processing. See my article
URL Rewriting in ASP.NET, Scott Guthrie's blog post
Tip/Trick: Url Rewriting with ASP.NET, and
A Look at ASP.NET 2.0's URL Mapping for more information on these topics.
Those of you who have used the RewritePath API in the past are probably familiar with some of the quirks and weaknesses in the rewriting approach. The primary
problem with RewritePath is how the method changes the virtual path used during the processing of a request. With URL rewriting, you needed to fix up the
postback destination of each Web Form (often by rewriting the URL a second time during the request) to avoid postbacks to the internal, rewritten URL.
In addition, most developers would implement URL rewriting as a one-way translation because there was no easy mechanism to let the URL rewriting logic work in two directions.
For example, it was easy to give the URL rewriting logic a public-facing URL and have the logic return the internal URL of a Web Form. It was difficult to give the
rewriting logic the internal URL of a Web Form and have it return the public URL required to reach the form. The latter is useful when generating hyperlinks to other Web
Forms that hide behind rewritten URLs.
The ASP.NET Routing system cleanly decouples URLs from web page file names and does so in a manner that is easier to implement and without the aforementioned baggage inherent
in traditional URL rewriting approaches. This article does not explore the depths of ASP.NET Routing - see Routing
with ASP.NET Web Forms and the ASP.NET Routing technical documentation for a deeper look at the ins
and outs of ASP.NET Routing. Instead, this article walks through the steps you will need to perform to use ASP.NET Routing in a Web Forms application.
Also, be sure to download the sample web application, available at the end of this article. It includes a working data-driven, Web Forms-based web application that utilizes
ASP.NET Routing. This demo application is discussed and screen shots are provided in the steps below.
Step 0: Prerequisites
In order to use the ASP.NET Routing system you need to be using ASP.NET 3.5 SP1. If you are using Visual Studio 2008 SP1 or Visual Web Developer 2008 SP1 you're good to go.
Step 1: Add a Reference to System.Web.Routing to Your Project
The classes that power the ASP.NET Routing system live in the System.Web.Routing assembly, which is already installed on your machine's Global Assembly Cache (GAC)
if you have the .NET Framework 3.5 SP1 installed. However, you need to add this assembly to the references of your project. From Visual Studio, right-click on your project
and choose Add References from the context menu. Then, from the .NET tab, scroll down until you find the System.Web.Routing assembly, select it, and click OK.
Step 2: Add the UrlRoutingModule HTTP Module to Your Website's Configuration (and UrlRoutingHandler, If Needed)
When request for a URL like /Categories/Beverages arrives at the web server, ASP.NET must route the request to the appropriate physical file. This routing involves
parsing the URL and determining which route handler to dispatch the request to. We'll discuss route handlers momentarily, but in a nutshell a route handler is a class
that is invoked when a particular URL pattern is received; the route handler is responsible for specifying the HTTP Handler that should process the request. (An HTTP Handler
is a .NET class that can generate content for a request. All ASP.NET pages are HTTP Handlers. For more on HTTP Handlers, consult HTTP
Handlers and HTTP Modules in ASP.NET.)
Registering the UrlRoutingModule HTTP Module entails adding a bit of markup to the <httpModules> element in Web.config.
If you use IIS 7.0 in the development or production environments you'll also need to register the same HTTP Module in the <system.webServer> section.
Furthermore, you'll need to add an HTTP Handler in the <system.webServer> section, as well (UrlRoutingHandler).
Step 3: Define the Routes in Global.asax
To use the ASP.NET Routing system you need to define one to many routes when the application starts. Start by adding the Global Application Class file type to your project
(Global.asax), where we'll add code to the Application_Start event.
The routes defined in Global.asax indicate what route handlers are responsible for what URL
patterns. A popular pattern for MVC applications is Controller/Action/ID, meaning that requests of the form /Products/View/Aniseed Syrup
or Categories/Edit/Beverages would be handled by the configured route handler. You have total flexibility in defining what routes exist in your application.
You can define multiple parts to patterns, define default values for missing parts, and even constraint parts to certain types of inputs.
The demo application available for download at the end of this article is a simple data-driven application that uses the Northwind database and accepts "hackable" URLs of
the following patterns:
/Categories/All - lists all categories in the database
/Categories/CategoryName - lists the products for the specified category
/Products/ProductName - displays information about the specified product
Consequently, I defined three routes in my Global.asax file's Application_Start event handler, as the following code shows. (Note: the
RouteTable object and RouteCollection and Route classes are located in the System.Web.Routing namespace, so you'll
either need to import that namespace or fully qualify these class names, as in System.Web.Routing.RouteTable.)
void RegisterRoutes(RouteCollection routes)
{
// Register a route for Categories/All
routes.Add(
"All Categories",
new Route("Categories/All", new CategoryRouteHandler())
);
// Register a route for Categories/{CategoryName}
routes.Add(
"View Category",
new Route("Categories/{*CategoryName}", new CategoryRouteHandler())
);
// Register a route for Products/{ProductName}
routes.Add(
"View Product",
new Route("Products/{ProductName}", new ProductRouteHandler())
);
}
The RouteTable.Routes collection defines the routes for the application. This collection is passed to the RegisterRoutes method (which I created)
and from there three new Route objects are added to the collection. The Add method takes in two inputs: the name of the route ("All Categories", "View Category",
and "View Product") and a Route object that defines the route. The Route object constructor accepts two inputs: the URL pattern and a
route handler class. (We'll create the route handler class in the next step.)
The first route says, "If a request comes in for /Categories/All have the request handled by CategoryRouteHandler."
The second route says, "Should a request that matches the pattern /Categories/CategoryName arrive, have it handled by CategoryRouteHandler."
Note that the URL pattern contains an asterisk: "Categories/{*CategoryName}". This asterisk indicates that the route should match anything
after the "Categories/" portion, even if the portion after contains forward slashes itself. This is necessary because some of the Northwind category names have forward slashes
in their name, such as the category "Meat/Produce". In short, we want the Routing system to match the URL /Categories/Meat/Produce to the second route defined
above rather than it trying to match it against some pattern with three pieces. See this ASP.NET Forums post for a
bit more background on this matter.
The final route instructs the
system to use the ProductRouteHandler for requests that match the pattern /Products/ProductName. Note that the variable portions of the
patterns use the syntax {parameterName}. When a URL matches one of these patterns - such as /Categories/Beverages - the value of the parameter portion
(Beverages, in this case) is accessible from the route handler, as we'll see in a moment.
Step 4: Create the Route Handler Classes
A route handler class is a class that is passed information about the incoming request and must return an HTTP Handler to process the request. It must implement the
IRouteHandler interface and, as a consequence, must provide at
least one method - GetHttpHandler - which is responsible for returning the HTTP Handler (or ASP.NET page) that will process the request.
Typically, a route handler performs the following steps:
Parse the URL as needed
Load any information from the URL that needs to be passed to the ASP.NET page or HTTP Handler that will handle this request. One way to convey such information is
to place it in the HttpContext.Items collection, which serves as a repository for storing data that persists the length of the request. (See
HttpContext.Items - a Per-Request Cache Store for background on the Items collection.)
Return an instance of the ASP.NET page or HTTP Handler that does the processing
The code for the ProductRouteHandler follows. (The using statements have been omitted for brevity; refer to the download for the complete code for this class.)
public class ProductRouteHandler : IRouteHandler
{
public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
string productName = requestContext.RouteData.Values["ProductName"] as string;
if (string.IsNullOrEmpty(productName))
return Helpers.GetNotFoundHttpHandler();
else
{
// Get information about this product
NorthwindDataContext DataContext = new NorthwindDataContext();
Product product = DataContext.Products.Where(p => p.ProductName == productName).SingleOrDefault();
if (product == null)
return Helpers.GetNotFoundHttpHandler();
else
{
// Store the Product object in the Items collection
HttpContext.Current.Items["Product"] = product;
return BuildManager.CreateInstanceFromVirtualPath("~/ViewProduct.aspx", typeof(Page)) as Page;
}
}
}
}
The first line of code in the GetHttpHandler picks out the value of the ProductName parameter from the URL by using the RequestContext
object's RouteData property. For example, if the visitor requested /Products/Chai then the ProductName parameter will equal "Chai".
If the ProductName parameter is null or an empty string then the HTTP Handler for a particular web page (~/NotFound.aspx) is returned.
If the ProductName parameter is set then the database is queried to get information about said product. (I used the LINQ-to-SQL tool for data access
in this demo application.) Should no matching product be found, the NotFound.aspx page is returned. Otherwise, the product information is stored in the
HttpContext.Items collection and an instance of the ~/ViewProduct.aspx page is returned. Note that the syntax for returning an HTTP Handler instance
of an ASP.NET page is BuildManager.CreateInstanceFromVirtualPath(virtualPathToWebPage, typeof(Page)) as Page. The virtualPathToWebPage part cannot
include a querystring.
The CategoryRouteHandler class is similar to ProductRouteHandler. The key difference is that it uses either the AllCategories.aspx page
or CategoryProducts.aspx page for rendering, depending on whether the URL was /Categories/All or /Categories/CategoryName.
Step 5: Create the ASP.NET Pages That Process the Requests
At this point all of the routing configuration is complete. All that remains is to create the ASP.NET pages to process the various routes (ViewProduct.aspx,
CategoryProducts.aspx, and AllCategories.aspx). For the demo application these pages are pretty simple - they use a data source control that is
programmatically bound to the corresponding Category or Product object in the HttpContext.Items collection. For example, the
ViewProduct.aspx page contains a DetailsView control with fields defined to display the product's name, supplier, quantity per unit, price, and other pertinent
information. The page's code-behind class has the following (abbreviated) code:
protected Product Product
{
get
{
return HttpContext.Current.Items["Product"] as Product;
}
}
The Product property returns the Product object stored in the HttpContext.Items collection. This object is then bound to the DetailsView
control (dvProductInfo) in the Page_Load event handler. One oddity which may have caught your eye is the first line in Page_Load.
The data source controls like the DetailsView can only be bound to a collection of objects. Therefore, we cannot bind the Product object directly to the DetailsView, but
instead must create an array of Product objects that contains our sole Product of interest and then bind that array to the DetailsView.
The Demo Application In Action
The screen shots below show the demo application in action. The first screen shot shows the website when visiting /Categories/All. Note that the page shows all
of the categories on the page with a link to view the products for the category (such as /Categories/Beverages). These links can be built up manually by generating
a URL in the form /Categories/CategoryName for each category. A better approach is to have the URL generated for you by the ASP.NET Routing system based on
the route and the URL parameter values. To use this latter approach check out the RouteCollection object's
GetVirtualPath method.
(See the Helpers class in the demo application for code snippets of this method in action.)
Clicking on a category takes you to the "hackable" URL /Categories/CategoryName, which lists the products for the category with each product name as a link
to /Products/ProductName. The following screen shot shows the /Categories/Dairy Products page.
Finally, clicking on a product takes you to the corresponding product page (/Products/ProductName). The screen shot below shows the product page for
Queso Cabrales (/Products/Queso Cabrales).
Conclusion
The ASP.NET Routing system introduced in the .NET Framework SP1 makes truly "hackable" URLs in ASP.NET applications a real possibility and without the baggage and pain points
accompanying traditional URL rewriting techniques. While ASP.NET Routing is closely associated with ASP.NET MVC applications, the Routing framework can be used in Web Form
applications as well.