ASP.NET MVC Tip #29 – Build a Controller to Debug Your Custom Routes

In this tip, I demonstrate how you can create a special controller that you can use to test your custom routes. I also explain how you can give your routes back their names so you can more effectively unit test your routes.

In this tip, I demonstrate how you can create a custom controller that you can use to debug the custom routes that you add to your ASP.NET MVC applications (see Figure 1). I also explain how you can create unit tests that test your custom routes by name.

Figure 1 – Using the RouteDebugger Controller

image

I was inspired to undertake this project by Phil Haack’s ASP.NET Routing Debugger:

http://haacked.com/archive/2008/03/13/url-routing-debugger.aspx

However, there are some important differences between the Route Debugger that I describe in this tip and Phil’s Haack’s Route Debugger. First, I wanted to be able to test my custom routes by name. For example, if you create two new routes named MyCustomRoute1 and MyCustomRoute2, I wanted my Route Debugger to be able to tell me which of these two routes were actually called.

Second, I wanted to be able to test my custom routes when performing different types of HTTP operations. For example, I wanted to test which route is called when you perform a GET operation versus a POST operation. And, potentially, I want to be able to test routes with other types of changes in the HttpContext.

Finally, I wanted to implement my Route Debugger as a controller that I could add to any ASP.NET MVC application simply by adding a reference to the RouteDebugger assembly. After you add a reference to the RouteDebugger assembly, you can invoke the RouteDebugger with the following URL:

/RouteDebugger

 

Give Your Routes Back Their Names

One challenge that you quickly encounter when using URL Routing concerns route names. When you create a new route, you can supply the new route with a name. Unfortunately, however, there is no way to get a route name back again. The Route class itself does not have a Name property. Furthermore, the RouteCollection class does not enable you to retrieve a list of route names.

Because you cannot get route names back from the Route or RouteCollection classes, you cannot easily debug or test routes by name. I want my Route Debugger to be able to tell me which routes are used by name. Furthermore, I want to create unit tests that test whether a particular route with a particular name was used. Therefore, before we do anything else, we need to give our routes their names back.

The NamedRoute class in Listing 1 inherits from the Route class. The NamedRoute class simply adds a new Name property to the base Route class.

Listing 1 – NamedRoute.cs

using System.Web.Routing;

namespace RouteDebugger
{
    public class NamedRoute : Route
    {
        private string _name;

        public NamedRoute(string name, string url, IRouteHandler routeHandler):base(url, routeHandler)
        {
            _name = name;
        }

        public NamedRoute(string name, string url, RouteValueDictionary defaults, RouteValueDictionary constraints, IRouteHandler routeHandler)
            : base(url, defaults, constraints, routeHandler)
        {
            _name = name;
        }

        public NamedRoute(string name, string url, RouteValueDictionary defaults, RouteValueDictionary constraints, RouteValueDictionary dataTokens, IRouteHandler routeHandler)
            : base(url, defaults, constraints, dataTokens, routeHandler)
        {
            _name = name;
        }

        public string Name
        {
            get { return _name; }
        }

    }

}

You don’t need to make any changes to your existing route tables in your Global.asax file to use the NamedRoute class instead of the Route class. The RouteDebugger project includes a new set of RouteCollection extension methods that sneakily replace the RouteCollection extension methods included with the ASP.NET MVC framework. See Listing 2.

Listing 2 – RouteCollectionExtensions.cs

using System.Web.Routing;
using System.Web.Mvc;

public static class RouteCollectionExtensions
{
    public static void IgnoreRoute(this RouteCollection routes, string url)
    {
        routes.IgnoreRoute(string.Empty, url, null);
    }

    public static void IgnoreRoute(this RouteCollection routes, string name, string url)
    {
        routes.IgnoreRoute(name, url, null);
    }

    public static void IgnoreRoute(this RouteCollection routes, string name, string url, object constraints)
    {
        var newRoute = new RouteDebugger.NamedRoute(name, url, new StopRoutingHandler());
        routes.Add(name, newRoute);
    }

    public static void MapRoute(this RouteCollection routes, string name, string url)
    {
        routes.MapRoute(name, url, null, null);
    }

    public static void MapRoute(this RouteCollection routes, string name, string url, object defaults)
    {
        routes.MapRoute(name, url, defaults, null);
    }

    public static void MapRoute(this RouteCollection routes, string name, string url, object defaults, object constraints)
    {
        var newRoute = new RouteDebugger.NamedRoute(name, url, new MvcRouteHandler());
        newRoute.Defaults = new RouteValueDictionary(defaults);
        newRoute.Constraints = new RouteValueDictionary(constraints);
        routes.Add(name, newRoute);
    }
}

The RouteDebugger project (which you can at the end of this tip) includes both the NamedRoute and RouteCollectionExtension classes. If you add a reference to the RouteDebugger assembly to an ASP.NET MVC project, then the routes in the project will be converted to instances of the NamedRoute class automatically.

Create the Route Debugger Controller

Now that we have given our routes their names again, we can create the RouteDebugger controller class. This controller class is contained in Listing 3.

Listing 3 – RouteDebuggerController.cs

using System;
using System.Collections.Generic;
using System.Text;
using System.Web.Mvc;
using System.Web.Routing;
using MvcFakes;

namespace RouteDebugger.Controllers
{
    public class RouteDebuggerController : Controller
    {
        public string Index(string url, string httpMethod)
        {

            url = MakeAppRelative(url);
            httpMethod = httpMethod ?? "GET";

            var fakeContext = new FakeHttpContext(url, httpMethod);
            var httpMethodOptions = formatOptions(httpMethod, new string[]{"GET","POST","PUT","DELETE","HEAD"} );
            var routeDataText = GetRoutesText(fakeContext);
            return string.Format(htmlFormat, url, httpMethodOptions, routeDataText);
        }

        private string GetRoutesText(FakeHttpContext fakeContext)
        {
            var sb = new StringBuilder();
            foreach (NamedRoute route in RouteTable.Routes)
            {
                var rd = route.GetRouteData(fakeContext);
                // Get match
                var isMatch = false;
                var match = rd == null ? "No Match" : "Match";

                // Get values
                var values = "N/A";
                if (rd != null)
                {
                    isMatch = true;
                    values = formatValues(rd.Values);
                }

                // Get defaults
                var defaults = formatValues(route.Defaults);

                // Get constraints
                var constraints = formatValues(route.Constraints);

                // Get dataTokens
                var dataTokens = formatValues(route.DataTokens);

                // Create table row
                var row = formatRow(isMatch, match, route.Name, route.Url, defaults, constraints, dataTokens, values);
                sb.Append(row);
            }
            return sb.ToString();
        }

        private string formatValues(RouteValueDictionary values)
        {
            if (values == null)
                return "N/A";
            var col = new List<String>();
            foreach (string key in values.Keys)
            {
                object value = values[key] ?? "[null]";
                col.Add(key + "=" + value.ToString());
            }
            return String.Join(", ", col.ToArray());
        }

        private string formatOptions(string selected, string[] values)
        {
            var sb = new StringBuilder();
            foreach (string value in values)
            {
                var showSelected = String.Empty;
                if (value == selected)
                    showSelected = "selected='selected'";
                sb.AppendFormat("<option value='{0}' {1}>{0}</option>", value, showSelected);
            }
            return sb.ToString();
        }

        private string formatRow(bool hilite, params string[] cells)
        {
            var sb = new StringBuilder();
            sb.Append(hilite ? "<tr class='hilite'>":"<tr>");
            foreach (string cell in cells)
                sb.AppendFormat("<td>{0}</td>", cell);
            sb.Append("</tr>");
            return sb.ToString();
        }

        private string MakeAppRelative(string url)
        {
            if (!url.StartsWith("~"))
            {
                if (!url.StartsWith("/"))
                    url = "~/" + url;
                else
                    url = "~" + url;
            }
            return url;
        }

        const string htmlFormat = @"
            <html>
            <head>
                <title>Route Debugger</title>
                <style type='text/css'>
                table {{ border-collapse:collapse }}
                td
                {{
                    font:10pt Arial;
                    border: solid 1px black;
                    padding:3px;
                }}
                .hilite {{background-color:lightgreen}}
                </style>
            </head>
            <body>

            <form action=''>
            <label for='url'>URL:</label>
            <input name='url' size='60' value='{0}' />
            <select name='httpMethod'>
            {1}
            </select>
            <input type='submit' value='Debug' />
            </form>

            <table>
            <caption>Routes</caption>
            <tr>
                <th>Matches</th>
                <th>Name</th>
                <th>Url</th>
                <th>Defaults</th>
                <th>Constraints</th>
                <th>DataTokens</th>
                <th>Values</th>
            </tr>
            {2}
            </table>

            </body>
            </html>

            ";

    }
}

There are four special things about the RouteDebugger controller. First, it takes advantage of a class named the FakeHttpContext class from a project called MvcFakes. I’ve used the MvcFakes project in a number of my previous tips to fake such ASP.NET MVC instrincis as the HttpContext, ControllerContext, and ViewContext.

Second, notice that the RouteDebugger controller takes advantage of the NamedRoute class that I described in the previous section. The RouteTable.Routes collection represents a collection of NamedRoutes rather than Routes.

Third, notice that the RouteDebugger enables you to pick an HTTP method when testing a route (see Figure 2). For example, you can debug the routes that are called when performing a GET versus a POST operation.

Figure 2 — Selecting an HTTP Method

image

Finally, notice that the RouteDebugger does not depend on an external view. The Index() method simply returns one gigantic string. That way, you don’t need to add a special view to an existing ASP.NET MVC application to use the RouteDebugger controller. An additional benefit is that the Route Debugger is View Engine agnostic. I got this idea from looking at the code for Phil Haack’s RouteDebugger.

After you add a referecne to the RouteDebugger assembly, you can invoke the RouteDebugger by entering the following URL into your browser:

/RouteDebugger

(Of course, this will only work if your MVC application includes the Default route or a custom route that points at the RouteDebugger).

If you enter a URL into the URL input field and hit the Debug button, the RouteDebugger will display a list of all routes configured in the current application. The RouteDebugger indicates the routes that the URL (and HTTP Method) matches. The first route matched will be the route that is actually used.

Unit Testing Routes by Name

Now that we have given our custom routes their names back, we can build unit tests that test routes by name. For example, the unit test class in Listing 4 contains two unit tests.

Listing 4 – RouteTest.cs

using System.Web.Routing;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MvcFakes;
using RouteDebugger;
using Tip29;

namespace Tip29Tests.Routes
{
    /// <summary>
    /// Summary description for RouteTest
    /// </summary>
    [TestClass]
    public class RouteTest
    {
        [TestMethod]
        public void TestInsertRoutePOST()
        {
            // Arrange
            var routes = new RouteCollection();
            GlobalApplication.RegisterRoutes(routes);

            // Act
            var fakeContext = new FakeHttpContext("~/Movie/Insert", "POST");
            var routeData = routes.GetRouteData(fakeContext);

            // Assert
            NamedRoute matchedRoute = (NamedRoute)routeData.Route;
            Assert.AreEqual("Insert", matchedRoute.Name);
        }

        [TestMethod]
        public void TestInsertRouteGET()
        {
            // Arrange
            var routes = new RouteCollection();
            GlobalApplication.RegisterRoutes(routes);

            // Act
            var fakeContext = new FakeHttpContext("~/Movie/Insert", "GET");
            var routeData = routes.GetRouteData(fakeContext);

            // Assert
            NamedRoute matchedRoute = (NamedRoute)routeData.Route;
            Assert.AreNotEqual("Insert", matchedRoute.Name);
        }

    }
}

The first unit test, named TestInsertRoutePost, tests a custom route named Insert when performing a POST operation. It verifies that the Insert route is the route called when you post to the URL ~/Movie/insert.

The second unit test, named TestInsertRouteGET, verifies that the Insert route is not called when a GET operation is performed with the same URL ~/Movie/Insert. Together, these unit tests verifiy that the Insert route can be called only during a POST operation.

Summary

In this tip, I’ve demonstrated how to create a Route Debugger that you can use to debug the routes in any of your ASP.NET MVC applications. Just add a reference to the RouteDebugger.dll assembly included with the code download. I also explained how to magically replace the anonymous routes in a route table with named routes. Finally, I provided sample unit tests for testing your custom routes by name.

Download the Code

Discussion

  1. http:// says:

    Like your tips on MVC but not so liking the little code boxes. Have you actually tried to use your own tutorials? Scrolling up and down and side to side to read the code is a major PITA. Real estate on the web is free so why worry about the room? If anything use collapsible boxes or click to view all code widgets.

  2. http:// says:

    @Zunama — I switched the plug-in that I use to display code — is this better?

  3. Elijah Manor says:

    I love this blog post! I have an issue with my route and this will help me track down the issue. By the way, I like the new display code plug-in. I think I’ll switch to that one too!

  4. http:// says:

    The MvcFakesFakeControllerContext.cs needs to be modified to work with the MVC beta. The controller now needs to be of type ControllerBase, not IController.

    Otherwise, you will get this error:

    cannot convert from ‘System.Web.Mvc.IController’ to ‘System.Web.Mvc.ControllerBase

  5. 32WE prefer to use what is expert said….so thanks a lot stephen.

  6. FR i prefer to use what is expert said….so thanks a lot stephen.

  7. Kimball Johnson says:

    Hi, could you give me an example of how you suggest I set up the route to the RouteDebugger, please?

    I keep getting an error that the url is a null object when I do F5 in visual studio.

    Thanks

  8. rf23 I tried to mock a call to a Linq to SQL query, but I am struggling.

  9. fast degrees says:

    How can you cache data in MVC? e.g If a cached version of a returned object through say LINQ to SQL is current I want to use that and not have to query the database again. In traditional ASp.NET apps (webforms) we can use the page’s cache properties to store data. I can’t see where this would be done within MVC, I assume a cache at the controller level is needed?.

  10. NikeAir says:

    Nice article, very helpful. Thanks!—
    Nike ,Jodon ,kappa shoes are selling on the way..Come on
    …………….Nike shoes ! Addidas ! crazy buying ————————- Nike Shoes || air yeezy

  11. ggg4 It looks like DataContextExtensions.cs line 45 of the Save method should pass the primaryKeyName through to Update.

  12. Nathanial says:

    The controller now needs to be of type ControllerBase, not IController.

    Otherwise, you will get this error:

    swingers clubs toronto |web design toronto |cheap toronto web hosting

  13. Very interesting post..Iam very much enjoyed by reading your site..this article has provided a useful info..Thanks for the info given..

  14. HD Video Converter says:

    As the users of HD Camcorders like Sony, Canon, Panasonic, this HD Converter is necessary to help us convert hd Video easily and quickly. The Converter for HD provides several practical editing functions to help you achieve ideal output effect. Trim function is to cut videos into clips which you can just convert and transfer to your player. Crop function helps you remove black bars around the movie. You could use Effect function to adjust video brightness, contrast, saturation and more parameters. More powerful and considerate functions are waiting for you to explore.MKV Converter l FLV Converter l DVD Ripper !…