Chapter 24. Debugging routes – ASP.NET MVC 2 in Action

Chapter 24. Debugging routes

This chapter covers

  • Customizing the routing system
  • Inspecting route matches

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.

24.1. Extending the routing system

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.

Listing 24.1. MapRoute, used to specify 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.

Listing 24.2. A more detailed way of specifying routes

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.

Listing 24.3. Adding another route
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.

Listing 24.4. A controller action that handles the new route
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.

Listing 24.5. A controller action to respond to the default route
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?

Figure 24.1. This message doesn’t tell us much about what’s wrong. An action couldn’t be found on the controller, but which one?

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.

24.2. Inspecting routes at runtime

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.

Listing 24.6. A custom route handler that creates an associated IHttpHandler

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.

Listing 24.7. Rendering route diagnostic information to the response stream

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.

Listing 24.8. Assigning routes to our custom route handler
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.

Figure 24.2. Appending the query string parameter “?routeinfo” to our URL gives us detailed information about the current request’s route. We can see now that the wrong route was chosen.

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.

 

Warning

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.

Listing 24.9. Switching the IRouteHandler implementation for debug mode
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.

 

Note

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.

 

24.3. Summary

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.