Techniques to Personalize Your URLs Using ASP.NET MVC
Control URL structure through constraints and logic that are recognized by ASP.NET MVC applications
June 22, 2011
ASP.NET MVC gives developers full control over the structure of the URLs that are used to access their web applications. In ASP.NET Web Forms, the URL is mostly a reference to a physical file that lives on the web server. The ASP.NET runtime maps the server part of the URL to a server directory, and it uses everything that follows the server part to compose the actual path for the ASPX resource. We could say that, in Web Forms, the user asks for some content and receives whatever the system returns. In ASP.NET MVC, by contrast, the URL is more like a command that the user sends to the web server. In other words, ASP.NET MVC lets the user tell instead of just ask.
Technically speaking, you don't have to switch to ASP.NET MVC if the only change you seek is the ability to use personalized URLs. By writing a bunch of custom HTTP handlers and binding them to ad hoc URLs, you can achieve the same effect of using handcrafted URLs. On the other hand, this is just the mechanism that ASP.NET MVC leverages, and it's what makes ASP.NET MVC work on top of the same runtime environment as classic ASP.NET.
Routing is the technology that lets you gain total control over URLs. Routing is fully integrated both in ASP.NET MVC and ASP.NET 4. In Web Forms, the routing engine maps a recognized URL only to an ASPX page, whereas in MVC, the routing engine maps any recognized URL to a pair represented by a controller name and an action name. The routing related extensions that you find in ASP.NET MVC provide a good deal of control and programming power.
Routing in Action
To exercise control over incoming URLs, older versions of ASP.NET have a feature named URL rewriting. At its core, URL rewriting consists of hooking up a request, parsing the original URL, and instructing the HTTP runtime environment to serve a "possibly related but different" URL. ASP.NET MVC and newer versions of ASP.NET Web Forms rely on the URL-routing HTTP module for this service.
The URL-routing HTTP module intercepts any URL, parses the content, and dispatches the request to the most appropriate executor. Additionally, it denies the request if the URL doesn't match any predefined pattern.
Remember that in ASP.NET MVC, users make requests for actions on resources. However, the MVC framework doesn't mandate the syntax for describing resources and actions. The expression "acting on resources" might make you think of Representational State Transfer (REST). ASP.NET MVC is loosely REST-oriented in that it does acknowledge concepts such as resource and action, but it leaves you free to use your own syntax to express and implement resources and actions.
Your personalized URL syntax is expressed through a collection of URL patterns, also known as routes. A route is a string that represents the URL string without including protocol, server, or port information. A route may be a constant string, but it will more likely contain a few placeholders. Here's an example of a route:
/home/about
This route is a constant string, and it's matched only by URLs whose absolute path is /home/about. Most of the time, though, you deal with parametric routes that incorporate one or more placeholders. A parameter is identified by a string enclosed in curly brackets, as follows:
/{resource}/{action}
/Customer/{action}
Both routes are matched by any URLs that contain exactly two segments in addition to server information. The second segment requires that the first segment also equals the Customer string. By contrast, the first segment doesn't pose specific constraints on the content of the segments. The name of the placeholder ({action} in this example) is the key that your code uses to programmatically retrieve the content of the corresponding segment from the actual URL. Here's the default route for an ASP.NET MVC application:
{controller}/{action}/{id}
The route contains three placeholders separated by the slash (/) delimiter. The following URL matches this default route:
/Customers/Edit/ALFKI
You can add routes that have as many placeholders as is appropriate, and you can add as many of these routes as you want. You can even remove the default route from an ASP.NET MVC application.
Defining Routes
Routes are usually registered in the global.asax file and processed at the application's startup. A route is characterized by a few attributes, such as name, URL pattern, default values, constraints, data tokens, and route handler. The attributes that you most commonly set are name, URL pattern, and default values.
Figure 1 shows the typical code that you use to register a valid route for an application. The code is an excerpt from the global.asax file in the default ASP.NET MVC project.
Supported routes must be added to a static collection of route objects that are managed by ASP.NET MVC. You typically use the handy MapRoute extension method to populate the collection, as shown in Figure 1. The MapRoute method offers a variety of overloads, and it works well most of the time. However, this method doesn't let you configure every possible aspect of a route object. If, in the process of setting values on a route, you have to set something that MapRoute doesn't support, then you could resort to using the following code:
// Create a new route and add it to the system collection
var route = new Route(...);
RouteTable.Routes.Add("NameOfTheRoute", route);.
In the Figure 1 code, notice that the first parameter is the name of the route; each route should have a unique name. The second parameter is the URL pattern, which can include as many parameters as you need. The third parameter is an object that specifies default values for the URL parameters. Default values are used in case the parser can't match a declared URL parameter to any segment of the actual URL.
Default Values and Optional URL Parameters
Omitting default values may sometimes have unpleasant side effects. If you don't specify a default value, the URL parser assumes that any declared parameter is required. If the URL parser can't match one of the URL parameters, it just skips the route. If, instead, you provide a default value, then you provide more information to the parser, which may resolve the URL to a different route. You can also consider a given parameter as optional. In this scenario, an optional parameter is a parameter for which the URL may or may not contain a value. If the URL does not contain a value, the parameter must go undefined.
I'll review the effects of these rules by offering a few examples. Routes are checked in the order in which they appear in global.asax. To make sure that routes are recognized as appropriate, you must list them from the most specific to the least specific. In any case, keep in mind that the search for a matching route always ends at the first match. Suppose, for example, that you have the following routes in the given order:
{Orders}/{Year}/{Month}
{Orders}/{Year}
If you assign default values for both {Year} and {Month} in the first route, the second route is never evaluated. This is because, thanks to the default values, the first route is always a match regardless of whether the URL specifies a year and a month. Furthermore, note that a trailing slash (/) is also a pitfall. "{Orders}/{Year}" and "{Orders}/{Year}/" are two very different things. One won't match the other even though, logically (at least from a user's perspective), you'd expect them to. In other words, if you list both routes, they will be treated separately.
Route Static Constraints
The list of constraints that you optionally define for a route represents another factor that influences how URLs are matched to routes. A route constraint is a stronger condition that you impose on incoming URLs than just matching the pattern. Without constraints, any URL whose composition is compatible with the URL pattern is accepted. By imposing a constraint, you restrict the range of acceptable URLs to only those that contain specific data. A constraint can be defined in various ways, including through a regular expression. Here's a sample route that has constraints:
routes.MapRoute(
"ProductInfo",
"{controller}/{productId}/{locale}",
new { controller = "Product", action = "Index", locale="en-us" },
new { productId = @"d{8}",
locale = ""[a-z]{2}-[a-z]{2}" });
This route is composed of three blocks: controller, product ID, and locale. Without further restrictions, the following URL would work:
/dino/does/mobile
You'll still probably receive a 404 error message for this URL because it's unlikely that you have a dinoController class. But consider the following:
/product/iphone4/italy
This makes a lot more sense, and it will likely trigger an action on the product controller. If you expect the URL to trigger a search on products in a specific local market, you might want to stop invalid URLs at the gate by adding static constraints on some parameters. In particular, the route requires that the productId placeholder must be a numeric sequence of exactly eight digits, whereas the locale placeholder must be a pair of two-letter strings separated by a hyphen. Constraints don't ensure that all invalid product IDs and locale codes are stopped at the gate. But they do, at least, save you a good deal of work.
Route Handlers and Dynamic Constraints
A route minimally defines a set of rules, according to which the routing module decides whether or not the incoming request URL is acceptable to the application. The component that ultimately decides how to remap the requested URL is the route handler.
The route handler is an object that processes any requests that match a given route; it also decides about the HTTP handler that will ultimately serve the request. A route handler is a class that implements the IRouteHandler interface, as follows:
public interface IRouteHandler
{
IHttpHandler GetHttpHandler(RequestContext requestContext);
}
The ASP.NET MVC framework doesn't offer many built-in route handlers. This is probably a sign that the need for a custom route handler is not all that common. Yet, the extensibility point exists, and you can take advantage of it if you have to.
When might you need a custom route handler? Regardless of how you define your URL patterns, any request must always be resolved in terms of a controller name and an action name. This is one of the pillars of ASP.NET MVC. The controller name is automatically read from the URL if the URL includes a {controller} placeholder. The action name is read from an {action} placeholder.
However, it's still acceptable to have completely custom URLs that are devoid of such placeholders. In this case, it's your responsibility to indicate controller and action through default values, as follows:
routes.MapRoute(
"SampleRoute",
"about",
new { controller = "Home", action = "About"}
);
If controller and action names (and other details) can't be resolved in a static way, you might want to take the following steps: Write a custom route handler, explore the details of the request, and figure out the various aspects of an ASP.NET MVC request, such as controller name, action name, and route handler. Figure 2 shows a sample route handler that programmatically determines controller and action names for a constant URL, such as /about.
For a route that requires a custom handler, the registration process is a bit different. Here's the code you must use in RegisterRoutes:
public static void RegisterRoutes(RouteCollection routes)
{
var aboutRoute = new Route("about", new AboutRouteHandler());
routes.Add("SampleAboutRoute", aboutRoute);
:
}
When you add a route based on a custom route handler that programmatically sets controller and action names, you may run into trouble with the links that are generated by the Html.ActionLink helper. You typically use this helper to create route-based links for menus and for other visual elements of the user interface. If you add a route that has a custom handler (and sensibly put it before the standard MVC route), you may be surprised to see that ActionLink fails to get you the right URL for simple pairs such as Home/Index. Figure 3 shows this effect.
Figure 3: Subtle effects of uncontrolled routing
ActionLink gets controller and action parameters, and it works through the list of registered routes to try to figure out which one can host those parameters. A constant URL, such as /about, declares nothing about controller/action in the route table. Therefore, it implicitly tells the routing component that it can accommodate any value for controller/action. That's why a constant URL is used in the manner shown in Figure 3.
To resolve this issue, you can change ActionLink by using RouteLink and, thereby, expressly indicate which route you want the URL to be created after. Or, better yet, you can specify in the custom route that controller and action are optional parameters—or you can provide default values for them.
The Route Forward
In modern web coding, the URL is more an expression of an action than a reference to a web page. Through this shift, we see the results of adopting the "tell-don't-ask" paradigm, which leads to cleaner and better designed code. Through the use of routes, you indicate which URLs your application will accept, and you decide what shape to give them.
Dino Esposito, author of Programming Microsoft ASP.NET MVC (Microsoft Press), is a trainer and consultant specializing in web architectures and effective code design principles and practices. Get in touch via weblogs.asp.net/despos.
About the Author
You May Also Like