In chapter 16, you learned all about routing, so you probably already understand that routing is a complex and important topic. What happens when routing doesn’t behave the way we expect?
In this chapter, we’ll extend the routing system to provide diagnostic information about which routes are being matched for a given web request.
The UrlRoutingModule is an implementation of IHttpModule and represents the entry point into the ASP.NET MVC Framework. This module examines each request, builds up the RouteData for the request, finds an appropriate IRouteHandler for the given route matched, and finally redirects the request to the IRouteHandler’s IHttpHandler.
In any ASP.NET MVC application, the default route looks like the one in listing 24.1. The MapRoute method is a simplified way of specifying routes.
routes.MapRoute("default", "{controller}/{action}/{id}",
new { Controller="home", Action="index",
id=UrlParameter.Optional});
Most of the applications you’ll work with will use this style of adding routes. There’s also a more verbose method, which allows us to customize the classes that are used as part of the route. Listing 24.2 shows the same route but without using the MapRoute helper method.
That third argument in listing 24.2 tells the framework which IRouteHandler to use for this route. We’re using the built-in MvcRouteHandler that ships with the framework. This class is used by default when we call the MapRoute method, but we can change this to a custom route handler and take control in interesting ways.
An IRouteHandler is responsible for creating an appropriate IHttpHandler to handle the request, given the details of the request. This is a good place to change the way routing works, or perhaps to gain control extremely early in the request pipeline. The MvcRouteHandler simply constructs an MvcHandler to handle a request, passing it a RequestContext, which contains the RouteData and an HttpContextBase.
A quick example will help illustrate the need for a custom route handler. When defining our routes, we’ll sometimes run across errors. Let’s assume we’ve defined the route shown in listing 24.3.
routes.MapRoute("CategoryRoute", "{category}/{action}",
new { Controller = "Products", Action="index" });
Here we’ve added a new custom route at the top position that will accept URLs like /apparel/index, use the ProductsController, and call the Index action on it, passing in the category as a parameter to the action, as shown in listing 24.4. Listing 24.4 is a good example of a custom route that makes our URLs more readable.
public class ProductsController : Controller
{
public ActionResult Index(string category)
{
return View();
}
}
Now, let’s assume that we have another controller, HomeController, which has an Index action to show the start page, as shown in listing 24.5.
public class HomeController : Controller
{
public ActionResult Index()
{
return View();
}
}
We’d like the URL for the action in listing 24.4 to look like /home/index; but if we try this URL, we’ll get a 404 error, as shown in figure 24.1. Why?
The problem isn’t apparent from that error message. We certainly have a controller called HomeController, and it has an action method called Index. If we dig deep into the routes, we can deduce that this URL was picked up by the first route, {category}/{action}, which wasn’t what we intended. We should be able to quickly identify a routing mismatch so that we can fix it speedily.
With many custom routes, it’s easy for a URL to be caught by the wrong route. It’d be nice if we had a diagnostic tool to display which routes are being matched (and used) so we could quickly catch these types of errors.
To see the route rules as they’re matched at runtime, we can add a special query string parameter that we can tack onto the end of the URL. This will signify that instead of rendering the regular view, our custom route debugger should instead circumvent the request and provide a simple HTML view of the route information.
The current route information is stored in an object called RouteData, available to us in the IRouteHandler interface. The route handler is also the first to get control of the request, so it’s a great place to intercept and alter the behavior for any route, as shown in listing 24.6.
A route handler’s normal responsibility is to construct and hand off the IHttpHandler that will handle this request. By default, this is MvcHandler. In our CustomRouteHandler, we first check to see if the query string parameter is present ; we do this with a simple regular expression on the URL query section. If the query string contains a routeInfo parameter, the OutputRouteDiagnostics method is called, which will display diagnostic information to the user.
The OutputRouteDiagnostics method is shown in listing 24.7.
This method outputs two tables: one for the current route data, and one for the routes in the system. Each route will return null for GetRouteData if the route doesn’t match the current request. The table is then colored to show which routes matched, and a little arrow indicates which route is in use for the current URL. The response is ended to prevent any further rendering.
To make use of the new CustomRouteHandler, we have to alter the current routes, as shown in listing 24.8.
private static RouteBase CreateRoute(string url, object defaults)
{
return new Route(url, new RouteValueDictionary(defaults),
new CustomRouteHandler());
}
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.Add(CreateRoute("{category}/{action}", new {
controller = "products",
action = "index"}));
routes.Add(CreateRoute("{controller}/{action}/{id}", new {
controller = "home",
action = "index",
id=UrlParameter.Optional}));
}
Here we’re simply creating routes as we did before, but this time we’re setting them up with our new CustomRouteHandler class. A helper method is used to avoid too much code duplication and to allow an experience similar to the MapRoute method we used previously.
The end result is incredibly helpful. It shows us all the routes that are defined, color-coded by whether or not they match the current request. Let’s use the /home/ index URL that resulted in a 404 in figure 24.1, but this time we’ll add “?routeinfo” to the query string (shown in figure 24.2). We can see in the route data table that the value home was picked up as a product category. The route table confirms that the category route was picked up first, because it matched.
Now, we can immediately tell that the current route used isn’t the one we intended. We can also tell whether other routes match this request by the color of the cells. (If you’re reading the print version of this book, this might not be apparent; but if you run the sample application, you’ll see that rows 2 and 3 are green.)
We can quickly identify the issue as a routing problem and fix it accordingly. In this case, if we add constraints to the first route such that {category} isn’t the same as one of our controllers, the problem is resolved.
Remember that order matters! 09oThe first route matched is the one used.
We wouldn’t want this information to be visible in a deployed application, so we use it only to aid our development. We could also build a switch that changes the routes to the CustomRouteHandler if we’re in debug mode, which would be a more automated solution. Listing 24.9 shows a simple way of accomplishing this using preprocessor directives.
private static RouteBase CreateRoute(string url, object defaults)
{
IRouteHandler routeHandler = new MvcRouteHandler();
#if DEBUG
routeHandler = new CustomRouteHandler();
#endif
return new Route(url, new RouteValueDictionary(defaults), routeHandler);
}
In this example, we’re modifying our helper method to change out the IRouteHandler implementation to the standard one if the code is built in release mode.
This example was inspired by the route debugger Phil Haack posted on his blog, Haacked, for an early preview of the ASP.NET MVC Framework. It’s a great example of what you can do with the information provided by the routing system. His original “ASP.NET Routing Debugger” blog entry is here: http://mng.bz/7P2N.
Routing is a complex topic, and a small mistake can mean that an entire site is inaccessible. By using this technique of extending via the IRouteHandler interface, we can customize the routing system and leverage it to create a nice route debugger. Working with this tool is a great way to understand how our routes are being matched and also which route is being used for the current request.
In the next chapter, we’ll learn how to customize Visual Studio to take advantage of some advanced features of ASP.NET MVC.