Chapter 16. Routing – ASP.NET MVC 2 in Action

Chapter 16. Routing

This chapter covers

  • Routing as a solution to URL issues
  • Designing a URL schema
  • Using routing in ASP.NET MVC
  • Testing routes
  • Using routing in Web Forms applications

So far in this book, we’ve stuck with the default routing configuration that comes with any new ASP.NET MVC project. In this chapter, we’ll cover the routing system in depth and learn how to create custom routes for our applications.

Routing is all about the URL and how we use it as an external input to the applications we build. The URL has led a short but troubled life, and the HTTP URL is currently being tragically misused by current web technologies. As the web began to change from being a collection of hyperlinked static documents into dynamically created pages and applications, the URL has been kidnapped by web technologies and undergone terrible changes, so that we now see file extensions like .aspx and .php mapping to physical files in public URLs. The URL is in trouble, and as the web becomes more dynamic, we, as software developers, can rescue it and bring back the simple, logical, readable, and beautiful resource locator that it was meant to be.

Rescuing the URL means changing the way we write web applications. Although routing isn’t core to all implementations of the MVC pattern, it’s often treated as a convenient way to add an extra level of separation between external inputs and the controllers and actions that make up an application. The code required to implement routing using the ASP.NET MVC Framework is reasonably trivial, but the thought behind designing a schema of URLs for an application can raise many issues.

In this chapter, we’ll go over the concept of routes and their relationships with MVC applications. We’ll also briefly cover how they apply to Web Forms projects. We’ll examine how to design a URL schema for an application, and then apply the concepts to create routes for a sample application. We’ll look at how to test routes to ensure they’re working as intended.

Now that you have an idea of how important routing is, we can start with the basics.

16.1. What are routes?

The history of the URL can be traced back to the very first web servers, where it was primarily used to point directly to documents in a folder structure. This URL would’ve been typical of an early URL, and it’s reasonably well structured and descriptive:

http://example.com/plants/roses.html

It seems to be pointing to information on roses, and the domain also seems to have a logical hierarchy. But hold on, what’s that .html extension on the end of the URL? This is where things started to go wrong for our friend the URL. Of course, .html is a file extension because the web server is mapping the path in the URL directly to a folder of files on the disk of the web server. The category of “plants” in our URL is created by having a folder called plants containing all documents about plants.

The key thing here is that the file extension of .html is probably redundant in this context, because the content type is being specified by the Content-Type header returned as part of the HTTP response. An example HTTP header is shown in listing 16.1, with the Content-Type header displayed in bold.

Listing 16.1. HTTP headers returned for an .html file
C:\> curl -I http://example.com/index.html

HTTP/1.1 200 OK
Date: Thu, 10 Jan 2008 09:03:29 GMT
Server: Apache/2.2.3 (CentOS)
Last-Modified: Tue, 15 Nov 2005 13:24:10 GMT
ETag: "280100-1b6-80bfd280"
Accept-Ranges: bytes
Content-Length: 438
Connection: close
Content-Type: text/html; charset=UTF-8

16.1.1. What’s that curl command?

The curl command shown in listing 16.1 is a Unix command that allows you to issue an HTTP GET request for a URL and return the output. The –I switch tells it to display the HTTP response headers. This and other Unix commands are available on Windows via the Cygwin shell for Windows (http://cygwin.com).

The response returned contains a Content-Type header set to text/html; charset=UTF-8, which specifies both a MIME type for the content and the character encoding. The file extension has no meaning in this situation.

 

File extensions aren’t all bad!

Reading this chapter so far, you might think that all file extensions are bad, but that isn’t the case. Knowing when information will be useful to the user is key to understanding when to use a file extension. Is it useful for the user to know that HTML has been generated from an .aspx source file? No, the MIME type is sufficient to influence how that content is displayed, so no extension should be shown. But if a Word document is being served, it would be good practice to include a .doc extension in addition to setting the correct MIME type, because that will be useful when the file is downloaded to the user’s computer.

 

Mapping the path part of a URL directly to a disk folder is at the root of the problems that web developers face today. As dynamic web technologies have developed, .html files containing information have changed to .aspx files containing source code. Suddenly the URL isn’t pointing to a document but to source code that fetches information from a database, and the filename must be generic because one source file can fetch any information it wants. What a mess!

Consider the following URL:

http://microsoft.com/downloads/details.aspx?FamilyID=9ae91ebe-3385-447c-8a30-
081805b2f90b&displaylang=en

The file path is /download/details.aspx, which is a reasonable attempt to be descriptive with the source code name, but it’s a generic page that fetches the actual download details from a database. The filename can’t possibly contain the important information that the URL should contain. Even worse, an unreadable GUID is used to identify the actual download, and at this point the URL has lost all meaning.

This is a perfect opportunity to create a beautiful URL. Decouple the source code filename from the URL, and it can become a resource locator again with the resource being a download package for Internet Explorer. The user never needs to know that this resource is served by a page called details.aspx. The result would look like this:

http://microsoft.com/downloads/windows-internet-explorer-7-for-windows-xp-sp2

This is clearly an improvement, but we’re assuming that the description of the item is unique. Ideally, in the design of an application, we could make some human-readable information like the title or description unique to support the URL schema. If this weren’t possible, we could implement another technique to end up with something like the following URL:

http://microsoft.com/downloads/windows-internet-explorer-7-for-windows-xp-
sp2/1987429874

In this final example, both a description of the download and a unique identifier are used. When the application comes to process this URL, the description can be ignored and the download looked up on the unique identifier. You might want to enforce agreement between the two segments for search engine optimization.

Unfortunately, having multiple URLs pointing to the same logical resource yields poor results for search engines. Let’s see how we can apply these ideas to create better URLs.

16.1.2. Taking back control of the URL with routing

For years, the server platform has dictated portions of the URL, such as the .aspx extension at the end. This problem has been around since the beginning of the dynamic web and affects almost all current web technologies, so you shouldn’t be surprised that many solutions to the problem have been developed. Although ASP.NET does offer options for URL rewriting, many ASP.NET developers ignore them.

Many web technologies, such as PHP and Perl, hosted on the Apache web server, solve this problem by using mod_rewrite. Python and Ruby developers have taken to the MVC frameworks, and both Django and Rails have their own sophisticated routing mechanisms.

 

Note

For more information on URL rewriting, you can see the article “URL Rewriting in ASP.NET” on MSDN (http://mng.bz/KotC). Apache’s mod_rewrite is discussed in the Apache documentation (http://httpd.apache.org/docs/2.2/mod/mod_rewrite.html). URL rewriting is also discussed in chapter 6.

 

A routing system in any MVC framework manages the decoupling of the URL from the application logic. It must manage this in both directions:

  • Inbound routingMapping URLs to a controller or action and any additional parameters (see figure 16.1)
    Figure 16.1. Inbound routing refers to taking an HTTP request (a URL) and mapping it to a controller and action.

  • Outbound routingConstructing URLs that match the URL schema from a controller, action, and any additional parameters (see figure 16.2)
    Figure 16.2. Outbound routing generates appropriate URLs from a given set of route data (usually controller and action).

Inbound routing, shown in figure 16.1, describes the URL invocation of a controller action. The HTTP request comes into the ASP.NET pipeline and is sent through the routes registered with the ASP.NET MVC application. Each route has a chance to handle the request, and the matching route then specifies the controller and action to be used.

Outbound routing, shown in figure 16.2, describes the mechanism for generating URLs for links and other elements on a site by using the routes that are registered. When the routing system performs both of these tasks, the URL schema can be truly independent of the application logic. As long as it’s never bypassed when constructing links in a view, the URL schema should be trivial to change independent of the application logic.

Now let’s take a look at how to build a meaningful URL schema for our application.

16.2. Designing a URL schema

As a professional developer, you wouldn’t start coding a new project before mapping out what the application will do and how it will look. The same should apply for the URL schema of an application. Although it’s hard to provide a definitive guide on designing URL schema (every website and application is different), we’ll discuss general guidelines with an example or two thrown in along the way.

Here’s a list of guidelines:

  • Make simple, clean URLs.
  • Make hackable URLs.
  • Allow URL parameters to clash.
  • Keep URLs short.
  • Avoid exposing database IDs wherever possible.
  • Consider adding unnecessary information.

These guidelines won’t all apply to every application you create, but you should keep them in mind while deciding on your final URL schema.

16.2.1. Make simple, clean URLs

When designing a URL schema, the most important thing to remember is that you should step back from your application and consider it from the point of view of your end user. Ignore the technical architecture you’ll need to implement the URLs. Remember that by using routing, your URLs can be completely decoupled from your underlying implementation. The simpler and cleaner a permalink is, the more usable a site becomes.

 

Permalinks and deep linking

Over the past few years, permalinks have gained popularity, and it’s important to consider them when designing a URL schema. A permalink is simply an unchanging direct link to a resource within a website or application. For example, on a blog, the URL to an individual post would usually be a permalink such as http://example.com/blog/post-1/hello-world.

 

Let’s take the example of an events-management sample application. In a Web Forms world, we might have ended up with a URL something like this:

http://example.com/eventmanagement/events_by_month.aspx?year=2008&month=4

Using a routing system, it’s possible to create a cleaner URL like this:

http://example.com/events/2008/04

This gives us the advantage of having an unambiguous hierarchical format for the date in the URL, which raises an interesting point. What would happen if we omitted that “04” in the URL? What would the user expect? This is described as hacking the URL.

16.2.2. Make hackable URLs

When designing a URL schema, it’s worth considering how a URL could be manipulated or “hacked” by the end user in order to change the data displayed. For example, it might reasonably be assumed that removing the parameter “04” from the following URL might present all events occurring in 2008:

http://example.com/events/2008/04

The same logic could suggest the more comprehensive list of routes shown in table 16.1.

Table 16.1. Partial URL schema for an events-management application

URL

Description

http://example.com/events

Displays all events

http://example.com/events/<year>

Displays all events in a specific year

http://example.com/events/<year>/<month>

Displays all events in a specific month

http://example.com/events/<year>/<month>/<date>

Displays all events on a specific day

Being this flexible with your URL schema is great, but it can lead to having an enormous number of potential URLs in your application. When you build your application views, you should always give appropriate navigation; remember, it may not be necessary to include a link to every possible URL combination on every page. It’s all right for some things to be a happy surprise when a user tries to hack a URL and for it to work!

 

Slash or dash?

It’s a general convention that if a slash is used to separate parameters, the URL should be valid if parameters are omitted. If the URL /events/2008/04/01/ is presented to users, they could reasonably assume that removing the last “day” parameter could increase the scope of the data shown by the URL. If this isn’t what’s desired in your URL schema, consider using hyphens instead of slashes because /events/2008-04-01/ wouldn’t suggest the same hackability.

 

The ability to hack URLs gives power back to the users. With dates, this is easy to express, but what about linking to named resources?

16.2.3. Allow URL parameters to clash

Let’s expand the routes and allow events to be listed by category. The most usable URL from the user’s point of view would probably be something like this:

http://example.com/events/meeting

But now we have a problem! We already have a route that matches /events/<something> used to list the events on a particular year, month, or day, so how are we now going to try to use /events/<something> to match a category as well? Our second route segment can now mean something entirely different; it clashes with the existing route. If the routing system is given this URL, should it treat that parameter as a category or a date?

Luckily, the routing system in ASP.NET MVC allows us to apply conditions. The syntax for this can be seen in section 16.3.3, but for now it’s sufficient to say that we can use regular expressions to make sure that routes only match certain patterns for a parameter. This means that we could have a single route that allows a request like / events/2009-01-01 to be passed to an action that shows events by date, and a request like /events/asp-net-mvc-in-action to be passed to an action that shows events by category. These URLs should clash with each other, but they don’t because we’ve made them distinct based on what characters will be contained in the URL.

This starts to restrict our model design. It will now be necessary to constrain event categories so that category names made entirely of numbers aren’t allowed. You’ll have to decide if this is a reasonable concession to make in your application for such a clean URL schema.

The next principle we’ll learn about is URL size. For URLs, size matters, and smaller is better.

16.2.4. Keep URLs short

Permalinks are passed around millions of times every day through email, instant messenger, micromessaging services such as SMS and Twitter, and even in conversation. Obviously for a URL to be spoken (and subsequently remembered!), it must be simple, short, and clean. Even when transmitting a permalink electronically this is important, because many URLs are broken due to line breaks in emails.

Short URLs are nice, but you shouldn’t sacrifice readability for the sake of brevity. Remember that when a link to your application is shared, it’s probably going to have only the limited context provided by whoever is sharing it. By having a clear, meaningful URL that’s still succinct, you can provide additional context that may make the difference between the link being ignored or clicked. For example, the following URL is very short, but it isn’t obvious what web resource it serves:

http://example.com/20101225

This URL can be made more readable by making it a touch longer. In the process, it’s more understandable:

http://example.com/paidholidays/20101225

The next guideline is both the most useful in terms of maintaining clarity, and the most violated, thanks to the default routes in the ASP.NET MVC Framework.

16.2.5. Avoid exposing database IDs wherever possible

When designing the permalink to an individual event, the key requirement is that the URL should uniquely identify the event. We obviously already have a unique identifier for every object that comes out of a database in the form of a primary key. This is usually some sort of integer, autonumbered from 1, so it might seem obvious that the URL schema should include the database ID.

For example, a site that’s used to host developer events might define a URL like this:

http://example.com/events/87

Unfortunately, the number 87 means nothing to anyone except the database administrator, and wherever possible you should avoid using database-generated IDs in URLs. This doesn’t mean you can’t use integer values in a URL where relevant, but try to make them meaningful.

An alternative might be to use a permalink identifier that isn’t generated by the database. For example:

http://example.com/events/houstonTechFest2008

Sometimes creating a meaningful identifier for a model adds benefits only for the URL and has no value apart from that. In cases like this, you should ask yourself if having a clean permalink is important enough to justify additional complexity not only on the technical implementation of the model, but also in the UI, because you’ll usually have to ask a user to supply a meaningful identifier for the resource.

This is a great technique, but what if you don’t have a nice unique name for the resource? What if you need to allow duplicate names, and the only unique identifier is the database ID? Our next trick will show you how to utilize both a unique identifier and a textual description to create a URL that’s both unique and readable.

16.2.6. Consider adding unnecessary information

If you must use a database ID in a URL, consider adding additional information that has no purpose other than to make the URL readable. Consider a URL for a specific session in our events application. The Title property isn’t necessarily going to be unique, and it’s probably not practical to have people add a text identifier for a session. If we add the word “session” just for readability, the URL might look something like this:

http://example.com/houstonTechFest2008/session-87

This isn’t good enough though, as it gives no indication what the session is about. Let’s add another superfluous parameter to it. The addition has no purpose other than description. It won’t be used at all while processing the controller action. The final URL could look like this:

http://example.com/houstonTechFest2008/session-87/an-introduction-to-mvc

This is much more descriptive, and the session-87 parameter is still there so we can look up the session by database ID. We’d have to convert the session name to a more URL-friendly format, but that would be trivial.

 

Search engine optimization

It’s worth mentioning the value of a well-designed URL when it comes to optimizing your site for search engines. It’s widely accepted that placing relevant keywords in a URL has a direct effect on search engine ranking, so bear the following tips in mind when you’re designing your URL schema.

1.  

Use descriptive, simple, commonly used words for your controllers and actions. Try to be as relevant as possible and use keywords that you’d like to apply to the page you’re creating.

2.  

Replace all spaces (which are encoded to an ugly %20 in a URL) with hyphens (-) when including text parameters in a route. Some people use underscores, but search engines agree that hyphens are term-separation characters.

3.  

Strip out all nonessential punctuation and unnecessary text from string parameters.

4.  

Where possible, include additional, meaningful information in the URL. Additional information like titles and descriptions provide context and search terms to search engines that can improve the site’s relevancy.

 

The routing principles covered in this section will guide you through your choice of URLs in your application. Decide on a URL schema before going live on a site, because URLs are the entry point into your application. If you have links out there in the wild and you change your URLs, you risk breaking those links and losing referral traffic from other sites.

 

REST and RESTful architectures

A style of architecture called REST (or RESTful architecture) is a recent trend in web development. REST stands for representational state transfer. The name may not be approachable, but the idea behind it absolutely is.

REST is based on the principle that every notable “thing” in an application should be an addressable resource. Resources can be accessed via a single, common URI, and a simple set of operations is available to those resources. This is where REST gets interesting. Using lesser-known HTTP methods (also referred to as verbs) like PUT and DELETE in addition to the ubiquitous GET and POST, we can create an architecture where the URL points to the resource (the “thing” in question) and the HTTP method can signify the method (what to do with the “thing”).

For example, if we use the URI /speakers/5 with the method GET, this shows a representation of the speaker as an HTML document if it’s viewed in a browser. Other operations might be as shown in the following table:

URL

Method

Action

/sessions

GET

List all sessions

/sessions

POST

Add a new session

/sessions/5

GET

Show session with ID 5

/sessions/5

PUT

Update session with ID 5

/sessions/5

DELETE

Delete session with ID 5

/sessions/5/comments

GET

List comments for session with ID 5

REST isn’t useful just as an architecture for rendering web pages. It’s also a means of creating reusable services. These same URLs can provide data for an Ajax call or a completely separate application. In some ways, REST is a backlash against the more complicated SOAP-based web services, as the complexity of SOAP often brought more problems than solutions.

If you’re coming from Ruby on Rails and are smitten with its built-in REST support, you’ll be disappointed to find that ASP.NET MVC has no built-in support for REST. But due to the extensibility provided by the framework, it’s not difficult to achieve a RESTful architecture.

 

Now that you’ve learned what kind of routes you can use, let’s create some with ASP.NET MVC.

16.3. Implementing routes in ASP.NET MVC

When you first create a new ASP.NET MVC project, two default routes (shown in listing 16.2) are created with the project template. They’re defined in Global.asax.cs. These routes include an ignore route to take certain URLs out of the ASP.NET MVC pipeline and a generic dynamic route that matches the common /controller/ action/id URL pattern.

Listing 16.2. Implementing default routes

In listing 16.2, the first operation is an IgnoreRoute . We don’t want Trace.axd, WebResource.axd, and other existing ASP.NET handlers routed through the MVC Framework, so the route {resource}.axd/{*pathInfo} ensures any request coming in with an extension of .axd won’t be served by ASP.NET MVC.

The second operation defines our first route. Routes are defined by calling MapRoute on a RouteCollection , which adds a Route object to the collection. So, what comprises a route? A route has a name, a URL pattern, default values, and constraints. The latter two are optional, but you’ll most likely use default values in your routes. The route in listing 16.2 is named Default, has a URL pattern of {controller}/{action}/{id}, and includes a default value dictionary that identifies the default controller and action. These default values are specified in an anonymous type, which was introduced in .NET 3.5 and carries forward into .NET 4.

If we pick apart this route, we can easily see its components: the first segment of the URL will be treated as the controller, the second segment as the action, and the third segment as the ID. Notice how these values are surrounded in curly braces. When a URL comes in with the following format, what do you think the values will be for controller, action, and ID?

http://example.com/users/edit/5

Figure 16.3 shows how the values are pulled out of the URL. Remember, this is only the default route template. You’re free to change this for your own applications.

Figure 16.3. Decomposing a URL into route values using the default route of {controller}/{action}/{id}

The route values, shown in table 16.2, are all strings. The controller will be extracted out of this URL as users. The controller part of the class name is implied by convention, so the controller class created will be UsersController. As you can probably already tell, routes aren’t case sensitive.

Table 16.2. The route values, set to the values extracted from the URL

Name

Value

Controller

"users"

Action

"edit"

ID

"5"

The action describes the name of the method to call on our controller. In ASP.NET MVC, an action is defined as a public method on a controller that returns an ActionResult. By convention, the framework will attempt to find a method on the specified controller that matches the name supplied for the action. If none is found, it will also look for a method that has the ActionNameAttribute applied with the specified action.

The remaining values defined in a route are pumped into the action method as parameters, or left in the Request.Params collection if no method parameters match. Notice that the ID is also a string, but if your action parameter is defined as an integer, a conversion will be done for you.

Listing 16.3 shows the action method that will be invoked as a result of the URL in figure 16.3.

Listing 16.3. An action method matching http://example.com/users/edit/5
public class UsersController : Controller
{
public ActionResult Edit(int id)
{
return View();
}
}

What happens if we omit the ID or action from our URL? What will the URL http://example.com/users match? To understand this, we have to look at the route defaults. In our basic route defined in listing 16.2, we can see that our defaults are defined as

new { controller = "Home", action = "Index", id = UrlParameter.IOptional }

This allows the value of "Index" to be assumed when the value for action is omitted in a request that matches this route. You can assign a default value for any parameter in your route.

We can see that the default routes are designed to give a reasonable level of functionality for an average application, but in almost any real-world application you want to design and customize a new URL schema. In the next section, we’ll design a URL schema using custom static and dynamic routes.

16.3.1. URL schema for an online store

Now we’re going to implement a route collection for a sample website. The site is a simple store selling widgets. Using the guidelines covered in this chapter, we’ve designed the URL schema shown in table 16.3.

Table 16.3. The URL schema for sample widget store

Route number

URL

Description

1

http://example.com/

Home page; redirects to the widget catalog list

2

http://example.com/privacy

Displays a static page containing site privacy policy

3

http://example.com/<widget code>

Shows a product detail page for the relevant widget code

4

http://example.com/<widget code>/buy

Adds the relevant widget to the shopping basket

5

http://example.com/basket

Shows the current user’s shopping basket

6

http://example.com/checkout

Starts the checkout process for the current user

There’s a new kind of URL in table 16.3 that we haven’t yet discussed. The URL in route 4 isn’t designed to be seen by the user—it’s linked via form posts. After the action has processed, it immediately redirects and the URL is never seen on the address bar. In cases like this, it’s still important for the URL to be consistent with the other routes defined in the application.

So, how do we add a route?

16.3.2. Adding a custom static route

Finally, it’s time to start implementing the routes that we’ve designed. We’ll tackle the static routes first, which are the first two listed in table 16.3. Route 1 in our schema is handled by our route defaults, so we can leave that one exactly as is.

The first route that we’ll implement is number 2, which is a purely static route. Let’s look at it in listing 16.4.

Listing 16.4. A static route
routes.MapRoute("privacy_policy", "privacy",
new {controller = "Help", action = "Privacy"});

The route in listing 16.4 does nothing more than map a completely static URL to an action and controller. Effectively, it maps http://example.com/privacy to the Privacy action of the Help controller.

 

Warning

The order in which routes are added to the route table determines the order in which they’ll be searched when looking for a match. This means routes should be listed in source code from highest priority with the most specific conditions down to lowest priority, or a catchall route. This is a common place for routing bugs to appear. Watch out for them!

 

Static routes are useful when there are a small number of URLs that deviate from the general rule. If a route contains information relevant to the data being displayed on the page, look at dynamic routes.

16.3.3. Adding a custom dynamic route

Four dynamic routes are added in this section (the latter four in table 16.3). We’ll consider them two at a time.

Listing 16.5 implements routes 3 and 4. The route declaration sits directly off the root of the domain, just as the privacy route did. It doesn’t simply accept any and all values—it instead makes use of a route constraint.

Listing 16.5. Implementation of routes 3 and 4
routes.MapRoute("widgets", "{widgetCode}/{action}",
new {controller = "Catalog", action = "Show"},
new {widgetCode = @"WDG-\d{4}"});

 

Tip

If you’re planning to host an ASP.NET MVC application on IIS 6, mapping issues will cause the default routing rules not to work. For a quick fix, simply change the URLs so they have extensions such as {controller}.mvc/{action}/{id}. Chapter 6 explores this technique in greater detail.

 

The Constraints parameter in MapRoute takes a dictionary in the form of an anonymous type that can contain a property for each named parameter in the route. In listing 16.5 we’re ensuring that the request will only match if the {widgetCode} parameter starts with WDG- followed by exactly four digits. In this case, because the constraint is specified as a string, the routing engine will treat this as a regular expression. But that’s not the only way to define a route constraint. We could create our own custom constraints by implementing the IRouteConstraint interface.

 

Tip

It’s good practice to make constants for regular expressions used in routes because they’re often used to create several routes.

 

Listing 16.6 shows a controller that can handle a request that matches the route in listing 16.5.

Listing 16.6. The controller action handling the dynamic routes

Listing 16.6 shows the action implementation in the controller for the route in listing 16.5. Although it’s simplified from a real-world application, it’s straightforward until we get to the case of the widget not being found. That’s a problem. The widget doesn’t exist and yet we’ve already assured the routing engine that we’d take care of this request. Because the widget is now being referred to by a direct resource locator, the HTTP specification says that if that resource doesn’t exist, we should return HTTP 404 not found. Luckily, that’s no problem; we can just change the status code in the Response and render the same 404 view that we’ve created for the catchall route. (We’ll cover catchall routes later in this chapter.)

 

Note

You may have noticed in the previous example that we appear to have directly manipulated the HttpResponse, but that isn’t the case. The Controller base class provides us with a shortcut property to an instance of HttpResponseBase. This instance acts as a facade to the actual Http-Response but allows you to easily use a mockup if necessary to maintain testability. For an even cleaner testing experience, consider using a custom ActionResult.

 

Finally, we can add routes 5 and 6 from the schema (see table 16.3). These routes are almost static routes, but they’ve been implemented with a parameter and a route constraint to keep the total number of routes low. There are two main reasons for this. First, each request must scan the route table to do the matching, so performance can be a concern for large sets of routes. Second, the more routes you have, the higher the risk of route priority bugs appearing. A low number of route rules is easier to maintain. The regular expression used for validation in listing 16.7 is simply to stop unknown actions from being passed to the controller.

Listing 16.7. Shopping basket and checkout rules
routes.MapRoute("catalog", "{action}",
new{controller="Catalog"},
new{action=@"basket|checkout"});

We’ve now added static and dynamic routes to serve up content for various URLs in our site. What happens if a request comes in that doesn’t match any requests? In this event, an exception is thrown, which is hardly what you’d want in a real application. To handle this, we use catchall routes.

16.3.4. Catchall routes

The final route we’ll add to the sample application is a catchall route to match any URL not yet matched by another rule. The purpose of this route is to display our HTTP 404 error message. Global catchall routes, like the one in listing 16.8, will catch anything, and as such should be the last route defined.

Listing 16.8. The catchall route
routes.MapRoute("catch-all", "{*catchall}", new {controller = "Error",
action = "NotFound"});

The value catchall gives a name to the information that the catchall route picked up. You can retrieve this value by providing an action parameter with the same name.

 

Note

The usual way of handling 404 errors in an ASP.NET application is to use the custom errors section of the Web.config file to define a custom 404 page. Although this approach can still be used in an ASP.NET MVC application, catchall routes can be used to provide greater control in cases where the incoming request doesn’t match any of the registered routes.

 

The action code for the 404 error can be seen in listing 16.9.

Listing 16.9. The controller action for the HTTP 404 custom error
public class ErrorController : Controller
{
public ActionResult Notfound()
{
Response.StatusCode = 404;
return View("404");
}
}

In this example, when the Notfound action is invoked, the HTTP status code is set to 404 and we render a custom view.

The example in listing 16.8 is a true catchall route that will literally match any URL that hasn’t been caught by the higher-priority rules. It’s valid to have other catchall parameters used in regular routes, such as /events/{*info}, which would catch every URL starting with /events/. But be cautious using these catchall parameters, because they’ll include any other text on the URL, including slashes and period characters (which are usually reserved as separators for route segments). It’s a good idea to use a regular expression parameter wherever possible so you remain in control of the data being passed into your controller action, rather than just grabbing everything. Another interesting use for a catchall route is for dynamic hierarchies, such as product categories. When you reach the limits of the routing system, you can create a catchall route and do it yourself.

 

Internet Explorer’s “friendly” HTTP error messages

If you’re using Internet Explorer to develop and browse your application, be careful that you aren’t seeing Internet Explorer’s “friendly” error messages when developing these custom 404 errors, because IE will replace your custom page with its own. To avoid this, select Tools > Internet Options and deselect the Show Friendly HTTP Error Messages option under the Browsing options on the Advanced tab. Your custom 404 page should appear. Don’t forget, though, that users of your application using IE may not see your custom error pages.

 

At this point, the default {controller}/{action}/{id} route can be removed because we’ve completely customized the routes to match our URL schema. Or you might choose to keep it around to serve as a default way to access your other controllers.

We’ve now customized the URL schema for our website. We’ve done this with complete control over our URLs, and without modifying where we keep our controllers and actions. This means that any ASP.NET MVC developer can come and look at our application and know exactly where everything is. This is a powerful concept.

Next, we’ll discover how to use the routing system from within our application.

16.4. Using the routing system to generate URLs

Nobody likes broken links. And because it’s so easy to change the URL routes for your entire site, what happens if you directly use those URLs from within your application (for example, linking from one page to another)? If you changed one of your routes, these URLs could be broken. The decision to change URLs doesn’t come lightly; it’s generally believed that you can harm your reputation in the eyes of major search engines if your site contains broken links. Assuming that you may have no choice but to change your routes, you’ll need a better way to deal with URLs in your applications.

Whenever we need a URL in our site, we ask the framework to give it to us rather than hard-coding it. We need to specify a combination of controller, action, and parameters, and the ActionLink method does the rest. ActionLink is a method on the HtmlHelper class included with the MVC Framework, and it generates a full HTML <a> element with the correct URL inserted to match a route specified by the object parameters passed in. Here’s an example of calling ActionLink:

<%= Html.ActionLink("WDG0001", "show", "catalog", new { widgetCode =
"WDG-0001" }, null) %>

This example generates a link to the show action on the catalog controller with an extra parameter specified for widgetCode. Here’s the output:

<a href="/WDG-0001">WDG0001</a>

Similarly, if you use the HtmlHelper class’s BeginForm method to build your form tags, it will generate your URL for you. As you saw in the previous section, the controller and action may not be the only parameters involved in defining a route. Sometimes additional parameters are needed to match a route.

Occasionally it’s useful to be able to pass parameters to an action that hasn’t been specified as part of the route:

<%= Html.ActionLink("WDG0002 (French)", "show", "catalog",
new { widgetCode = "WDG-0002", language = "fr" }, null) %>

This example shows that passing additional parameters is as simple as adding extra members to the object passed to ActionLink. If the parameter matches something in the route, it will become part of the URL. Otherwise, it will be appended to the query string. For example, here’s the link generated by the preceding code:

<a href="/WDG-0002?language=fr">WDG0002 (French)</a>

When using ActionLink, your route will be determined for you, based on the first matching route defined in the route collection. Most often this will be sufficient, but if you want to request a specific route, you can use RouteLink, which accepts a parameter to identify the route requested, like this:

<%= Html.RouteLink("WDG0003", "special-widget-route",
new { widgetCode = "WDG-0003" }, null) %>

This code will look for a route with the name special-widget-route. You’re unlikely to need to use this technique unless the URL generated by routing isn’t the desired one. Try to solve the issue by altering route ordering or with route constraints. Use RouteLink as a last resort.

Sometimes you need to obtain a URL, but not for the purposes of a link or form. This often happens when you’re writing Ajax code and you need to set the request URL. The UrlHelper class can generate URLs directly; it’s used by the ActionLink method and others. Here’s an example:

<%= Url.Action("show", "catalog",
new { widgetCode="WDG-0002", language="fr" }) %>

This code will also return the URL /WDG-0002?language=fr but without any surrounding tags.

16.5. Testing route behavior

When compared with the rest of the ASP.NET MVC Framework, testing routes isn’t easy or intuitive because a number of abstract classes need to be mocked out before route testing is possible. Luckily, MvcContrib has a nice fluent route-testing API that we can use to make testing these routes easier.

But before we look at that, listing 16.10 demonstrates how you’d test a route with NUnit and Rhino Mocks.

Listing 16.10. Testing routes, which can be painful
using System.Web;
using System.Web.Routing;
using NUnit.Framework;
using NUnit.Framework.SyntaxHelpers;
using Rhino.Mocks;

namespace BadRoutingTestExample.Tests
{
[TestFixture]
public class NaiveRouteTester
{
[Test]
public void root_matches_home_controller_index_action()
{
const string url = "~/";
var request = MockRepository
.GenerateStub<HttpRequestBase>();
request.Stub(x => x.AppRelativeCurrentExecutionFilePath)
.Return(url).Repeat.Any();
request.Stub(x => x.PathInfo)
.Return(string.Empty).Repeat.Any();

var context = MockRepository
.GenerateStub<HttpContextBase>();
context.Stub(x => x.Request)
.Return(request).Repeat.Any();

RouteTable.Routes.Clear();
MvcApplication.RegisterRoutes(RouteTable.Routes);
var routeData = RouteTable.Routes.GetRouteData(context);

Assert.That(routeData.Values["controller"],
Is.EqualTo("Home"));
Assert.That(routeData.Values["action"],
Is.EqualTo("Index"));
}
}
}

If all our route tests looked like listing 16.10, nobody would even bother testing. Those specific stubs on HttpContextBase and HttpRequestBase weren’t lucky guesses either; it took a peek inside Red Gate’s Reflector tool to find out what to mock. This isn’t how a testable framework should behave!

Luckily, we don’t have to deal with this if we’re smart. MvcContrib’s fluent route-testing API makes everything a lot easier. Listing 16.11 is the same test, using MvcContrib.

Listing 16.11. Cleaner route testing with MvcContrib’s TestHelper project

This is all done with the magic and power of extension methods and lambda expressions. Inside MvcContrib there’s an extension method on the string class that builds up a RouteData instance based on the parameters in the URL. The RouteData class has an extension method to assert that the route values match a controller and action .

You can see from listing 16.11 that the controller comes from the generic type argument to the ShouldMapTo<TController>() method. The action is then specified with a lambda expression. The expression is parsed to pull out the method call (the action) and any arguments passed to it. The arguments are matched with the route values. See the code for yourself on the MvcContrib site: http://mng.bz/rHBX.

Now it’s time to apply this to our widget store’s routing rules and make sure that we’ve covered the desired cases. We do that in listing 16.12.

Listing 16.12. Testing our example routes

Each of these simple test cases uses the NUnit testing framework. They also use the ShouldMapTo<T> extension method found in MvcContrib.TestHelper.

 

Note

In listing 16.12, we’ve separated each rule into its own test. It might be tempting to keep all these one-liners in a single test, but don’t forget the value of understanding why a test is failing. If you make a mistake, only distinct tests will break, giving you much more information than a single broken test_all_routes() test.

 

After running this example, we can see that all our routes are working properly. Figure 16.4 shows the ReSharper test runner results (the output may look slightly different depending on your testing framework and runner).

Figure 16.4. The results of our route tests in the ReSharper test runner

Armed with these tests, we’re free to make some refactorings or clean up our route rules, confident that we aren’t breaking existing URLs on our site. Imagine if product links on Amazon.com were suddenly broken due to a typo in some route rule... Don’t let that happen to you. It’s much easier to write automated tests for your site than it is to do manual exploratory testing for each release.

There’s an important facet of route testing that we’ve paid little attention to so far: outbound routing. As defined earlier, outbound routing refers to the URLs that are generated by the framework, given a set of route values. Helpers for testing outbound route generation are also included as part of the MvcContrib project.

Now that you’ve seen a complete example of realistic routing schemas, you’re prepared to start creating routes for your own applications. You’ve also seen some helpful unit-testing extensions to make unit testing inbound routes much easier. We haven’t yet mentioned that all this routing goodness is available to Web Forms projects as well!

16.6. Using routing with existing ASP.NET projects

The URL problems discussed at the start of this chapter (URLs tied directly to files on disk, no ability to embed dynamic content in the URL itself, and so on) can affect all websites and applications, and although you may not be in a position to adopt a full MVC pattern for an application, you should still care about your application’s URL usability. System.Web.Routing is a separate assembly released as part of .NET 3.5 SP1, and as you might guess, it’s available for use in Web Forms as well. With .NET 4, routing is rolled up into System.Web.dll and is available to any flavor of ASP.NET automatically.

Luckily, by importing the UrlRoutingModule from the System.Web.Routing assembly, we can use the routing mechanism from the MVC Framework in existing ASP.NET Web Forms applications. To get started, open an existing ASP.NET Web Forms project and add the lines from listing 16.13 in the assemblies and httpModules sections in your Web.config. If you’re deploying to IIS 7, you’ll also need the configuration in listing 16.14.

Listing 16.13. Configuration for the UrlRoutingModule

Listing 16.14. Configuration for IIS 7 Integrated mode

Next, we need to define a custom route handler that will—you guessed it—handle the route. You may have a custom route handler for each route, or you might choose to make it more dynamic. It’s entirely up to you.

Defining the route is similar to what we saw earlier, except that there are no controllers or actions to specify. Instead, you just specify a page. A sample route for Web Forms might look like this:

RouteTable.Routes.Add("ProductsRoute", new Route
(
"products/apparel",
new CustomRouteHandler("~/Products/ProductsByCategory.aspx",
"category=18")
));

The custom route handler simply needs to build the page. Listing 16.15 shows a bare-bones handler that will work.

Listing 16.15. A simple custom route handler

Now, requests for /products/apparel will end up being served by the ProductsByCategory.aspx page.

 

Note

When using UrlRoutingModule to add routing capabilities to your Web Forms application, you’re essentially directing traffic around parts of the normal ASP.NET request-processing pipeline. This means that it’s possible that the normal URL-based authorization features of ASP.NET can be circumvented. Even if users don’t have access to a particular page, they can view it if the CustomRouteHandler doesn’t implement authorization checking or if the route isn’t listed in the authorization rules in Web.config. Although the complete implementation is outside the scope of this text, you can use the UrlAuthorizationModule.CheckUrlAccessForPrincipal() method to verify that a user has access to a particular resource.

 

16.7. Summary

In this chapter, you learned how the routing module in the ASP.NET MVC Framework gives you virtually unlimited flexibility when designing routing schemas to implement both static and dynamic routes. Best of all, the code needed to achieve this is relatively insignificant.

Designing a URL schema for an application is the most challenging thing we’ve covered in this chapter, and there’s never a definitive answer as to what routes should be implemented. Although the code needed to generate routes and URLs from routes is simple, the process of designing that schema isn’t. Ultimately every application will apply the guidelines in a unique manner. Some people will be perfectly happy with the default routes created by the project template, whereas others will have complex, custom route definitions spanning multiple C# classes.

You learned that the order in which routes are defined determines the order they’re searched when a request is received, and that you must carefully consider the effects of adding new routes to the application. As more routes are defined, the risk of breaking existing URLs increases. Your insurance against this problem is route testing. Although route testing can be cumbersome, helpers like the fluent route-testing API in MvcContrib can certainly help.

The most important thing to note from this chapter is that no application written with the ASP.NET MVC Framework should be limited in its URLs by the technical choices made by source code layout—and that can only be a good thing! Separation of the URL schema from the underlying code architecture gives ultimate flexibility and allows you to focus on what would make sense for the user of the URL rather than what the layout of your source code requires. Make your URLs simple, hackable, and short, and they’ll become an extension of the user experience for your application.

In the next chapter, you’ll see some advanced deployment concepts for your ASP.NET MVC applications.