Exploring System.Web.Routing

Most of you have probably been hearing a lot recently about the new ASP.NET MVC framework and the many features that it has which will hopefully simplify web development for those of us that want to get “closer to the browser”. One of the features that they were initially implementing for the MVC framework was a new routing engine that gives you a flexible way of mapping urls to specific pages in your application. Early on they realized that the System.Web.Routing infrastructure was not only applicable to ASP.NET MVC, but could be used in any ASP.NET application to allow for much easier url rewriting. (They also realized that they wanted to use it in the Dynamic Data stuff!) Because of this they moved Routing out of the System.Web.Mvc namespace and into the System.Web namespace. Well, this namespace just shipped as part of .net 3.5 SP1, so lets take a look at how it works!

The first thing that you need to do in order to use these features are to add a reference to the System.Web.Routing dll:

image

System.Web.Routing has two core concepts. One is the concept of a route and the second is the concept of a route handler. A route is simply a class that holds a pattern which can be matched by urls coming into your application. Each url coming into the application will be matched against the list of Routes that you have defined, and if one matches then it will be used. A route will look something like this:

"Catalog/{Category}/{ProductId}"

This will match any url coming into the application that starts with “/Catalog/” so something like “/Catalog/Computers/3444″ would match this route. The items between the curly braces are called segments, and these will be captured and used later in our route handler. These routes are defined on static property of System.Web.Routing.RouteTable called “Routes”. These routes will be defined in the “Application_Start” method of your global.asax file like this:

void Application_Start(object sender, EventArgs e)
{
    RegisterRoutes(RouteTable.Routes);
}

public static void RegisterRoutes(RouteCollection routes)
{
    routes.Add(new Route("Catalog/{Category}/{ProductId}", new CatalogRouteHandler()));
}

Now, you may have noticed the “CatalogRouteHandler” class that we are creating in the “RegisterRoutes” method. This is a class that we need to create in order to processes a request that comes in matching the provided route. You can have any number of these Route handlers to handle different types of request, but for right now we are just going to have one.

These “Route Handlers” implement the IRouteHandler interface which only has a single method called “GetHttpHandler” which, as you probably guessed, returns an IHttpHandler. So, what is IHttpHandler? It is not a new interface, and is actually part of the System.Web namespace. This interface also supports just one method called ProcessRequest which takes an HttpContext. In the context of a normal ASP.NET application, the class that you are going to see which implements this is the Page class. If you are an ASP.NET developer I hope that you are familiar with the Page class, if not, then get a book! So, we have an Interface which returns an IHttpHandler, which in our case, is a page object.

So, I hope you see where this is going. We are just matching a bunch of urls against our list of patterns, and then when we find a matching pattern we will use the associated IRouteHandler to get the IHttpHandler which is going to be able to respond to our request! It is actually a lot more simple than it sounds!

As we already said, in our case, the IRouteHandler is going to be returning a Page object. What page object? It doesn’t really matter. We can map any number of urls to any number of Page objects. We could map all of our urls to a single Page object if we wanted. So, now that we know that we can map to any number of pages that we want, how does our IRouteHandler decided which page to map to? What kind of data do we get that allows it to make a decision? This is where the System.Web.Routing.RequestContext class comes in. The “GetHttpHandler” method that I mentioned above returns an IHttpHandler, but it also takes a parameter of type RequestContext. The RequestContext class contains has two properties, one is called “HttpContext” which is of type System.Web.HttpContextBase. The other is “RouteData” which is of type System.Web.Routing.RouteData.

If you saw “System.Web.HttpContextBase” and did a double-take then you are probably not alone. This is another class that was added in .net 3.5 SP1 which is simply an abstract wrapper for our old untestable friend the HttpContext. Remember earlier when I was talking about the nice new testable design, well, this is a big part of it. Just keep in mind that this is part of System.Web.Abstractions and so you’ll need to add a reference:

image

The HttpContext property just allows access to all of the normal information that we would garner from our HttpContext, so that we could make potential routing decisions based on the request data itself. For example if we wanted to do something different if we were operating under https or http. Or if we wanted to redirect different places based on the domain, or sub-domain coming in on the request.

The “RouteData” property is where all of our data concerning our route and the segments are stored. First it has a property called “Route” which is of type RouteBase, and this represents the matching route that was picked for this particular request.

Next there is a property called “Values” which holds all of the values and default values for the segments that were specified in the route that we created. For example, the route that we created at the beginning of this post (“Catalog/{Category}/{ProductId}”) had “Category” and “ProductId” segments, so for the url that we had above (“/Catalog/Computers/3444″) the “Values” property would have values for each:

routeData.Values["Category"] == "Computers"

routeData.Values["ProductId"] == "3444"

So, now we could use these values to pass in our HttpContext.Items collection to our asp.net page! This would be accomplished by implementing that IRouteHandler that we talked about earlier. Here is a basic IRouteHandler for you to take a look at, take note that we have not implemented any security or anything in this.

public class CatalogRouteHandler : IRouteHandler
{
    public IHttpHandler GetHttpHandler(RequestContext requestContext)
    {            
        foreach (KeyValuePair<string, object> token in requestContext.RouteData.Values)
        {                
            requestContext.HttpContext.Items.Add(token.Key, token.Value);
        }            
        IHttpHandler result = BuildManager
            .CreateInstanceFromVirtualPath("~/Product.aspx", typeof(Product)) as IHttpHandler;
        return result;
    }
}

From here you can see that we are shoving our Category and ProductId into the “HttpContext.Items” collection.

Now that we have defined our routes, and we have defined our CatalogRouteHandler, lets look at a few more features of the routes. The first thing that we can do is setup defaults for our routes. So, lets say that we want to default the “ProductId” to 0001 when the “Catalog/{Category}/{ProductId}” route is used without the “ProductId” segment. Let us go ahead and default “Category” to “Default” when that is not passed in as well. This actually quite easy and is simply an overload of the Route constructor. To define the route above with a product default would look like this:

private static void RegisterRoutes(RouteCollection routes)
{
    routes.Add(new Route("Catalog/{Category}/{ProductId}",
        new RouteValueDictionary(new { Category = "Default", ProductId = "0001" }),
        new CatalogRouteHandler()));
}

As you can see we are creating a new RouteValueDictionary and we are initializing it using the constructor that just uses the properties off a given object to initialize it. Here we are using an anonymous type with Category and ProductId properties. This could also be done by just using a collection initializer and it would look like this:

private static void RegisterRoutes(RouteCollection routes)
{                                   
    routes.Add(new Route("Catalog/{Category}/{ProductId}",
        new RouteValueDictionary { {"Category", "Default"}, {"ProductId", "0001"} },  
        new CatalogRouteHandler()));            
}

Now, there is another part to adding a route that we need to discuss and that is route constraints. Route constraints actually take more than one form. First we have regular expressions. Let’s say that we want to limit the ProductId on the route above to a max of 4 numbers. Then we could implement the constraint like this:

routes.Add(new Route("Catalog/{Category}/{ProductId}",
    new RouteValueDictionary { {"Category", "Default"}, {"ProductId", "0001"} },
    new RouteValueDictionary { {"ProductId", @"\d{1,4}"} },
    new CatalogRouteHandler()));

Here you see that we have another RouteValueDictionary that has a single entry for “ProductId” which takes the regular expression “\d{1,4}”. Well, this regex limits us to 1-4 digits. So, if we enter a 5 digit product id, then this route will no longer be matched. You can check any value that you can validate using a regex. Constraints also take on a second form, which is they implement the IRouteConstraint interface. This interface has a single method called “Match” that passes all relevant info about the request to your class, and allows you to put custom logic for route constraints into your own class. Pretty powerful stuff. There is one already built in called HttpMethodConstraint and it allows you to limit your route to a particular http verb such as “get” or “post”. These classes are just inserted into the same RouteValueDictionary that we used above, and they key doesn’t matter. The routing infrastructure will reflect the key and see that it supports the IRouteConstraint interface and call the appropriate method. If we wanted to limit the above route to just “get”, it would look like this:

routes.Add(new Route("Catalog/{Category}/{ProductId}",
    new RouteValueDictionary { {"Category", "Default"}, {"ProductId", "0001"} },
    new RouteValueDictionary { {"ProductId", @"\d{1,4}"}, {"httpMethod", new HttpMethodConstraint("get")} },
    new CatalogRouteHandler()));

Here we are giving it a key of “httpMethod”, but you could really call it anything you want. The HttpMethodConstraint just takes a list of http verbs as a constructor and then checks the request for them when trying to match the route.

At this point I think that you have a pretty darn good overview of how the new System.Web.Routing namespace works, but there is just one more quick thing that I want to touch on. It is the StopRoutingHandler class. All this class does is cause the routing infrastructure to stop trying to route a particular request. This is good if you want to stop a particular extension from checking each and every route. Instead of allowing this, you can just put in routes at the top that will stop the process. So, if we wanted to stop looking for a route on an *.asmx request, we could add this to the top of our route definitions:

routes.Add(new Route("{service}.asmx/{*path}", new StopRoutingHandler()));

Here we are saying that any request which ends in “.asmx” and then has any path following it matches this route. Once this is matched, the route should stop being processed. Very useful if you have a huge number of routes.

So, at this point you may be reading this and thinking “I see how this all works, but how in the world does this actually process requests? Don’t I need to get some module in the request pipeline?” And that answer is of course, “yes”. There is a class called System.Web.Routing.UrlRoutingModule which is an IHttpModule which plugs into your request pipeline and intercepts the urls as they are coming through to allow the routing support to work. This is done using the exact same mechanism that most of us are using now in order to do url rewriting. Add this to the “modules” section in your web.config:

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

And then add this to the handlers section (if you are using IIS7):

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

Well, that about wraps this up, I hope that you now have a good idea of how or if you can leverage the new routing infrastructure in .net 3.5 SP1.

Be Sociable, Share!

26 comments

  1. Justin, this is a great article! I haven’t been able to find this detailed of a look at routing before — Thanks for taking the time to write this.

  2. @Ryan Thanks! I was having a hard time figuring out a lot of it. Most of the information is out there, just in small bits and pieces, not in one big chunk. So I chunked it!

  3. Justin – Great article! Fills in the gaps nicely :)

    Only thing I’d add is that you’ll probably need a StopRoutingHandler to prevent WebResource.axd requests (or similar) from hitting your handlers (mentioned at http://chriscavanagh.wordpress.com/2008/03/11/aspnet-routing-goodbye-url-rewriting/ but more detail at http://msdn.microsoft.com/en-us/library/system.web.routing.stoproutinghandler.aspx).

  4. Justin, excellent post. This will be hugely useful. I was just going to start attempting to dig into routing this weekend. Thanks.

    -Ryan

  5. I liked it Justin, I think I’m good at picking the posts that shows things I never tried.
    It will be useful for me so much. Thank you

  6. Hi,

    I’ve just tried this in my recent download of SP1 and it appears that the RouteCollection.Add() method now wants an object decended from the abstract base class RouteBase instead of an object that implements IRouteHandler. Am I missing something?
    Thanks,

    John

  7. yeah, we have an own CMS based on System.Web.Routing. good review.

  8. Hi, very interesting, thank you! I assume the last line for the web.config shouldn’t include ‘class="xml" name="code">’ because it won’t compile.
    I do have some problems getting this to work though. Is it supposed to work on the asp.net development server (I have installed .net 3.5 SP1)?

  9. @Peter Yep, somehow part of the containing tag for the syntax highlighting somehow got in the middle of the web.config tag.

    And no, this won’t work on the asp.net dev server. I’ll have to look at it a bit to try and get it working in the dev server.

  10. Very good article!
    Congratulations!

  11. great article! where does the BuildManager class come from in this line?

    IHttpHandler result = BuildManager .CreateInstanceFromVirtualPath("~/Product.aspx", typeof(Product)) as IHttpHandler;

    Am I missing using statement? regarads

  12. ….System.Web.Compilation….

  13. I’m using FormsAuthentication in my site, in pages arrived at via Routing any references to My.User (or the same information via the HTTP objects) cause errors

  14. @Dan What kind of errors are you seeing?

  15. Hi Justin,

    Sorry, should have given more info but have spent some time on this. I’m "Object Reference not set…" exceptions because the values of requestContext.HttpContext.User and requestContext.HttpContext.Session are null – I’m using VB so the errors occur when my pages reference My.User.IsAuthenticated.

    Thanks

  16. DO you know if IIS changes are needed to enable the route handlers if they are matching on non .aspx, .asmx routes?

    For example in your writeup, is anything needed to customize ASP.NET to handle the routing options since no mention of an asp.net extension exists?

  17. @Mitchel It depends on what version of IIS and what mode you are running under. In IIS 6 you need to do special setup, and in IIS 7 if you are running in classic mode then you need to do some special setup as well. Check out msdn http://msdn.microsoft.com/en-us/library/cc668202.aspx

  18. Is it possible to handle requests without an extension? e.g. /aboutus or /photos

    im using iis6 i dont want to create lots of dirs with default.aspx in them id like the app to handle the request and execute the correct page ( i had tried using custom 404 handler – but found that .net was handling every 404 on the server even missing img requests ( or will i have to live with this and handle it early on in the processing)

  19. You have to setup IIS to pass all requests through the asp.net runtime. There is a decent tutorial on this here: http://www.eggheadcafe.com/aspnet/how-to/365113/serve-extensionless-url-f.aspx

  20. Carlos Fonseca

    Hi Justin,

    Great Article. I have been trying to make this feature to work, but no luck. I have put break points in the routing handler, but they are never hit. I triple checked my code, the web.config, the global.asax, the routes, everything. Can this feature have a bug ?

    Best Regards,

    Carlos

  21. Carlos Fonseca

    Hi Justin,

    Found my error. It seems that on older versions of IIS other than 7.0 for Routing to work properly you have to map an extention to the aspnet_isapi.dll.

    To anyone o may concern here is the link http://www.asp.net/learn/mvc/tutorial-08-cs.aspx

    Thanks anyway.

    Best Regards,
    Carlos

  22. @Carlos Sorry I didn’t see your comment earlier, I’m glad you worked your issue out.

  23. I was not able to apply this feature because the name of my project was something like "myproject.ext" and then running the application "http://localhost:nnnnn/myproject.ext/route1/route2&quot; gave me http 404 error. I think this is an handler bug. Changing the application name in "myproject" solved the problem.
    Sorry for my bad english. I think this consideration could be helpful
    picce

  24. Great article.

Leave a comment