Craig Rowe

Techlead / Developer

6th December 2009

Cargowire MVC: Routing

  1. URL Design
  2. Useful IRouteConstraints
  3. Conclusion
  4. N.B. Mini MVC Route Guide

Cargowire is now fully .NET MVC. Although it looks the same, I've been able to take advantage of the new (ish) MS implementation of the well known MVC pattern and surrounding technologies. During the changeover I wanted to be sure that my urls remained the same.

URL Design

When I first created cargowire I wanted to ensure my urls were 'nice'. That is to say they stayed around, they were short, they were obvious and they were extensionless urls.

Cargowire itself is a relatively short domain name and my main site sections were as follows:

  1. /blog
    1. /blog/2
    2. /blog/dotnetdev101
  2. /articles
    1. /articles/2
    2. /articles/dateformattinginxml
  3. /about
  4. /notes

This was originally implemented using a third party library (Intelligencia.UrlRewriter) running as an HttpModule. Intelligencia used a rather traditional set of regex based rules in an XML configuration file (in this case, web.config). So for example the following

              <rewrite url="~/blog/([0-9]+)" to="~/default.aspx?p=$1" processing="stop" />
            
would match the pagination urls for the blog i.e. /blog/2, /blog/3 and rewrite that to /default.aspx?p=2, /default.aspx?p=3.

This method was pretty standard, among other third party approaches like the ISAPI Filter 'ISAPI Rewrite'. However .NET 3.5 SP1 contains the System.Web.Routing.UrlRoutingModule. With this library we are able to create routes in the following way and add them to a RouteTable:

            protected void Application_Start()
            {
                // Via RouteCollection.Add
                RouteTable.Routes.Add(new Route {
                  Url = "{controller}/{action}/{id}",
                  Defaults = new { action = "Index", id = (string)null },
                  RouteHandler = typeof(MvcRouteHandler)
                });

              // Or via RouteCollection.MapRoute
              RouteTable.Routes.MapRoute("DefaultRoute", "{controller}/{action}/{id}", new { action = "Index", id = (string)null }, null);
            }
          

This RouteTable is statically available, and assigned to within the Application_Start event of your global.asax. When a request comes in the table is traversed and the first match found is used. For more info see the bottom of the page for a mini routing guide

One key difference here is that instead of using regular expressions the url is defined by a tokenized string. This meant that it was possible that my pagination and details routes would clash i.e. {controller}/{action}/{page} and {controller}/{action}/{detailsuri}. To avoid this I had two options:

Constraints (IRouteConstraint)
The MapRoute method takes an object with corresponding IRouteConstraints per route parameter. This means I could do something like:
routes.MapRoute("DefaultWithPage", "{controller}/{page}", new { controller = "Blog", action = "index", page = 1 }, new { page = new IntegerConstraint() });
An IntegerConstraint may then look like:
                public class IntegerConstraint : IRouteConstraint
                {
                  public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
                  {
                    string value = values[parameterName].ToString();
                    int parsedInt = 0;
                    return int.TryParse(value, out parsedInt); // True if the value is an integer
                  }
                }
              
Constraints (regex string)
The IRouteConstraint is fine but could perhaps be seen as overkill. Rather than unnecessarily creating a whole new class implementing IRouteConstraint we can instead pass regular expression strings such as: routes.MapRoute("DefaultRootWithPageAndFormat", "{page}", new { controller = "Blog", action = "index", uri = "" }, new { page = "[0-9]+" });

The much more concise approach of the regex was appropriate here. However constraints can be useful for other more complicated scenarios.

Useful IRouteConstraints

IRouteConstraints could provide an injection point for user defined shortcut checking, such as when '/septemberdeals' is created as a shortcut to the longer '/shop/discounts/2009-09' url. This could easily be a CMS requirement:

"The user can create on the fly direct links for publication or promotion purposes - for example '/septemberdeals' resolves to '/shop/discounts/2009-09' and so on."

Of course, a short url would normally map to a default controller route e.g. '/{controller}' with '/septemberdeals' trying to find a 'SeptemberDeals' controller.

So how about a Constraint that when it receives a request goes away and compares it to a list of known shortcuts (hopefully stored in memory at this point rather than incurring a database hit) returning false if none exist, allowing the route engine to continue down to the less specific '/{controller}' route. When a shortcut is found the route will match and the later '/controller' route will not be compared - avoiding an error when the 'septemberdeals' controller is not found.

I had a similar issue for cargowire, the main content mapped well to useful controllers. It was clear that an articles controller would be useful. It could interact with an articles service/respository layer and manage listings and details views based on the data. However some pages such as 'notes' were text content written directly to file, with arguably no real need for a full controller class.

On the prior version of cargowire I had a 'notes.aspx' and 'xsl.aspx' page that did nothing more complicated than output some words I had hard written straight in. To perform this action in the MVC version I made use of a FromValuesConstraint:

            /// 
            /// Originally sourced from: http://blogs.microsoft.co.il/blogs/bursteg/archive/2009/01/11/asp-net-mvc-route-constraints.aspx
            /// 
            public class FromValuesListConstraint : IRouteConstraint
            {
              private string[] _values;
              public FromValuesListConstraint(params string[] values) { this._values = values; }

              public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
              {
                // Get the value from the RouteValueDictionary
                string value = values[parameterName].ToString().ToLower();

                // Return true is the list of allowed values contains this value.
                return ((IList)_values).Contains(value);
              }
            }
          

This enabled me to create a PageController that merely output a view directly by name. The FromValuesListConstraint could then guarantee that the route only matched when there was a 'hard written' view to be found.

I could either put the FromValuesListConstraint on the '/{controller}' route with a list of known controllers, or on the PageController route with a list of the 'hard written' views. I ended up opting for the former, allowing me to easily add hard written views ad hoc without the need for a recompile. However I could just have easily done it the other way round, or implemented a more complex constraint such as one that used reflection to identify the list of controllers available or that read the view directory to find available views.

Conclusion

Constraints allow for powerful methods of route matching, far in excess of the limitations of using only regex for matching. By introducing code into the mix, in the form of constraints, any number of application scenarios can result in a route matching or not matching. A helpful resource when doing this is Phil Haack's Routing Debugger tool that allows for easy testing of routes including applying any associated constraints (see the related links below).


N.B. Mini MVC Route Guide

Routing in MVC works in the following way:

  1. Routes are defined in code and added to the static route collection
  2. When a url is requested the route table is traversed. The first route that matches is processed.
  3. The 'controller' parameter, by convention, maps to the instantiation of a controller class that should exist in your code e.g. a route '{controller}' with a request '/blog' would lead to the instantiation of a 'BlogController' class.
  4. The 'action' parameter, by convention, is used to call the appropriate method on that class with the remaining route parameters matching up to the methods parameters e.g.
    Route Request Matches
    {controller}/{action}/{page} /blog/index/2 The Index method of the BlogController, passing 2 as the int page parameter
  5. The defaults allow certain parts of the route to be missing e.g.
    Route Defaults Request Matches
    {controller}/{action}/{page} defaults = new { action = "index" } /blog/2 Will also match the '{controller}/{action}/{page}' and the BlogController index method
backback to top

Sources / Related Links


All article content is licenced under a Creative Commons Attribution-Noncommercial Licence.