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 5, 2010

Dissecting ASP.NET Routing

By Scott Mitchell


Introduction


The ASP.NET Routing framework allows developers to decouple the URL of a resource from the physical file on the web server. Specifically, the developer defines routing rules, which map URL patterns to a class or ASP.NET page that generates the content. For instance, you could create a URL pattern of the form Categories/CategoryName and map it to the ASP.NET page ShowCategoryDetails.aspx; the ShowCategoryDetails.aspx page would display details about the category CategoryName. With such a mapping, users could view category about the Beverages category by visiting www.yoursite.com/Categories/Beverages. In short, ASP.NET Routing allows for readable, SEO-friendly URLs.

ASP.NET Routing was first introduced in ASP.NET 3.5 SP1 and was enhanced further in ASP.NET 4.0. ASP.NET Routing is a key component of ASP.NET MVC, but can also be used with Web Forms. Two previous articles here on 4Guys showed how to get started using ASP.NET Routing: Using ASP.NET Routing Without ASP.NET MVC and URL Routing in ASP.NET 4.0.

This article aims to explore ASP.NET Routing in greater depth. We'll explore how ASP.NET Routing works underneath the covers to decode a URL pattern and hand it off the the appropriate class or ASP.NET page. Read on to learn more!

- continued -

How Routing Rules Are Processed


Using the ASP.NET Routing framework entails specifying one (or more) routing rules, which map URL patterns to an endpoint that is responsible for generating the content. These routing rules are stored in the RouteTable class's static Routes property and are typically defined in the Global.asax file's Application_Start event.

At its core, each routing rule is implemented as an object of type Route. In its simplest form, the Route object specifies two bits of information:

  • The URL pattern, and
  • The route handler responsible for processing the request
The URL pattern is a string that may contain both literal portions and URL parameters. For example, the URL pattern Categories/{CategoryName} has a literal portion - Categories - and a URL parameter, {CategoryName}. This URL pattern will match incoming URLs like www.yoursite.com/Categories/Beverages and www.yoursite.com/Categories/Desserts.

The route handler is an object that implements the IRouteHandler interface. This interface defines a single method, GetHttpHandler, which is responsible for returning the HTTP Handler to use to generate the response. An HTTP Handler is a class that implements the IHttpHandler interface. HTTP Handlers know how to process an ASP.NET request and generate content in response. All ASP.NET Web Form pages are HTTP Handlers; you can also create your own HTTP Handler classes from scratch, if needed. For more information on HTTP Handlers, see HTTP Handlers and HTTP Modules in ASP.NET.

An End-To-End Example: Creating a Simple Routing Rule, Route Handler, and HTTP Handler


Let's walk through the process of defining a routing rule and creating a route handler and HTTP Handler to process the request. Specifically, let's create the ability for someone to visit our website using any URL in the form echo/message. Doing so will simply return the value of message. In other words, if a user were to enter the URL www.yoursite.com/echo/Scott into their browser, they'd see the text "Scott" appear in the browser window; if they visited www.yoursite.com/echo/Jisun, they'd see the text "Jisun".

To start, we need to define our URL pattern. Create the Global.asax file (if it does not exist already) and in the Application_Start event handler add the following code:

void Application_Start(object sender, EventArgs e)
{
   // Register routes...
   Route echoRoute = new Route("echo/{message}", new EchoRouteHandler());
   RouteTable.Routes.Add(echoRoute);

}

(Note: The Route and RouteTable classes live in the System.Web.Routing namespace, so you'll either need to import that namespace or give the fully-qualified type name when using these classes, such as System.Web.Routing.RouteTable.)

Here we start by creating a Route object named echoRoute. The Route class's constructor allows us to specify the URL pattern and route handler in one go, which we've used to specify the URL pattern as "echo/{message}" and the route handler as an instance of the EchoRouteHandler class. We now need to create this route handler class.

Recall that the sole purpose of a route handler is to return the HTTP Handler responsible for processing the request. If you are familiar with ASP.NET MVC then you know that the class used to process the request (the Controller) depends on the values of the URL pattern's parameter(s). For this example, however, we want to use the same HTTP Handler to process the request regardless of the value of its message parameter. Therefore, the code for this class is exceptionally simple:

public class EchoRouteHandler : IRouteHandler
{
   public IHttpHandler GetHttpHandler(RequestContext requestContext)
   {
      return new EchoHandler();
   }
}

Notice how the EchoRouteHandler class implements IRouteHandler. In this case, the GetHttpHandler method simply returns an instance of the EchoHandler HTTP Handler, which we'll examine momentarily. However, this method could be more intricate. GetHttpHandler is passed a RequestContext object, which contains a reference to the HttpContext (which houses the intrinsic objects, Request, Response, etc.) and the RouteData (which contains information about the route, including the values of the URL parameter(s).

The last piece of the puzzle is the EchoHandler HTTP Handler. This HTTP Handler's sole purpose is to return the value of the message URL parameter. The values of the route's URL parameter(s) can be read from the RequestContext object, which is accessible from the Request object. In particular, use the Values property to retrieve a value from a URL parameter. The following code does just this - it reads the message URL parameter value and echos it back out via the Response.Write method.

public class EchoHandler : IHttpHandler
{
   public bool IsReusable
   {
      get { return false; }
   }

   public void ProcessRequest(HttpContext context)
   {
      string messageToEcho = context.Request.RequestContext.RouteData.Values["message"] as string;

      context.Response.Write(messageToEcho);
   }

}

With these three pieces in place - the routing rule, the route handler, and the HTTP Handler - we are now ready to test this functionality. The following screen shot shows the output when visiting echo/ASP.NET. Note how the value of the message URL parameter - "ASP.NET" - is echoed back to the client.

The message is echoed back to the client.

This demo is available for download at the end of this article. I recommend downloading it and trying it out. Set breakpoints in the EchoRouteHandler and EchoHandler classes to verify that these classes are invoked whenever a request that matches the URL pattern comes in.

Specifying URL Parameter Default Values


As we just saw, the URL pattern for a routing rule can contain any number of URL parameters. In order for the routing engine to match an incoming URL with one of these URL patterns all of the parameter values must be specified. For example, if you download the demo at the end of this article, fire it up, and then try to visit /echo without specifying a value for the message parameter, ASP.NET responds with a 404 status as there are no matching routing rules nor is there a folder or ASP.NET page named echo.

When defining a routing rule you may optionally provide default values for the URL parameters. To accomplish this, you can pass a RouteValueDictionary object into the Route object's constructor that spells out the default values for each URL parameter. The following code updates the route to use a default value of "Hello, World!" for the message URL parameter.

void Application_Start(object sender, EventArgs e)
{
   // Register routes...
   Route echoRoute = new Route(
         "echo/{message}",
         new RouteValueDictionary() { { "message", "Hello, World!" } },
         new EchoRouteHandler()
      );

   RouteTable.Routes.Add(echoRoute);
}

The syntax used to specify the RouteValueDictionary object uses the collection initializer syntax added to C# 3.0. Refer to the Object and Collection Initializers section of the C# Programming Guide for more information.

With the updated routing rule in place, visiting /echo emits the text, "Hello, World!", as the following screen shot illustrates:

The default message - Hello, World! - is echoed back to the client.

Adding URL Parameter Constraints


In addition to specifying default values, it is also possible to define constraints on the URL parameter values. Such constraints can be specified in one of two ways:
  • As a regular expression pattern, in which case the routing framework only matches the URL pattern if the regular expression matches the URL parameter value, or
  • As an object that implements IRouteConstraint
Imagine that we want to only echo back messages that start with a letter and are at least five characters long. That is, if a URL of the form echo/45 or echo/545scott or echo/abc comes in, we do not want to have it handled by our routing rule. (In the first two cases we don't want to handle it because the message parameter value starts with a number; in the first and third cases because the message is too short.) To accomplish this we could use the regular expression [^\d].{4,}, which says, in English, "Match only if the first character is not a digit and then there are four or more characters of any type.

As with default values, the collection of constraints can be specified via a RouteValueDictionary object in the Route object's constructor. The following code adds this regular expression constraint on the message URL parameter.

void Application_Start(object sender, EventArgs e)
{
   // Register routes...
   Route echoRoute = new Route(
         "echo/{message}",
         new RouteValueDictionary() { { "message", "Hello, World!" } },
         new RouteValueDictionary() { { "message", @"[^\d].{4,}" } },
         new EchoRouteHandler()
      );

   RouteTable.Routes.Add(echoRoute);
}

With this in place, visiting echo/abc or echo/45 will result in a 404 status, as the route is not matched and there are no pages or folders named abc or 45.

If the logic for the URL parameter constraints is too complex to be handled by a regular expression, you can create a class to perform the constraint check. This class must implement the IRouteConstraint interface, which defines an IsMatch method that returns a Boolean value indicating if the constraint has been met. This method is called by the routing engine when analyzing an incoming URL.

Let's create such a class for our echo route. Imagine that there are certain naughty words that we want to not echo back to the client, such as PHP and JSP. This list of offensive words might live in a database, although in my example they're hard-coded in the class. The following class, EchoConstraint implements IRouteConstraint; in its Match method it checks to see if the value of the message parameter is one of the invalid messages. If it is not one of the invalid messages then the constraint check passes, so its returns true; however, if the message is one of the invalid ones then the Match method returns false, which means the echo route will not be used and a 404 status will result.

public class EchoConstraint : IRouteConstraint
{
   public readonly string[] InvalidMessages = { "PHP", "JSP", "Ruby" };

   public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
   {
      string message = values["message"] as string;

      return !InvalidMessages.Contains(message);
   }
}

Next, we need to have the echo route use this EchoConstraint class (instead of the regular expression defined earlier). This is as simple as replacing this regular expression string with an instance of this class, as the following code shows:

void Application_Start(object sender, EventArgs e)
{
   // Register routes...
   Route echoRoute = new Route(
         "echo/{message}",
         new RouteValueDictionary() { { "message", "Hello, World!" } },
         new RouteValueDictionary() { { "message", new EchoConstraint() } },
         new EchoRouteHandler()
      );

   RouteTable.Routes.Add(echoRoute);
}

Now a user attempting to echo something inappropriate, such as "Ruby", will get a 404.

The constraint does not match the echo route for certain messages, such as Ruby.

The MapRoute and MapPageRoute Extension Methods


If you are familiar with ASP.NET MVC or if you have used the ASP.NET Routing framework with Web Forms as discussed in URL Routing in ASP.NET 4.0, then the steps described in this article may seem like overkill. When using routing in ASP.NET MVC or with Web Forms you don't have to create a routing handler and an HTTP Handler. Instead, you only have to define the routing rule in Global.asax using either the RouteTable.MapRoute or RouteTable.MapPageRoute methods.

Understand that these two methods are actually extension methods that, underneath the covers, do the same series of steps discussed earlier in this article. For example, the MapPageRoute, which is used to map a URL pattern to a physical ASP.NET page, accepts as input the URL pattern and the ASP.NET page to invoke. The following code snippet shows how to call this method. Here, a routing rule named "Home Route" is created that maps any request to the URL "Home" to the ASP.NET page Default.aspx. (And while not shown in this example, it is certainly possible to have parameters in the URL pattern and to specify default values and constraints for those parameters via the MapPageRoute method.)

RouteTable.Routes.MapPageRoute(
     "Home Route",         // Route name
     "Home",               // Url pattern
     "~/Default.aspx"      // Physical file
);

Behind the scenes, calling the MapPageRoute method causes a new routing rule to be added to the RouteTable.Routes collection using the URL pattern you specified. The route uses a routing handler of type PageRouteHandler, which is defined in the routing framework. This class, as you can likely guess, creates an instance of the specified ASP.NET page (Default.aspx in the above example) and returns that to be used as the HTTP Handler for processing the request.

The MapRoute method used by ASP.NET MVC works in a similar manner.

Conclusion


ASP.NET Routing is a powerful and quite useful framework added to ASP.NET 3.5 SP1 that cleanly decouples the URL from the physical file, and is a key component of ASP.NET MVC. With ASP.NET Routing it's possible to craft readable, SEO-friendly URLs with minimal effort. Underneath the covers, the ASP.NET Routing system maps an incoming URL to a routing handler, which is responsible for returning the HTTP Handler to process the request. You can further tailor the URLs associated with a route by specifying default values and constraints for the URL's parameters.

Happy Programming!

  • By Scott Mitchell


    Attachments:


  • Download the code associated with this article
  • Further Readings:


  • ASP.NET Routing (technical docs)
  • URL Routing in ASP.NET 4.0
  • Using ASP.NET Routing Without ASP.NET MVC


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