I’m just done updating my expression routing extensions class: First I’ve added support for extraction default values from Nullable
parameters (oh boy, how could I miss that one!). Second, and more interestingly maybe, I’ve added support for automatically extracting constraints.
Let’s say you’ve got this route:
routes.MapRoute<ProductsController>( "brands/{title}-{brandId}/{pageIndex}", c => c.Brands(0, 0), cultureNames, null, new { brandId = @"\d+", pageIndex = @"\d+" });
The two regex values are there to ensure that request for ~/brands/somebrand-1/foo
or ~/brands/somebrand-bar/1
are never even forwarded to the action method. It is part of the contract and does not make sense. (I have setup my routing in such a way that the final fallback/wildcard route maps to a content management module that renders a ‘page not found’ if the slug cannot be found in the database.)
If you take a look at the action method Brands(int, int?)
you’ll see that it is perfectly possible to deduct these constraints from the expression. I decided to add support for this as follows:
- Explicit constraints (passed to the MapRoute method), always win
- If the type of the parameter is
IConvertible
(including nullable types), I automatically add a constraint - Strings parameters are ignored
Just like the GetActionDefaults
, I’ve added a GetActionConstraints
, that parses the method call as follows:
private static RouteValueDictionary GetActionConstraints(MethodCallExpression call, RouteValueDictionary constraints) { if (constraints == null) constraints = new RouteValueDictionary(); foreach (var parameter in call.Method.GetParameters()) { // if there's an explicit constraint, keep it, if it's a string, just ignore it if(constraints.ContainsKey(parameter.Name) || parameter.ParameterType == typeof(string)) continue; var converter = TypeDescriptor.GetConverter(parameter.ParameterType); if(converter != null && converter.CanConvertFrom(typeof(string))) { constraints.Add(parameter.Name, new ConversionTestConstraint(converter)); } return constraints; } }
The method adds a ConversionTestConstrating
that takes the specific converter (we need this, since we can’t be sure we will be able to determine the type (or converter) the moment the constraint is called.
The ConversionTestConstraint is somewhat ugly, I admit, but in my defense: I have to catch a general Exception because most TypeConverters are implemented badly (in my opinion). The conversion will rarely fail, so I guess it’s not the end of the world, but still.
public class ConversionTestConstraint : IRouteConstraint { private readonly TypeConverter converter; public ConversionTestConstraint(TypeConverter converter) { this.converter = converter; } public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection) { if (!values.ContainsKey(parameterName)) return true; try { converter.ConvertFrom(values[parameterName]); return true; } catch (Exception) // WTF?! ConvertFrom throws System.Exception with FormatException as InnerException { return false; } } }
Now I can simply put the following, with the same result:
routes.MapRoute<ProductsController>( "brands/{title}-{brandId}/{pageIndex}", c => c.Brands(0, 0), cultureNames);
