Defining Routes using Regular Expressions in ASP.NET MVC
Author: Fredrik Kalseth
Having played about with the recently released beta 2 of the ASP.NET MVC framework, I was kind of disappointed by the still fairly restrictive routing abilities of the framework. Currently, setting up a route for a request is done using a tokenized string, allowing each token to map to a controller, action and parameters on the action. This works fine in most situations, but can be frustratingly restrictive in others.
In a post detailing the routing system of the framework, Scott Guthrie also answered a comment which asked why they weren’t using regular expressions for defining routes by claiming that only a subset of people really know how to use them. He did however allow for the possibility that it might be added as an option later – but why wait when we can implement it ourselves? Here’s my quick and dirty RegexRoute class:
public class RegexRoute : Route
{
private readonly Regex _urlRegex;
public RegexRoute(string urlPattern, IRouteHandler routeHandler)
: this(urlPattern, null, routeHandler)
{}
public RegexRoute(string urlPattern, RouteValueDictionary defaults, IRouteHandler routeHandler)
: base(null, defaults, routeHandler)
{
_urlRegex = new Regex(urlPattern, RegexOptions.Compiled);
}
public override RouteData GetRouteData(HttpContextBase httpContext)
{
string requestUrl = httpContext.Request.AppRelativeCurrentExecutionFilePath.Substring(2) + httpContext.Request.PathInfo;
Match match = _urlRegex.Match(requestUrl);
RouteData data = null;
if(match.Success)
{
data = new RouteData(this, this.RouteHandler);
// add defaults first
if (null != this.Defaults)
{
foreach (KeyValuePair<string, object> def in this.Defaults)
{
data.Values = def.Value;
}
}
// iterate matching groups
for (int i = 1; i < match.Groups.Count; i++)
{
Group group = match.Groups;
if (group.Success)
{
string key = _urlRegex.GroupNameFromNumber(i);
if (!String.IsNullOrEmpty(key) && !Char.IsNumber(key, 0)) // only consider named groups
{
data.Values = group.Value;
}
}
}
}
return data;
}
}
Incidentally, a great «side effect» of having the route defined as a regular expression is that it allows us to perform validation in-place, which kind of renders the concept of Constraints unnecessary.
Here’s a quick example route defined as a regular expression:
routes.Add(new RegexRoute(@"^Books/((?<ssn>{3}(-?){2}\1{4})|(?<query>.+)?)$", new MvcRouteHandler())
{
Defaults = new RouteValueDictionary(new { controller = "Book", action = "Find" })
});
Now the route above could, and possibly should, have been split into two different routes; one for finding books by SSN and a different one for searching book titles, but I wanted to demonstrate the flexibility gained by using regular expressions. With the above route, urls like «mysite.com/Books/Last+Argument+Of+Kings» and «mysite.com/Books/0575077905» both map to the Find action on the BookController class, with the appropriate parameter initialized:
public class BookController : Controller
{
public void Find(string query, int? ssn)
{
// ... gets the book by ssn if present, otherwise searches using the query
}
}
Even with regular expression support, I’m still not 100% happy with the routing; it won’t allow me to overload controller actions. In the above example, for instance, I would much rather have two separate Find(string query) and Find(int ssn) methods, and then have the correct one invoked depending on whether the query or SSN parameter was present in the route – but the MvcRouteHandler doesn’t know how to resolve that and just throws an exception. Look out for another post investigating the implementation of a custom route handler for working around that, soon 🙂
This post is also available in: English