When you think ASP, think...
Recent Articles
All Articles
ASP.NET Articles
ASPFAQs.com
Message Board
Related Web Technologies
User Tips!
Coding Tips
Search

Sections:
Book Reviews
Sample Chapters
Commonly Asked Message Board Questions
JavaScript Tutorials
MSDN Communities Hub
Official Docs
Security
Stump the SQL Guru!
Web Hosts
XML
Information:
Advertise
Feedback
Author an Article
Jobs

ASP ASP.NET ASP FAQs Message Board Feedback ASP Jobs
 
Print this Page!
Published: Wednesday, May 13, 2009

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!

- continued -

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):

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.

In Routing with ASP.NET Web Forms, author K. Scott Allen highlights the pitfalls of URL rewriting:

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).

<configuration>
   ...

   <system.web>
      ...
      <httpModules>
         ...
         <add name="UrlRoutingModule" type="System.Web.Routing.UrlRoutingModule, System.Web.Routing, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
      </httpModules>

   </system.web>

   <system.webServer>
      <validation validateIntegratedModeConfiguration="false"/>
      <modules>
         ...
         <add name="UrlRoutingModule" type="System.Web.Routing.UrlRoutingModule, System.Web.Routing, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
      </modules>


      <handlers>
         ...
         <add name="UrlRoutingHandler" preCondition="integratedMode" verb="*" path="UrlRouting.axd" type="System.Web.HttpForbiddenHandler, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
      </handlers>
      ...
   </system.webServer>

</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
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 Application_Start(object sender, EventArgs e)
{
   RegisterRoutes(RouteTable.Routes);
}

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:

  1. Parse the URL as needed
  2. 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.)
  3. 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 void Page_Load(object sender, EventArgs e)
{
   dvProductInfo.DataSource = new Product[] { Product };
   dvProductInfo.DataBind();
}

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.)

The /Categories/All page lists all categories.

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.

The /Categories/Dairy Products page lists the dairy products for sale.

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).

The /Products/Queso Cabrales page lists information about 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!

  • By Scott Mitchell


    Attachments:


  • Download the Demo Code Used in this Article

    Further Reading:


  • Using ASP.NET Routing Independent of MVC
  • ASP.NET Routing (technical documentation)
  • Routing with ASP.NET Web Forms
  • ASP.NET Routing Debugger
  • Dissecting ASP.NET Routing


  • ASP.NET [1.x] [2.0] | ASPMessageboard.com | ASPFAQs.com | Advertise | Feedback | Author an Article