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!
ASP.NET Routing in ASP.NET 4... |
---|
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 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. |
The Case for ASP.NET Routing
In 1999 usability expert Jakob Neilsen defined six essential components to a "good" URL (emphasis mine):
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 likeThe 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.
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.
In Routing with ASP.NET Web Forms, author K. Scott Allen highlights the pitfalls of URL rewriting:
Those of you who have used theThe 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.RewritePath
API in the past are probably familiar with some of the quirks and weaknesses in the rewriting approach. The primary problem withRewritePath
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.
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
).
<configuration>
|
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
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 Application_Start(object sender, EventArgs e)
|
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. (SeeHttpContext.Items
- a Per-Request Cache Store for background on theItems
collection.) - Return an instance of the ASP.NET page or HTTP Handler that does the processing
using
statements have been omitted for brevity; refer to the download for the complete code for this class.)
public class ProductRouteHandler : IRouteHandler
|
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 void Page_Load(object sender, EventArgs e)
|
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.
Happy Programming!
Attachments:
Further Reading: