Quantcast
Channel: typesafe » .Net
Viewing all articles
Browse latest Browse all 10

Mapping ASP.Net MVC Routes With Expressions – Automatic Constraints

$
0
0

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);


Viewing all articles
Browse latest Browse all 10

Trending Articles