MVC Route Constraints
May 31, 2011 Leave a comment
I started to dig into MVC3 routing a bit more over the weekend. I came across some routing constraints and realized that there is a clear progression. I created an out of the box MVC3 web application (I am using Razor) and then added a Product Controller with the default methods and 1 View for the Details method. I made 1 change to the out of the box convention – I changed the name of the int parameter to the Details method to productId.
The Details controller method pushes the parameter back out to the View:
public ActionResult Details(int productId) { return View(productId); }
and the view parrots the productId back to the user:
@{ ViewBag.Title = "Details"; Layout = "~/Views/Shared/_Layout.cshtml"; } <h2>Details</h2> <br /> You Entered = @Model
I then dropped into Global.asax to work with the constraints. The first thing I did was to add a route to the routing table to account for the productid parameter:
routes.MapRoute( "Product", "Product/{productId}", new { controller = "Product", action = "Details" } );
This worked fine to a point. Product/1 resolved:
but Product/foo was allowed by the routing engine and the method threw an error:
To handle this malformed parameter, I added a regular expression to the constraint definition (using Stephen Walter’s blog post as an example:
routes.MapRoute( "ProductWithConstraint", "Product/{productId}", new { controller = "Product", action = "Details" }, new { productId = @"\d+" } );
Now, when Product/foo comes in, I got a 404.
The next scenario I wanted to handle was that only some integers are allowed as a parameter. For example, perhaps only products that are in stock are allowed – which means that you need to do a database call before defining the route constraint definition. To that end, I created a new class that inherited from IRouteConstraint (based on Yuri Nayyeri blog post):
public class ProductRouteConstraint: IRouteConstraint { public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection) { if ((routeDirection == RouteDirection.IncomingRequest) && (parameterName.ToLower(CultureInfo.InvariantCulture) == "productid")) { int productId = 0; try { productId = Convert.ToInt32(values["productId"]); if (productId > 0 && productId < 10) { return true; } } catch (FormatException formatException) { return false; } } return false; } }
I then added this constraint to the routing table:
routes.MapRoute( "ProductWithConstraintClass", "Product/{productId}", new { controller = "Product", action = "Details" }, new { productId = new ProductRouteConstraint() } );
And I got the desired result – products less than 0 or greater than 10 returned a 404.
So the progression in my mind is:
- No constraints
- Reg Ex constraints in the Global.asax
- Create a class that implements IRouteConstraint
I my mind, I would rather skip #2 and go to #3 altogether. Having all of the logic encapsulated in 1 class seems cleaner and not having the Global.asax mucked up with custom defs makes for a more maintainable solution.