6th December 2009
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.
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:
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:
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 } }
The much more concise approach of the regex was appropriate here. However constraints can be useful for other more complicated scenarios.
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.
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).
Routing in MVC works in the following way:
Route | Request | Matches |
---|---|---|
{controller}/{action}/{page} | /blog/index/2 | The Index method of the BlogController, passing 2 as the int page parameter |
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 |
All article content is licenced under a Creative Commons Attribution-Noncommercial Licence.