Chapter 9 – Understanding Routing

This is a rough draft of a chapter from the book ASP.NET MVC Framework Unleashed by Stephen Walther. Comments are welcome and appreciated. When the book is published, the text from this blog entry will be removed and only the code listings will remain.

Order this Book from Amazon

When you request a page from an ASP.NET MVC application, the request gets routed to a particular controller. In this chapter, you learn how to use the ASP.NET Routing module to control how browser requests get mapped to controllers and controller actions.

In the first part of this chapter, you learn about the Default route that you get when you create a new ASP.NET MVC application. You learn about the standard parts of any route.

Next, you learn how to debug the routes in your ASP.NET MVC applications. You learn how to use the Route Debugger project included with this book to see how browser requests get mapped to particular routes.

We also discuss how you can create custom routes. You learn how to extract custom parameters from a URL. You also learn how to create custom constraints that restrict the URLs that match a particular route.

Finally, we tackle the important topic of testing your routes. You learn how to build unit tests for custom routes and route constraints.

Using the Default Route

You configure ASP.NET Routing in an application’s Global.asax file. This makes sense because the Global.asax file contains event handlers for application lifecycle events such as the application Start and application End events. Because you want your routes to be enabled when an application first starts, routing is set up in the application Start event.

When you create a new ASP.NET MVC application, you get the Global.asax file in Listing 1.

*** Begin Note ***

The default route defined in the Global.asax file only works with Internet Information Server 7.0 and the ASP.NET Development Web Server. If you need to deploy your ASP.NET MVC application to an older version of Internet Information Server, see Chapter 15, Deploying MVC Applications.

*** End Note ***

Listing 1 – Global.asax.cs (C#)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;

namespace MvcApplication1
{
    // Note: For instructions on enabling IIS6 or IIS7 classic mode,
    // visit http://go.microsoft.com/?LinkId=9394801

    public class MvcApplication : System.Web.HttpApplication
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

            routes.MapRoute(
                "Default",                                              // Route name
                "{controller}/{action}/{id}",                           // URL with parameters
                new { controller = "Home", action = "Index", id = "" }  // Parameter defaults
            );

        }

        protected void Application_Start()
        {
            RegisterRoutes(RouteTable.Routes);
        }
    }
}

Listing 1 – Global.asax.vb (VB)

' Note: For instructions on enabling IIS6 or IIS7 classic mode,
' visit http://go.microsoft.com/?LinkId=9394802

Public Class MvcApplication
    Inherits System.Web.HttpApplication

    Shared Sub RegisterRoutes(ByVal routes As RouteCollection)
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}")

        ' MapRoute takes the following parameters, in order:
        ' (1) Route name
        ' (2) URL with parameters
        ' (3) Parameter defaults
        routes.MapRoute( _
            "Default", _
            "{controller}/{action}/{id}", _
            New With {.controller = "Home", .action = "Index", .id = ""} _
        )

    End Sub

    Sub Application_Start()
        RegisterRoutes(RouteTable.Routes)
    End Sub
End Class

The Global.asax file in Listing 1 includes two methods named Application_Start() and RegisterRoutes(). The Application_Start() method is called once, and only once, when an ASP.NET application first starts. In Listing 1, the Application_Start() method simply calls the RegisterRoutes() method.

*** Begin Note ***

Why does the Global.asax file include a separate method called RegisterRoutes()? Why isn’t the code in the RegisterRoutes() method simply contained in the Application_Start() method?

A separate method was created to improve testability. You can call the RegisterRoutes() method from your unit tests without instantiating the HttpApplication class.

*** End Note ***

The RegisterRoutes() method is used to configure all of the routes in an application. The RegisterRoutes() method in Listing 1 configures the Default route with the following code:

[C#]

routes.MapRoute(
    "Default",                     // Route name
    "{controller}/{action}/{id}",  // URL with parameters
    new { controller = "Home", action = "Index", id = "" }  // Parameter defaults
);

[VB]

routes.MapRoute( _
   "Default", _
   "{controller}/{action}/{id}", _
   New With {.controller = "Home", .action = "Index", .id = ""} _
)

You configure a new route by calling the MapRoute() method. This method accepts the following parameters:

· Name – The name of the route

· URL – The URL pattern for the route

· Defaults – The default values of the route parameters

· Constraints – A set of constraints that restrict the requests that match the route

· Namespaces – A set of namespaces that restrict the classes that match the route

The MapRoute() method has multiple overloads. You can call the MapRoute() method without supplying the Defaults, Constraints, or Namespaces parameters.

The default route configured in the Global.asax file in Listing 1 is named, appropriate enough, Default.

The URL parameter for the Default route matches URLs that satisfy the pattern {controller}/{action}/{id}. Therefore, the Default route matches URLs that look like this:

· /Product/Insert/23

· /Home/Index/1

· /Do/Something/Useful

However, the Default route does not match a URL that looks like this:

· /Product/Insert/Another/Item

The problem with this last URL is that it has too many segments. It has four different segments (four forward slashes) and the URL pattern {controller}/{action}/{id} only matches URLs that have three segments.

The URL pattern {controller}/{action}/{id} maps the first segment to a parameter named controller, the second segment to a parameter named action, and the final segment to a parameter named id.

The controller and action parameters are special. The ASP.NET MVC framework uses the controller parameter to determine which MVC controller to use to handle the request. The action parameter represents the action to call on the controller in response to the request.

If you create additional parameters – parameters that are not named controller or action – then the additional parameters are passed to an MVC controller action when the action is invoked. For example, the id parameter is passed as a parameter to a controller action.

Finally, the Default route includes a set of default values for the controller, action, and id parameters. By default, the controller parameter has the value Home, the action parameter has the value Index, and the id parameter has the value “” (empty string).

For example, imagine that you enter the following URL into the address bar of your browser:

http://www.MySite.com/Product

In that case, the controller, action, and id parameter would have the following values:

controller : Product

action: Index

id : “”

Now, imagine that you request the default page for a website:

http://www.MySite.com/

In that case, the controller, action, and id parameters would have the following values:

controller : Home

action: Index

id : “”

In this case, the ASP.NET MVC framework would invoke the Index() action method on the HomeController class.

*** Begin Note ***

The code used to specify the defaults for a route might appear strange to you. This code is taking advantage of two new features of the Visual Basic .NET 9.0 and the C# 3.0 languages called anonymous types and property initializers. You can learn about these new language features by reading Appendix A.

*** End Note ***

Debugging Routes

In the next section, I show you how you can add custom routes to the Global.asax file. However, before we start creating custom routes, it is important to have some way of debugging our routes. Otherwise, things get confusing fast.

Included with the code that accompanies this book is a project named RouteDebugger. If you add a reference to the assembly generated by this project then you can debug the routes configured within any ASP.NET MVC application.

Here’s how you add a reference to the RouteDebugger assembly. Select the menu option Project, Add Reference to open the Add Reference dialog box (see Figure 1). Select the Browse tab and browse to the assembly named RouteDebugger.dll located in the RouteDebuggerBinDebug folder. Click the OK button to add the assembly to your project.

Figure 1 – Using the Add Reference dialog box

clip_image002

After you add the RouteDebugger assembly to an ASP.NET MVC project, you can debug the routes in the project by entering the following URL into the address bar of your browser:

/RouteDebugger

Invoking the RouteDebugger displays the page in Figure 2. You can enter any relative URL into the form field and view the routes that match the URL. The URL should be an application relative URL and start with the tilde character ~.

Figure 2 – Using the Route Debugger

clip_image004

Whenever you enter a URL into the Route Debugger, the Route Debugger displays all of the routes from the application’s route table. Each route that matches the URL is displayed with a green background. The first route that matches the URL is the route that would actually be invoked when the application is running.

*** Begin Warning ***

Be warned that changing your routes might prevent you from using the route debugger. You cannot invoke the RouteDebugger unless your application includes a route that maps to the RouteDebugger controller.

*** End Warning ***

Creating Custom Routes

You can build an entire ASP.NET MVC application without creating a single custom route. However, there are situations in which it makes sense to create a new route. For example, imagine that you are creating a blog application and you want to route requests that look like this:

/Archive/12-25-2008

When someone requests this URL, you want to display blog entries for the date 12-25-2008.

The Default route defined in the Global.asax would extract the following parameters from this URL:

controller: Archive

action: 12-25-1966

This is wrong. You don’t want to invoke a controller action named 12-25-1966. Instead, you want to pass this date to a controller action.

Listing 2 contains a custom route, named BlogArchive, which correctly handles requests for blog entries.

Listing 2 – BlogArchive Route (C#)

routes.MapRoute(
  "BlogArchive",
  "Archive/{entryDate}",
  new { controller = "Blog", action = "Archive" }
);

Listing 2 – BlogArchive Route (VB)

routes.MapRoute( _
   "BlogArchive", _
   "Archive/{entryDate}", _
   New With {.controller = "Blog", .action = "Archive"} _
)

The BlogArchive route matches any URL that satisfies the pattern /Archive/{entryDate}. The route invokes a controller named Blog and a controller action method named Archive(). The entryDate parameter is passed to the Archive() action method.

You can use the controller in Listing 3 with the BlogArchive route. This controller contains an Archive() action method that echoes back the value of the entryDate parameter.

Listing 3 – ControllersBlogController.cs (C#)

using System;
using System.Web.Mvc;

namespace MvcApplication1.Controllers
{
    public class BlogController : Controller
    {
        public string Archive(DateTime entryDate)
        {
            return entryDate.ToString();
        }

    }
}

Listing 3 – ControllersBlogController.vb (VB)

Public Class BlogController
    Inherits System.Web.Mvc.Controller

    Function Archive(ByVal entryDate As DateTime) As String
        Return entryDate.ToString()
    End Function

End Class

The order that you add a custom route to the Global.asax file is important. The first route matched is used. For example, if you reverse the order of the BlogArchive and Default routes in Listing 2, then the Default route would always be executed instead of the BlogArchive route.

*** Begin Warning ***

The order of your routes in the Global.asax file matters.

*** End Warning ***

Creating Route Constraints

When you create a custom route, you can include route constraints. A constraint restricts the requests that match a route. There are three basic types of constraints: regular expression constraints, the HttpMethod constraint, and custom constraints.

Using Regular Expression Constraints

You can use a regular expression constraint to prevent a request from matching a route unless a parameter extracted from the request matches a particular regular expression pattern. You can use regular expressions to match just about any string pattern including currency amounts, dates, times, and numeric formats.

For example, the BlogArchive custom route that we created in the previous section was created like this:

[C#]

routes.MapRoute(
  "BlogArchive",
  "Archive/{entryDate}",
  new { controller = "Blog", action = "Archive" }
);

[VB]

routes.MapRoute( _
   "BlogArchive", _
   "Archive/{entryDate}", _
   New With {.controller = "Blog", .action = "Archive"} _
)

This custom route matches the following URLs:

/Archive/12-25-1966

/Archive/02-09-1978

Unfortunately, the route also matches these URLs:

/Archive/apple

/Archive/blah

There is nothing to prevent you from entering something that is not a date in the URL. If you request a URL like /Archive/apple, then you get the error page in Figure 3.

Figure 3 – Entering a URL with an invalid date

clip_image006

We really need to prevent URLs that don’t contain dates from matching our BlogArchive route. The easiest way to fix our route is to add a regular expression constraint. The following modified version of the BlogArchive route won’t match URLs that don’t contain dates in the format 01-01-0001:

[C#]

routes.MapRoute(
  "BlogArchive",
  "Archive/{entryDate}",
  new {controller = "Blog", action = "Archive"},
  new {entryDate = @"d{2}-d{2}-d{4}"}
);

[VB]

routes.MapRoute( _
  "BlogArchive", _
  "Archive/{entryDate}", _
  New With {.controller = "Blog", .action = "Archive"}, _
  New With {.entryDate = "d{2}-d{2}-d{4}"} _
)

The fourth parameter passed to the MapRoute() method represents the constraints. This constraint prevents a request from matching this route when the entryDate parameter does not match the regular expression d{2}-d{2}-d{4}. In other words, the entryDate must match the pattern of two decimals followed by a dash followed by two decimals followed by a dash followed by four decimals.

You can quickly test your new version of the BlogArchive route with the RouteDebugger. The page in Figure 4 shows the matched routes when an invalid date is entered. Notice that the BlogArchive route is not matched.

Figure 4 – Using the RouteDebugger with the modified BlogArchive route

clip_image008

Using the HttpMethod Constraint

The URL Routing framework includes a special constraint named the HttpMethod constraint. You can use the HttpMethod constraint to match a route to a particular type of HTTP operation. For example, you might want to prevent a particular URL from being accessed when performing an HTTP GET but not when performing an HTTP POST.

*** Begin Note ***

Instead of using the HttpMethod constraint, consider using the AcceptVerbs attribute. You can apply the AcceptVerbs attribute to a particular controller action or an entire controller to prevent a controller action from being invoked unless the action is invoked with the right HTTP method. We discuss the AcceptVerbs attribute in Chapter 3, Understanding Controllers.

*** End Note ***

For example, the following route, named ProductInsert, can be called only when performing an HTTP POST operation:

[C#]

routes.MapRoute(
    "ProductInsert",
    "Product/Insert",
     new  {controller = "Product", action = "Insert"},
     new  {method = new HttpMethodConstraint("POST")}
);

[VB]

routes.MapRoute( _
    "ProductInsert", _
    "Product/Insert", _
     New With {.controller = "Product", .action = "Insert"}, _
     New With {.method = New HttpMethodConstraint("POST")} _
)

You can check whether the ProductInsert route really works by taking advantage of the RouteDebugger. The RouteDebugger enables you to pick an HTTP method that you want to simulate. The page in Figure 5 illustrates testing the ProductInsert route when performing an HTTP POST operation.

clip_image010
Figure 5 – Matching the ProductInsert route when performing an HTTP POST

Creating an Authenticated Constraint

If you need to create a more complicated constraint — something that you cannot easily represent with a regular expression — then you can create a custom constraint. You create a custom constraint by creating a class that implements the IRouteConstraint interface. This interface is really easy to implement since it includes only one method: the Match() method.

For example, Listing 4 contains a new constraint named the AuthenticatedConstraint. The AuthenticatedConstraint prevents a request from matching a route when the request is not made by an authenticated user.

Listing 4 – ConstraintsAuthenticatedConstraint.vb (C#)

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

namespace MvcApplication1.Constraints
{
    public class AuthenticatedConstraint : IRouteConstraint
    {

        public bool Match
            (
                HttpContextBase httpContext,
                Route route,
                string parameterName,
                RouteValueDictionary values,
                RouteDirection routeDirection
            )
        {
            return httpContext.Request.IsAuthenticated;
        }

    }
}

Listing 4 – ConstraintsAuthenticatedConstraint.vb (VB)

Imports System.Web
Imports System.Web.Routing

Public Class AuthenticatedConstraint
    Implements IRouteConstraint

    Public Function Match _
    ( _
        ByVal httpContext As HttpContextBase, _
        ByVal route As Route, _
        ByVal parameterName As String, _
        ByVal values As RouteValueDictionary, _
        ByVal routeDirection As RouteDirection _
    ) As Boolean Implements IRouteConstraint.Match
        Return HttpContext.Request.IsAuthenticated
    End Function

End Class

In Listing 4, the Match() method simply returns the value of the HttpContext.Request.IsAuthenticated property to determine whether the current request is an authenticated request. If the Match() method returns the value False, then the request fails to match the constraint and the route is not matched.

After you create the AuthenticatedConstraint, you can use it with a route like this:

[C#]

routes.MapRoute(
    "Admin",
    "Admin/{action}",
    new {controller = "Admin"},
    new {Auth = new AuthenticatedConstraint()}
);

[VB]

routes.MapRoute( _
    "Admin", _
    "Admin/{action}", _
    New With {.controller = "Admin"}, _
    New With {.Auth = New AuthenticatedConstraint()} _
)

It is important to understand that the AuthenticatedConstraint prevents only a particular route from matching a request. Another route, that does not include the AuthenticatedConstraint, might match the very same request and invoke the very same controller action. In the next section, I show you how to create a constraint that prevents a route from ever invoking a particular controller.

*** Begin Note ***

The ASP.NET MVC framework includes an Authorize attribute that you can apply to either a particular action or entire controller to prevent access from unauthorized users. We discuss the Authorize attribute in Chapter 3, Understanding Controllers.

*** End Note ***

Creating a NotEqual Constraint

If you want to create a route that will never match a particular controller action — or more generally, that will never match a particular route parameter value – then you can create a NotEqual constraint.

The code for the NotEqual constraint is contained in Listing 5.

Listing 5 – ConstraintsNotEqual.cs (C#)

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

namespace MvcApplication1.Constraints
{
    public class NotEqual:IRouteConstraint
    {
        private string _value;

        public NotEqual(string value)
        {
            _value = value;
        }

        public bool Match
            (
                HttpContextBase httpContext,
                Route route,
                string parameterName,
                RouteValueDictionary values,
                RouteDirection routeDirection
            )
        {
            var paramValue = values[parameterName].ToString();
            return String.Compare(paramValue, _value, true) != 0;
        }

    }
}

Listing 5 – ConstraintsNotEqual.vb (VB)

Public Class NotEqual
    Implements IRouteConstraint

    Private _value As String

    Sub New(ByVal value As String)
        _value = value
    End Sub

    Public Function Match( _
        ByVal httpContext As HttpContextBase, _
        ByVal route As Route, _
        ByVal parameterName As String, _
        ByVal values As RouteValueDictionary, _
        ByVal routeDirection As RouteDirection _
    ) As Boolean Implements IRouteConstraint.Match
        Dim paramValue = values(parameterName).ToString()
        Return String.Compare(paramValue, _value, True) <> 0
    End Function

End Class

The NotEqual constraint performs a case-insensitive match of the value of a parameter against a field named _value. If there is a match, the constraint fails and the route is skipped.

After you create the NotEqual constraint, you can create a route that uses the constraint like this:

[C#]

routes.MapRoute(
    "DefaultNoAdmin",
    "{controller}/{action}/{id}",
    new {controller = "Home", action = "Index", id = ""},
    new {controller = new NotEqual("Admin")}
);

[VB]

routes.MapRoute( _
    "DefaultNoAdmin", _
    "{controller}/{action}/{id}", _
    New With {.controller = "Home", .action = "Index", .id = ""}, _
    New With {.controller = New NotEqual("Admin")} _
)

This route works just like the Default route except for the fact that it will never match when the controller parameter has the value Admin. You can test the NotEqual constraint with the RouteDebugger. In Figure 6, the URL /Admin/Delete matches the Default route, but it does not match the DefaultNoAdmin route.

Figure 6 – Using the NotEqual Constraint

clip_image012

Using Catch-All Routes

Normally, in order to match a route, a URL must contain a particular number of segments. For example, the URL /Product/Details matches the following route:

[C#]

routes.MapRoute(
    "Product1",
    "Product/{action}",
    new {controller = "Product"}
);

[VB]

routes.MapRoute( _
    "Product1", _
    "Product/{action}", _
    New With {.controller = "Product"} _
)

However, this URL does not match the following route:

[C#]

routes.MapRoute(
    "Product2",
    "Product/{action}/{id}",
    new {controller = "Product"}
);

[VB]

routes.MapRoute( _
    "Product2", _
     "Product/{action}/{id}", _
     New With {.controller = "Product"} _
)

This route requires a URL to have 3 segments and the URL /Product/Details has only two segments (two forward slashes).

*** Begin Note ***

The URL being requested is not required to have the same number of segments as a route’s URL parameter. When a route parameter has a default value, a segment is optional. For example, the URL /Home/Index matches a route that has the URL pattern {controller}/{action}/{id} when the id parameter has a default value.

*** End Note ***

If you want to match a URL, regardless of the number of segments in the URL, then you need to create something called a catch-all parameter. Here’s an example of a route that uses a catch-all parameter:

[C#]

routes.MapRoute(
    "SortRoute",
    "Sort/{*values}",
    new {controller = "Sort", action = "Index"}
);

[VB]

routes.MapRoute( _
    "SortRoute", _
    "Sort/{*values}", _
    New With {.controller = "Sort", .action = "Index"} _
)

Notice that the route parameter named values has a star in front of its name. The star marks the parameter as a catch-all parameter. This route matches any of the following URLs:

/Sort

/Sort/a/b/d/c

/Sort/Women/Fire/Dangerous/Things

All of the segments after the first segment are captured by the catch-all parameter.

*** Begin Note ***

A catch-all parameter must appear as the very last parameter. Think of a catch-all parameter as a Visual Basic .NET parameter array.

*** End Note ***

The Sort controller in Listing 6 illustrates how you can retrieve the value of a catch-all parameter within a controller action.

Listing 6 – ControllersSortController.cs (C#)

using System;
using System.Web.Mvc;

namespace MvcApplication1.Controllers
{
    public class SortController : Controller
    {

        public string Index(string values)
        {
            var brokenValues = values.Split('/');
            Array.Sort(brokenValues);
            return String.Join(", ", brokenValues);
        }


    }
}

Listing 6 – ControllersSortController.vb (VB)

Imports System
Imports System.Web.Mvc

Public Class SortController
    Inherits Controller

    Public Function Index(ByVal values As String) As String
        Dim brokenValues = values.Split("/"c)
        Array.Sort(brokenValues)
        Return String.Join(", ", brokenValues)
    End Function

End Class    

Notice that the catch-all parameter is passed to the Index() action as a string (you cannot pass the value as an array). The Index() method simply sorts the values contained in the catch-all parameter and returns a string with the values in alphabetical order (see Figure 7).

clip_image014

Figure 7 – Using a catch-all parameter

Testing Routes

Every feature of the ASP.NET MVC framework was designed to be highly testable and URL Routing is no exception. In this section, I describe how you can unit test both your routes and your route constraints.

Why would you want to build unit tests for your routes? If you build route unit tests then you can detect whether changes in your application break existing functionality automatically. For example, if your existing routes are covered by unit tests, then you know immediately whether introducing a new route prevents an existing route from ever being called.

Using the MvcFakes and RouteDebugger Assemblies

In order to unit test your custom routes, I recommend that you add references to two assemblies: the RouteDebugger and the MvcFakes assemblies.

If you want to test your routes by name, then you need to add a reference to the RouteDebugger assembly. The RouteDebugger assembly replaces the anonymous routes in your MVC application with named routes. That way, you can build tests that check whether a particular route is called by name.

I also recommend that you add a reference to the MvcFakes assembly. The MvcFakes assembly contains a set of fake objects that you can use in your unit tests. For example, MvcFakes includes a FakeHttpContext object. You can use the FakeHttpContext object to fake every aspect of a browser request.

Both of these assemblies are included with the code that accompanies this book. You can add references to these assemblies to your test project by selecting the menu option Project, Add Reference, selecting the Browse tab, and browsing to the following two assemblies (see Figure 12):

Chapter09RouteDebuggerBinDebugRouteDebugger.dll

CommonCodeMvcFakesBinDebugMvcFakes.dll

Figure 12 – Adding a reference to an assembly

clip_image016

Testing If a URL Matches a Route

Let’s start with a basic but very useful unit test. Let’s create a unit test that verifies that a particular URL matches a particular route. The unit test is contained in Listing 8.

Listing 8 – RoutesRouteTest.cs (C#)

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

namespace MvcApplication1.Tests.Routes
{
    [TestClass()]
    public class RouteTest
    {
        [TestMethod]
        public void DefaultRouteMatchesHome()
        {
            // Arrange
            var routes = new RouteCollection();
            MvcApplication.RegisterRoutes(routes);

            // Act
            var context = new FakeHttpContext("~/Home");
            var routeData = routes.GetRouteData(context);

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

    }
}

Listing 8 – RoutesRouteTest.vb (VB)

Imports Microsoft.VisualStudio.TestTools.UnitTesting
Imports System.Web.Routing
Imports MvcFakes
Imports RouteDebugger.Routing

<TestClass()> _
Public Class RouteTest

    <TestMethod()> _
    Public Sub DefaultRouteMatchesHome()
        ' Arrange
        Dim routes = New RouteCollection()
        MvcApplication.RegisterRoutes(routes)

        ' Act
        Dim context = New FakeHttpContext("~/Home")
        Dim routeData = routes.GetRouteData(context)

        ' Assert
        Dim matchedRoute = CType(routeData.Route, NamedRoute)
        Assert.AreEqual("Default", matchedRoute.Name)
    End Sub

End Class

You can add the unit test in Listing 8 to a Test project by selecting the menu option Project, Add New Test and selecting the Unit Test template. Remember to add the assembly references discussed in the previous section or the unit test won’t compile.

*** Warning ***

Don’t select the Unit Test Wizard. Also, don’t select the tempting menu option Project, Add New Unit Test. Either option launches the Unit Test Wizard. The Unit Test Wizard creates a unit test that launches a web server (we don’t want to do that). Instead, always pick the menu option Project, Add New Test and select the Unit Test template.

*** End Warning ***

After you create the unit test in Listing 8, you can run it by entering the keyboard combination CTRL-R, A. Alternatively; you can click the Run All Tests in Solution button contained in the test toolbar (see Figure 9).

Figure 9 – Run all tests in the solution

clip_image018

The test in Listing 8 verifies that the URL ~/Home matches the route named Default. The unit test consists of three parts.

The first part, the Arrange part, sets up the routes by creating a new route collection and passing the route collection to the RegisterRoutes() method exposed by the Global.asax file (notice that you use the MvcApplication class to refer to the class exposed by Global.asax).

The second part, the Act part, sets up the fake HttpContext which represents the browser request for ~/Home. The FakeHttpContext object is part of the MvcFakes project. The FakeHttpContext object is passed to the GetRouteData() method. This method takes an HttpContext and returns a RouteData object that represents information about the route matched by the HttpContext.

Finally, the Assert part verifies that the RouteData represents a route named Default. At this point, the unit test either succeeds or fails. If it succeeds, then you get the test results in Figure 15.

Figure 15 – The test results

clip_image020

Testing Routes with Constraints

Let’s try testing a slightly more complicated route. Earlier in this chapter, we discussed the HttpMethodConstraint which you can use to match a route only when the right HTTP method is used. For example, the following route should only match a browser request performed with an HTTP POST operation:

[C#]

routes.MapRoute(
    "ProductInsert",
    "Product/Insert",
     new  {controller = "Product", action = "Insert"},
     new  {method = new HttpMethodConstraint("POST")}
);

[VB]

routes.MapRoute( _
    "ProductInsert", _
    "Product/Insert", _
     New With {.controller = "Product", .action = "Insert"}, _
     New With {.method = New HttpMethodConstraint("POST")} _
)

The HttpMethodConstraint restricts this route to match only POST requests. This is the intention, how do you test it? Easy, fake the HTTP operation with the FakeHttpContext object.

The unit test in Listing 9 contains two unit tests. The first test verifies that the ProductInsert route is matched when performing a POST operation. The second test verifies that the ProductInsert route is not matched when performing a GET operation.

Listing 9 – RoutesRouteTest.cs (with ProductInsert tests) (C#)

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

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

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




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

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

    // Assert
    if (routeData != null)
    {
        var matchedRoute = (NamedRoute)routeData.Route;
        Assert.AreNotEqual("ProductInsert", matchedRoute.Name);
    }
}

Listing 9 – RoutesRouteTest.vb (with ProductInsert tests) (VB)

<TestMethod()> _
Public Sub ProductInsertMatchesPost()
    ' Arrange
    Dim routes = New RouteCollection()
    MvcApplication.RegisterRoutes(routes)

    ' Act
    Dim context = New FakeHttpContext("~/Product/Insert", "POST")
    Dim routeData = routes.GetRouteData(context)

    ' Assert
    Dim matchedRoute = CType(routeData.Route, NamedRoute)
    Assert.AreEqual("ProductInsert", matchedRoute.Name)
End Sub




<TestMethod()> _
Public Sub ProductInsertDoesNotMatchGet()
    ' Arrange
    Dim routes = New RouteCollection()
    MvcApplication.RegisterRoutes(routes)

    ' Act
    Dim context = New FakeHttpContext("~/Product/Insert", "GET")
    Dim routeData = routes.GetRouteData(context)

    ' Assert
    If routeData IsNot Nothing Then
        Dim matchedRoute = CType(routeData.Route, NamedRoute)
        Assert.AreNotEqual("ProductInsert", matchedRoute.Name)
    End If
End Sub

The second parameter passed to the constructor for the FakeHttpContext object determines the HTTP operation that the FakeHttpContext object represents.

Summary

In this chapter, you learned how to control how browser requests map to controllers and controller actions by taking advantage of ASP.NET Routing. We started by discussing the standard parts of a route.

You also learned how to debug the routes contained in the Global.asax file by taking advantage of the RouteDebugger assembly. We took advantage of the RouteDebugger when building our custom routes.

We also discussed how you can use route constraints to limit the routes that match a browser request. In particular, you learned how to use regular expression route constraints, HttpMethod route constraints, and custom route constraints.

Finally, you learned how to build unit tests for your custom routes. You learned how to take advantage of the FakeHttpContext object to fake different browser requests and test the routes that are matched.

Download the Code

Discussion

  1. Craig Carns says:

    Stephen – Very good content and I understand why you chose to have this as the second chapter, but as person new to the subject this chapter dives heavily into advanced routing scenarios before having a good understanding of the MVC paradiagm (which I assume you will be covering in more depth in CH3+. You could simply put a note at the start of “Creating Custom Routes” to skip this section for now and come back to later. Just an opinion.

  2. cowgaR says:

    well, Stephen, I’ve received your “webforms” book on my birthday, and it was really great. Although I liked some parts of it more than the others (content of .net 3.5 was somewhat “unfinished”. I mean whole LINQ part and some new C# 3.0 features.) I haven’t had doubt it is one of the most popular books there…

    I thought the MVC will be another milestone, but I can’t say that I like the too much “theoretical” approach you have taken in this new book (the 1st and 2nd part).

    As an example I’ll give you Bruce Eckel’s writing, which nice in the first editions of Java, to the time when he began working on C# (after finishing 4th Java ed) he had to abandon the book 😉
    There was too-too much theory how things works. Although I had some “wow” moments reading it (the “beta” electronic version) that you can get only by reading Eckel’s excellent matterial, the electronic version was more or less boring and I often slept after few pages. It was slow-paced and too much theoretical. And it was just about the language.

    That’s why I think you should probably start your chapters with building your first application (from scratch) using scaffolding features and HTML helpers. And showing how “easy” to the contrary, it is. Because, basicaly, you can explain whole MVC concept in a few minutes.

    Then probably explain and dive more to the Views/Controllers problematic, and later on to the model/OR/M topic. And all that is needed.

    Last but not least something about routes, but which dear reader doesn’t need to understand too much in the begining. As simple modification is easy to do and simple explanation that routes doesn’t point to the resource on the server/disc will do.

    Because once reader grasp a concept, will make his first app, and later grasp advanced concept, he will begin seeing problems with the routes and will need your chapter as a salt. But I think he (and I) won’t get it in the beginning of the book.

    A newbie reading a book, and seeing Routes as 2nd chapter, still not able to build application yet, will be imho distracted.
    For me, starting learning ASP.NET MVC, routes were the last thing I’ve learnt (and I’ve skipped ScottGu blog part), and honestly I didn’t need it too much (basic stuff is easy to do).

    Just my 2 cents, I am really looking forward for your book (I pre-ordered it) but I want it to be concise, practical, to the point with advanced code.
    And you can throw more theory in some later chapters “how things works”.

    Because non-English readers like myself will get really bored when they need to read 300 pages to get to the metal.

    I hope you know what I mean. Thanks. And sorry for criticism, there are certainly ppl that like your approach.

  3. Yousaid says:

    Steve,
    I believe I have all the books you’ve ever written on .Net, but there’s one thing I have found missing in all. You do not include a “Try it” section. That is, at the end of every chapter, you throw in a problem for the reader to resolve and then at the very end of the book, you provide a solution to the problem. This reinforces what was learned in the chapter and MVC being an entirely new (almost) concept, this may be very helpful.
    My 1.5cents ofcourse.
    cheers,
    yousaid

  4. leonluchen says:

    Hi,Steve
    Nice content. I think I know and like the way you write this book. Please go deep enough in every chapter.

  5. @craig, @cowgaR – Thanks for the comments. I’ve rearranged the chapters in the book in response. I’ve now pushed the Routing chapter back to Chapter 9. I’ve created a new Chapter 2 that consists of a walkthrough of a basic ASP.NET MVC application. The new chapter is here:
    stephenwalther.com/…/…net-mvc-application.aspx

  6. andrex says:

    Good article. But missed information about IgnoreRoute method. Interesting how and where it can be used.

  7. avinash says:

    I have two pages

    1:– App->season.aspx
    2:– App->Admin->AddSeason.aspx

    How i navigate to this two different pages using Asp.net MVC?

    Which are the codes i need to write in global.ascx file?

  8. @avinash — I’m not sure I understand your question, but let me give it a shot.

    For the URL /Season, I would create the route:

    routes.MapRoute(
    “Season”,
    “Season”,
    new {controller = “Season”, action = “Index”}}
    );

    The /Admin/AddSeason URL will work with the default route.

  9. Filemon says:

    Hi Stephen,

    First of all, congrats for the great job.

    What about dynamic URLs stored in the database, without specific structure ?

    For exemple for a blog

    http://domain.com/my-first-entry
    http://domain.com/here-is-another-entry

    I don’t want to hardcore URLs structure as everything must be managed from the database
    (think about a CMS like DotNetNuke)

  10. Dan Crowell says:

    Hey Stephen,

    I really enjoy your writing and approach to software development. I am currently reading your ASP.net 3.5 Unleashed book.

    Creating routes that include dashes allows you to include keywords in a URL. This is beneficial from a search engine optimization perspective. You might consider mentioning the SEO benefits that can be achieved by having dashes in URLs.

    Cheers, Dan

  11. mh415 says:

    “routes.MapRoute” wants a Route Name, but what’s that ever used for?

  12. Brian says:

    Stephen – More good stuff. I’m glad you’ve rearranged the order which this chapter appears.

    You mention in one of the notes above that you discuss authorized requests in Chapter 3:

    “The ASP.NET MVC framework includes an Authorize attribute that you can apply to either a particular action or entire controller to prevent access from unauthorized users. We discuss the Authorize attribute in Chapter 3, Understanding Controllers.”

    In the current version of Chapter 3, I don’t believe you discuss the Authorize attribute. Maybe you’ll add that to the chapter?

  13. I’ve created a new Chapter 2 that consists of a walkthrough of a basic ASP.NET MVC application. The new chapter is here:

  14. Very good contents. Now I understand very well about Routing. Thanks Stephen.

  15. vick says:

    Easy Video Converter : Video To AVI, Video To MPEG, Video To WMV, Video To RM, Video To iPod Video Converter Free.EZ Video To iPod Converter : Best 3GP To iPod, MP4 To iPod, MOV To iPod, AVI To iPod, MPEG To iPod, WMV To iPod, ASX To iPod, ASF To iPod, RM To iPod rm converter.

  16. As long as you have the area code and the seven digit number, you should be able to locate any number’s owner, if you know cell phone lookup reviews viste this web: cell phone lookups reviews

  17. Very good example is given in attachment. I understand that very well. Thanks.

  18. We discuss the Authorize attribute in Chapter 3, Understanding Controllers.
    Information Technology Diploma | Performing Arts school

  19. Michal Clark says:

    You mention in one of the notes above that you discuss authorized requests in Chapter 3.
    Health science school | Hr degree

  20. online games says:

    I really enjoy your writing and approach to software development. I am currently reading your ASP.net 3.5 Unleashed book.

  21. C# Homework says:

    I find this information really useful, thanks.

  22. nick says:

    I find this information really useful, thanks.
    football live scoresoccer live scorelive scores basketball

  23. batista says:

    Very good example is given in attachment. I understand that very well.
    UK kamagra wholesaleUK kamagra supplierBuy Kamagra tablets

  24. 4444444444 It’s lucky to know this, if it is really true. Companies tend not to realize when they create security holes from day-to-day operation.