Support for InlineConstraints in Route Template

This commit is contained in:
harshgMSFT 2014-05-16 17:52:36 -07:00
parent bde341caed
commit 187d8e4911
22 changed files with 711 additions and 83 deletions

View File

@ -0,0 +1,61 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using Microsoft.AspNet.Http;
namespace Microsoft.AspNet.Routing.Constraints
{
/// <summary>
/// Constrains a route by several child constraints.
/// </summary>
public class CompositeRouteConstraint : IRouteConstraint
{
/// <summary>
/// Initializes a new instance of the <see cref="CompositeRouteConstraint" /> class.
/// </summary>
/// <param name="constraints">The child constraints that must match for this constraint to match.</param>
public CompositeRouteConstraint([NotNull] IEnumerable<IRouteConstraint> constraints)
{
Constraints = constraints;
}
/// <summary>
/// Gets the child constraints that must match for this constraint to match.
/// </summary>
public IEnumerable<IRouteConstraint> Constraints { get; private set; }
/// <summary>
/// Calls Match on the child constraints.
/// The call returns as soon as one of the child constraints does not match.
/// </summary>
/// <param name="httpContext">The HTTP context associated with the current call.</param>
/// <param name="route">The route that is being constrained.</param>
/// <param name="routeKey">The route key used for the constraint.</param>
/// <param name="values">The route value dictionary.</param>
/// <param name="routeDirection">The direction of the routing,
/// i.e. incoming request or URL generation.</param>
/// <returns>True if all the constraints Match,
/// false as soon as one of the child constraints does not match.</returns>
/// <remarks>
/// There is no guarantee for the order in which child constraints are invoked,
/// also the method returns as soon as one of the constraints does not match.
/// </remarks>
public bool Match([NotNull] HttpContext httpContext,
[NotNull] IRouter route,
[NotNull] string routeKey,
[NotNull] IDictionary<string, object> values,
RouteDirection routeDirection)
{
foreach (var constraint in Constraints)
{
if (!constraint.Match(httpContext, route, routeKey, values, routeDirection))
{
return false;
}
}
return true;
}
}
}

View File

@ -0,0 +1,39 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Globalization;
using Microsoft.AspNet.Http;
namespace Microsoft.AspNet.Routing.Constraints
{
/// <summary>
/// Constrains a route parameter to represent only 32-bit integer values.
/// </summary>
public class IntRouteConstraint : IRouteConstraint
{
/// <inheritdoc />
public bool Match([NotNull] HttpContext httpContext,
[NotNull] IRouter route,
[NotNull] string routeKey,
[NotNull] IDictionary<string, object> values,
RouteDirection routeDirection)
{
object value;
if (values.TryGetValue(routeKey, out value) && value != null)
{
if (value is int)
{
return true;
}
int result;
string valueString = Convert.ToString(value, CultureInfo.InvariantCulture);
return Int32.TryParse(valueString, NumberStyles.Integer, CultureInfo.InvariantCulture, out result);
}
return false;
}
}
}

View File

@ -7,7 +7,7 @@ using System.Globalization;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Microsoft.AspNet.Http; using Microsoft.AspNet.Http;
namespace Microsoft.AspNet.Routing namespace Microsoft.AspNet.Routing.Constraints
{ {
public class RegexConstraint : IRouteConstraint public class RegexConstraint : IRouteConstraint
{ {

View File

@ -0,0 +1,146 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using Microsoft.AspNet.Routing.Constraints;
namespace Microsoft.AspNet.Routing
{
/// <summary>
/// The default implementation of <see cref="IInlineConstraintResolver"/>. Resolves constraints by parsing
/// a constraint key and constraint arguments, using a map to resolve the constraint type, and calling an
/// appropriate constructor for the constraint type.
/// </summary>
public class DefaultInlineConstraintResolver : IInlineConstraintResolver
{
private readonly IDictionary<string, Type> _inlineConstraintMap = GetDefaultConstraintMap();
/// <summary>
/// Gets the mutable dictionary that maps constraint keys to a particular constraint type.
/// </summary>
public IDictionary<string, Type> ConstraintMap
{
get
{
return _inlineConstraintMap;
}
}
private static IDictionary<string, Type> GetDefaultConstraintMap()
{
return new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase)
{
// Type-specific constraints
{ "int", typeof(IntRouteConstraint) },
};
}
/// <inheritdoc />
/// <example>
/// A typical constraint looks like the following
/// "exampleConstraint(arg1, arg2, 12)".
/// Here if the type registered for exampleConstraint has a single constructor with one argument,
/// The entire string "arg1, arg2, 12" will be treated as a single argument.
/// In all other cases arguments are split at comma.
/// </example>
public virtual IRouteConstraint ResolveConstraint([NotNull] string inlineConstraint)
{
string constraintKey;
string argumentString;
int indexOfFirstOpenParens = inlineConstraint.IndexOf('(');
if (indexOfFirstOpenParens >= 0 && inlineConstraint.EndsWith(")", StringComparison.Ordinal))
{
constraintKey = inlineConstraint.Substring(0, indexOfFirstOpenParens);
argumentString = inlineConstraint.Substring(indexOfFirstOpenParens + 1,
inlineConstraint.Length - indexOfFirstOpenParens - 2);
}
else
{
constraintKey = inlineConstraint;
argumentString = null;
}
Type constraintType;
if (!_inlineConstraintMap.TryGetValue(constraintKey, out constraintType))
{
// Cannot resolve the constraint key
return null;
}
if (!typeof(IRouteConstraint).GetTypeInfo().IsAssignableFrom(constraintType.GetTypeInfo()))
{
throw new InvalidOperationException(
Resources.FormatDefaultInlineConstraintResolver_TypeNotConstraint(
constraintType, constraintKey, typeof(IRouteConstraint).Name));
}
return CreateConstraint(constraintType, argumentString);
}
private static IRouteConstraint CreateConstraint(Type constraintType, string argumentString)
{
// No arguments - call the default constructor
if (argumentString == null)
{
return (IRouteConstraint)Activator.CreateInstance(constraintType);
}
var constraintTypeInfo = constraintType.GetTypeInfo();
ConstructorInfo activationConstructor = null;
object[] parameters = null;
var constructors = constraintTypeInfo.DeclaredConstructors.ToArray();
// If there is only one constructor and it has a single parameter, pass the argument string directly
// This is necessary for the Regex RouteConstraint to ensure that patterns are not split on commas.
if (constructors.Length == 1 && constructors[0].GetParameters().Length == 1)
{
activationConstructor = constructors[0];
parameters = ConvertArguments(activationConstructor.GetParameters(), new string[] { argumentString });
}
else
{
string[] arguments = argumentString.Split(',').Select(argument => argument.Trim()).ToArray();
var matchingConstructors = constructors.Where(ci => ci.GetParameters().Length == arguments.Length).ToArray();
var constructorMatches = matchingConstructors.Length;
if (constructorMatches == 0)
{
throw new InvalidOperationException(
Resources.FormatDefaultInlineConstraintResolver_CouldNotFindCtor(
constraintTypeInfo.Name, argumentString.Length));
}
else if (constructorMatches == 1)
{
activationConstructor = matchingConstructors[0];
parameters = ConvertArguments(activationConstructor.GetParameters(), arguments);
}
else
{
throw new InvalidOperationException(
Resources.FormatDefaultInlineConstraintResolver_AmbiguousCtors(
constraintTypeInfo.Name, argumentString.Length));
}
}
return (IRouteConstraint)activationConstructor.Invoke(parameters);
}
private static object[] ConvertArguments(ParameterInfo[] parameterInfos, string[] arguments)
{
var parameters = new object[parameterInfos.Length];
for (int i = 0; i < parameterInfos.Length; i++)
{
var parameter = parameterInfos[i];
var parameterType = parameter.ParameterType;
parameters[i] = Convert.ChangeType(arguments[i], parameterType, CultureInfo.InvariantCulture);
}
return parameters;
}
}
}

View File

@ -0,0 +1,18 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNet.Routing
{
/// <summary>
/// Defines an abstraction for resolving inline constraints as instances of <see cref="IRouteConstraint"/>.
/// </summary>
public interface IInlineConstraintResolver
{
/// <summary>
/// Resolves the inline constraint.
/// </summary>
/// <param name="inlineConstraint">The inline constraint to resolve.</param>
/// <returns>The <see cref="IRouteConstraint"/> the inline constraint was resolved to.</returns>
IRouteConstraint ResolveConstraint([NotNull] string inlineConstraint);
}
}

View File

@ -7,6 +7,8 @@ namespace Microsoft.AspNet.Routing
{ {
IRouter DefaultHandler { get; set; } IRouter DefaultHandler { get; set; }
IInlineConstraintResolver InlineConstraintResolver { get; set; }
void Add(IRouter router); void Add(IRouter router);
} }
} }

View File

@ -0,0 +1,111 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Text.RegularExpressions;
using Microsoft.AspNet.Routing.Template;
using Microsoft.AspNet.Routing.Constraints;
namespace Microsoft.AspNet.Routing
{
public static class InlineRouteParameterParser
{
// One or more characters, matches "id"
private const string ParameterNamePattern = @"(?<parameterName>.+?)";
// Zero or more inline constraints that start with a colon followed by zero or more characters
// Optionally the constraint can have arguments within parentheses
// - necessary to capture characters like ":" and "}"
// Matches ":int", ":length(2)", ":regex(\})", ":regex(:)" zero or more times
private const string ConstraintPattern = @"(:(?<constraint>.*?(\(.*?\))?))*";
// A default value with an equal sign followed by zero or more characters
// Matches "=", "=abc"
private const string DefaultValueParameter = @"(?<defaultValue>(=.*?))?";
private static readonly Regex _parameterRegex = new Regex(
"^" + ParameterNamePattern + ConstraintPattern + DefaultValueParameter + "$",
RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
public static TemplatePart ParseRouteParameter([NotNull] string routeParameter,
[NotNull] IInlineConstraintResolver constraintResolver)
{
var isCatchAll = routeParameter.StartsWith("*", StringComparison.Ordinal);
var isOptional = routeParameter.EndsWith("?", StringComparison.Ordinal);
routeParameter = isCatchAll ? routeParameter.Substring(1) : routeParameter;
routeParameter = isOptional ? routeParameter.Substring(0, routeParameter.Length - 1) : routeParameter;
var parameterMatch = _parameterRegex.Match(routeParameter);
if (!parameterMatch.Success)
{
return TemplatePart.CreateParameter(name: string.Empty,
isCatchAll: isCatchAll,
isOptional: isOptional,
defaultValue: null,
inlineConstraint: null
);
}
var parameterName = parameterMatch.Groups["parameterName"].Value;
// Add the default value if present
var defaultValueGroup = parameterMatch.Groups["defaultValue"];
var defaultValue = GetDefaultValue(defaultValueGroup);
// Register inline constraints if present
var constraintGroup = parameterMatch.Groups["constraint"];
var inlineConstraint = GetInlineConstraint(constraintGroup, constraintResolver);
return TemplatePart.CreateParameter(parameterName,
isCatchAll,
isOptional,
defaultValue,
inlineConstraint);
}
private static string GetDefaultValue(Group defaultValueGroup)
{
if (defaultValueGroup.Success)
{
var defaultValueMatch = defaultValueGroup.Value;
// Strip out the equal sign at the beginning
Contract.Assert(defaultValueMatch.StartsWith("=", StringComparison.Ordinal));
return defaultValueMatch.Substring(1);
}
return null;
}
private static IRouteConstraint GetInlineConstraint(Group constraintGroup,
IInlineConstraintResolver constraintResolver)
{
var parameterConstraints = new List<IRouteConstraint>();
foreach (Capture constraintCapture in constraintGroup.Captures)
{
var inlineConstraint = constraintCapture.Value;
var constraint = constraintResolver.ResolveConstraint(inlineConstraint);
if (constraint == null)
{
throw new InvalidOperationException(
Resources.FormatInlineRouteParser_CouldNotResolveConstraint(
constraintResolver.GetType().Name, inlineConstraint));
}
parameterConstraints.Add(constraint);
}
if (parameterConstraints.Count > 0)
{
var constraint = parameterConstraints.Count == 1 ?
parameterConstraints[0] :
new CompositeRouteConstraint(parameterConstraints);
return constraint;
}
return null;
}
}
}

View File

@ -22,7 +22,12 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="BuilderExtensions.cs" /> <Compile Include="BuilderExtensions.cs" />
<Compile Include="Constraints\CompositeRouteConstraint.cs" />
<Compile Include="Constraints\IntRouteConstraint.cs" />
<Compile Include="DefaultInlineConstraintResolver.cs" />
<Compile Include="IInlineConstraintResolver.cs" />
<Compile Include="INamedRouter.cs" /> <Compile Include="INamedRouter.cs" />
<Compile Include="InlineRouteParameterParser.cs" />
<Compile Include="IRouteCollection.cs" /> <Compile Include="IRouteCollection.cs" />
<Compile Include="IRouteConstraint.cs" /> <Compile Include="IRouteConstraint.cs" />
<Compile Include="IRouter.cs" /> <Compile Include="IRouter.cs" />

View File

@ -11,7 +11,7 @@ namespace Microsoft.AspNet.Routing
= new ResourceManager("Microsoft.AspNet.Routing.Resources", typeof(Resources).GetTypeInfo().Assembly); = new ResourceManager("Microsoft.AspNet.Routing.Resources", typeof(Resources).GetTypeInfo().Assembly);
/// <summary> /// <summary>
/// The supplied route name '{0}' is ambiguous and matched more than one routes. /// The supplied route name '{0}' is ambiguous and matched more than one route.
/// </summary> /// </summary>
internal static string NamedRoutes_AmbiguousRoutesFound internal static string NamedRoutes_AmbiguousRoutesFound
{ {
@ -19,7 +19,7 @@ namespace Microsoft.AspNet.Routing
} }
/// <summary> /// <summary>
/// The supplied route name '{0}' is ambiguous and matched more than one routes. /// The supplied route name '{0}' is ambiguous and matched more than one route.
/// </summary> /// </summary>
internal static string FormatNamedRoutes_AmbiguousRoutesFound(object p0) internal static string FormatNamedRoutes_AmbiguousRoutesFound(object p0)
{ {
@ -42,6 +42,54 @@ namespace Microsoft.AspNet.Routing
return GetString("DefaultHandler_MustBeSet"); return GetString("DefaultHandler_MustBeSet");
} }
/// <summary>
/// The constructor to use for activating the constraint type '{0}' is ambiguous. Multiple constructors were found with the following number of parameters: {1}.
/// </summary>
internal static string DefaultInlineConstraintResolver_AmbiguousCtors
{
get { return GetString("DefaultInlineConstraintResolver_AmbiguousCtors"); }
}
/// <summary>
/// The constructor to use for activating the constraint type '{0}' is ambiguous. Multiple constructors were found with the following number of parameters: {1}.
/// </summary>
internal static string FormatDefaultInlineConstraintResolver_AmbiguousCtors(object p0, object p1)
{
return string.Format(CultureInfo.CurrentCulture, GetString("DefaultInlineConstraintResolver_AmbiguousCtors"), p0, p1);
}
/// <summary>
/// Could not find a constructor for constraint type '{0}' with the following number of parameters: {1}.
/// </summary>
internal static string DefaultInlineConstraintResolver_CouldNotFindCtor
{
get { return GetString("DefaultInlineConstraintResolver_CouldNotFindCtor"); }
}
/// <summary>
/// Could not find a constructor for constraint type '{0}' with the following number of parameters: {1}.
/// </summary>
internal static string FormatDefaultInlineConstraintResolver_CouldNotFindCtor(object p0, object p1)
{
return string.Format(CultureInfo.CurrentCulture, GetString("DefaultInlineConstraintResolver_CouldNotFindCtor"), p0, p1);
}
/// <summary>
/// The constraint type '{0}' which is mapped to constraint key '{1}' must implement the '{2}' interface.
/// </summary>
internal static string DefaultInlineConstraintResolver_TypeNotConstraint
{
get { return GetString("DefaultInlineConstraintResolver_TypeNotConstraint"); }
}
/// <summary>
/// The constraint type '{0}' which is mapped to constraint key '{1}' must implement the '{2}' interface.
/// </summary>
internal static string FormatDefaultInlineConstraintResolver_TypeNotConstraint(object p0, object p1, object p2)
{
return string.Format(CultureInfo.CurrentCulture, GetString("DefaultInlineConstraintResolver_TypeNotConstraint"), p0, p1, p2);
}
/// <summary> /// <summary>
/// The constraint entry '{0}' must have a string value or be of a type which implements '{1}'. /// The constraint entry '{0}' must have a string value or be of a type which implements '{1}'.
/// </summary> /// </summary>
@ -74,6 +122,22 @@ namespace Microsoft.AspNet.Routing
return GetString("TemplateRoute_CannotHaveCatchAllInMultiSegment"); return GetString("TemplateRoute_CannotHaveCatchAllInMultiSegment");
} }
/// <summary>
/// The route parameter '{0}' has both an inline deafult value and an explicit default value specified. A route parameter cannot contain an inline default value when a default value is specified explicitly. Consider removing one of them.
/// </summary>
internal static string TemplateRoute_CannotHaveDefaultValueSpecifiedInlineAndExplicitly
{
get { return GetString("TemplateRoute_CannotHaveDefaultValueSpecifiedInlineAndExplicitly"); }
}
/// <summary>
/// The route parameter '{0}' has both an inline deafult value and an explicit default value specified. A route parameter cannot contain an inline default value when a default value is specified explicitly. Consider removing one of them.
/// </summary>
internal static string FormatTemplateRoute_CannotHaveDefaultValueSpecifiedInlineAndExplicitly(object p0)
{
return string.Format(CultureInfo.CurrentCulture, GetString("TemplateRoute_CannotHaveDefaultValueSpecifiedInlineAndExplicitly"), p0);
}
/// <summary> /// <summary>
/// A path segment cannot contain two consecutive parameters. They must be separated by a '/' or by a literal string. /// A path segment cannot contain two consecutive parameters. They must be separated by a '/' or by a literal string.
/// </summary> /// </summary>
@ -138,6 +202,22 @@ namespace Microsoft.AspNet.Routing
return GetString("TemplateRoute_CatchAllCannotBeOptional"); return GetString("TemplateRoute_CatchAllCannotBeOptional");
} }
/// <summary>
/// An optional parameter cannot have default value.
/// </summary>
internal static string TemplateRoute_OptionalCannotHaveDefaultValue
{
get { return GetString("TemplateRoute_OptionalCannotHaveDefaultValue"); }
}
/// <summary>
/// An optional parameter cannot have default value.
/// </summary>
internal static string FormatTemplateRoute_OptionalCannotHaveDefaultValue()
{
return GetString("TemplateRoute_OptionalCannotHaveDefaultValue");
}
/// <summary> /// <summary>
/// A catch-all parameter can only appear as the last segment of the route template. /// A catch-all parameter can only appear as the last segment of the route template.
/// </summary> /// </summary>
@ -250,6 +330,22 @@ namespace Microsoft.AspNet.Routing
return string.Format(CultureInfo.CurrentCulture, GetString("TemplateRoute_ValidationMustBeStringOrCustomConstraint"), p0, p1, p2); return string.Format(CultureInfo.CurrentCulture, GetString("TemplateRoute_ValidationMustBeStringOrCustomConstraint"), p0, p1, p2);
} }
/// <summary>
/// The inline constraint resolver of type '{0}' was unable to resolve the following inline constraint: '{1}'.
/// </summary>
internal static string InlineRouteParser_CouldNotResolveConstraint
{
get { return GetString("InlineRouteParser_CouldNotResolveConstraint"); }
}
/// <summary>
/// The inline constraint resolver of type '{0}' was unable to resolve the following inline constraint: '{1}'.
/// </summary>
internal static string FormatInlineRouteParser_CouldNotResolveConstraint(object p0, object p1)
{
return string.Format(CultureInfo.CurrentCulture, GetString("InlineRouteParser_CouldNotResolveConstraint"), p0, p1);
}
private static string GetString(string name, params string[] formatterNames) private static string GetString(string name, params string[] formatterNames)
{ {
var value = _resourceManager.GetString(name); var value = _resourceManager.GetString(name);

View File

@ -123,12 +123,24 @@
<data name="DefaultHandler_MustBeSet" xml:space="preserve"> <data name="DefaultHandler_MustBeSet" xml:space="preserve">
<value>A default handler must be set on the RouteCollection.</value> <value>A default handler must be set on the RouteCollection.</value>
</data> </data>
<data name="DefaultInlineConstraintResolver_AmbiguousCtors" xml:space="preserve">
<value>The constructor to use for activating the constraint type '{0}' is ambiguous. Multiple constructors were found with the following number of parameters: {1}.</value>
</data>
<data name="DefaultInlineConstraintResolver_CouldNotFindCtor" xml:space="preserve">
<value>Could not find a constructor for constraint type '{0}' with the following number of parameters: {1}.</value>
</data>
<data name="DefaultInlineConstraintResolver_TypeNotConstraint" xml:space="preserve">
<value>The constraint type '{0}' which is mapped to constraint key '{1}' must implement the '{2}' interface.</value>
</data>
<data name="GeneralConstraints_ValidationMustBeStringOrCustomConstraint" xml:space="preserve"> <data name="GeneralConstraints_ValidationMustBeStringOrCustomConstraint" xml:space="preserve">
<value>The constraint entry '{0}' must have a string value or be of a type which implements '{1}'.</value> <value>The constraint entry '{0}' must have a string value or be of a type which implements '{1}'.</value>
</data> </data>
<data name="TemplateRoute_CannotHaveCatchAllInMultiSegment" xml:space="preserve"> <data name="TemplateRoute_CannotHaveCatchAllInMultiSegment" xml:space="preserve">
<value>A path segment that contains more than one section, such as a literal section or a parameter, cannot contain a catch-all parameter.</value> <value>A path segment that contains more than one section, such as a literal section or a parameter, cannot contain a catch-all parameter.</value>
</data> </data>
<data name="TemplateRoute_CannotHaveDefaultValueSpecifiedInlineAndExplicitly" xml:space="preserve">
<value>The route parameter '{0}' has both an inline default value and an explicit default value specified. A route parameter cannot contain an inline default value when a default value is specified explicitly. Consider removing one of them.</value>
</data>
<data name="TemplateRoute_CannotHaveConsecutiveParameters" xml:space="preserve"> <data name="TemplateRoute_CannotHaveConsecutiveParameters" xml:space="preserve">
<value>A path segment cannot contain two consecutive parameters. They must be separated by a '/' or by a literal string.</value> <value>A path segment cannot contain two consecutive parameters. They must be separated by a '/' or by a literal string.</value>
</data> </data>
@ -141,6 +153,9 @@
<data name="TemplateRoute_CatchAllCannotBeOptional" xml:space="preserve"> <data name="TemplateRoute_CatchAllCannotBeOptional" xml:space="preserve">
<value>A catch-all parameter cannot be marked optional.</value> <value>A catch-all parameter cannot be marked optional.</value>
</data> </data>
<data name="TemplateRoute_OptionalCannotHaveDefaultValue" xml:space="preserve">
<value>An optional parameter cannot have default value.</value>
</data>
<data name="TemplateRoute_CatchAllMustBeLast" xml:space="preserve"> <data name="TemplateRoute_CatchAllMustBeLast" xml:space="preserve">
<value>A catch-all parameter can only appear as the last segment of the route template.</value> <value>A catch-all parameter can only appear as the last segment of the route template.</value>
</data> </data>
@ -162,4 +177,7 @@
<data name="TemplateRoute_ValidationMustBeStringOrCustomConstraint" xml:space="preserve"> <data name="TemplateRoute_ValidationMustBeStringOrCustomConstraint" xml:space="preserve">
<value>The constraint entry '{0}' on the route with route template '{1}' must have a string value or be of a type which implements '{2}'.</value> <value>The constraint entry '{0}' on the route with route template '{1}' must have a string value or be of a type which implements '{2}'.</value>
</data> </data>
<data name="InlineRouteParser_CouldNotResolveConstraint" xml:space="preserve">
<value>The inline constraint resolver of type '{0}' was unable to resolve the following inline constraint: '{1}'.</value>
</data>
</root> </root>

View File

@ -26,6 +26,8 @@ namespace Microsoft.AspNet.Routing
public IRouter DefaultHandler { get; set; } public IRouter DefaultHandler { get; set; }
public IInlineConstraintResolver InlineConstraintResolver { get; set; }
public void Add([NotNull] IRouter router) public void Add([NotNull] IRouter router)
{ {
var namedRouter = router as INamedRouter; var namedRouter = router as INamedRouter;

View File

@ -30,14 +30,23 @@ namespace Microsoft.AspNet.Routing
throw new InvalidOperationException(Resources.DefaultHandler_MustBeSet); throw new InvalidOperationException(Resources.DefaultHandler_MustBeSet);
} }
routes.Add(new TemplateRoute(routes.DefaultHandler, name, template, defaults, constraints: null)); routes.Add(new TemplateRoute(routes.DefaultHandler,
name,
template,
defaults,
constraints: null,
inlineConstraintResolver: routes.InlineConstraintResolver));
return routes; return routes;
} }
public static IRouteCollection MapRoute(this IRouteCollection routes, string name, string template, public static IRouteCollection MapRoute(this IRouteCollection routes, string name, string template,
object defaults, object constraints) object defaults, object constraints)
{ {
MapRoute(routes, name, template, new RouteValueDictionary(defaults), new RouteValueDictionary(constraints)); MapRoute(routes,
name,
template,
new RouteValueDictionary(defaults),
new RouteValueDictionary(constraints));
return routes; return routes;
} }
@ -55,15 +64,23 @@ namespace Microsoft.AspNet.Routing
return routes; return routes;
} }
public static IRouteCollection MapRoute(this IRouteCollection routes, string name, string template, public static IRouteCollection MapRoute(this IRouteCollection routes,
IDictionary<string, object> defaults, IDictionary<string, object> constraints) string name,
string template,
IDictionary<string, object> defaults,
IDictionary<string, object> constraints)
{ {
if (routes.DefaultHandler == null) if (routes.DefaultHandler == null)
{ {
throw new InvalidOperationException(Resources.DefaultHandler_MustBeSet); throw new InvalidOperationException(Resources.DefaultHandler_MustBeSet);
} }
routes.Add(new TemplateRoute(routes.DefaultHandler, name, template, defaults, constraints)); routes.Add(new TemplateRoute(routes.DefaultHandler,
name,
template,
defaults,
constraints,
routes.InlineConstraintResolver));
return routes; return routes;
} }
} }

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Microsoft.AspNet.Routing.Constraints;
namespace Microsoft.AspNet.Routing namespace Microsoft.AspNet.Routing
{ {

View File

@ -3,10 +3,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts; using System.Diagnostics.Contracts;
using System.Globalization; using System.Globalization;
using System.Linq;
namespace Microsoft.AspNet.Routing.Template namespace Microsoft.AspNet.Routing.Template
{ {
@ -18,7 +16,7 @@ namespace Microsoft.AspNet.Routing.Template
private const char EqualsSign = '='; private const char EqualsSign = '=';
private const char QuestionMark = '?'; private const char QuestionMark = '?';
public static Template Parse(string routeTemplate) public static Template Parse(string routeTemplate, IInlineConstraintResolver constraintResolver)
{ {
if (routeTemplate == null) if (routeTemplate == null)
{ {
@ -30,7 +28,7 @@ namespace Microsoft.AspNet.Routing.Template
throw new ArgumentException(Resources.TemplateRoute_InvalidRouteTemplate, "routeTemplate"); throw new ArgumentException(Resources.TemplateRoute_InvalidRouteTemplate, "routeTemplate");
} }
var context = new TemplateParserContext(routeTemplate); var context = new TemplateParserContext(routeTemplate, constraintResolver);
var segments = new List<TemplateSegment>(); var segments = new List<TemplateSegment>();
while (context.Next()) while (context.Next())
@ -174,24 +172,35 @@ namespace Microsoft.AspNet.Routing.Template
} }
} }
var rawName = context.Capture(); var rawParameter = context.Capture();
var isCatchAll = rawName.StartsWith("*", StringComparison.Ordinal); // At this point, we need to parse the raw name for inline constraint,
var isOptional = rawName.EndsWith("?", StringComparison.Ordinal); // default values and optional parameters.
var templatePart = InlineRouteParameterParser.ParseRouteParameter(rawParameter,
context.ConstraintResolver);
if (isCatchAll && isOptional)
if (templatePart.IsCatchAll && templatePart.IsOptional)
{ {
context.Error = Resources.TemplateRoute_CatchAllCannotBeOptional; context.Error = Resources.TemplateRoute_CatchAllCannotBeOptional;
return false; return false;
} }
rawName = isCatchAll ? rawName.Substring(1) : rawName; if (templatePart.IsOptional && templatePart.DefaultValue != null)
rawName = isOptional ? rawName.Substring(0, rawName.Length - 1) : rawName; {
// Cannot be optional and have a default value.
// The only way to declare an optional parameter is to have a ? at the end,
// hence we cannot have both default value and optional parameter within the template.
// A workaround is to add it as a separate entry in the defaults argument.
context.Error = Resources.TemplateRoute_OptionalCannotHaveDefaultValue;
return false;
}
var parameterName = rawName; var parameterName = templatePart.Name;
if (IsValidParameterName(context, parameterName)) if (IsValidParameterName(context, parameterName))
{ {
segment.Parts.Add(TemplatePart.CreateParameter(parameterName, isCatchAll, isOptional)); segment.Parts.Add(templatePart);
return true; return true;
} }
else else
@ -392,12 +401,13 @@ namespace Microsoft.AspNet.Routing.Template
private HashSet<string> _parameterNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase); private HashSet<string> _parameterNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
public TemplateParserContext(string template) public TemplateParserContext(string template, IInlineConstraintResolver constraintResolver)
{ {
Contract.Assert(template != null); Contract.Assert(template != null);
_template = template; _template = template;
_index = -1; _index = -1;
ConstraintResolver = constraintResolver;
} }
public char Current public char Current
@ -416,6 +426,12 @@ namespace Microsoft.AspNet.Routing.Template
get { return _parameterNames; } get { return _parameterNames; }
} }
public IInlineConstraintResolver ConstraintResolver
{
get;
private set;
}
public bool Back() public bool Back()
{ {
return --_index >= 0; return --_index >= 0;

View File

@ -17,7 +17,11 @@ namespace Microsoft.AspNet.Routing.Template
}; };
} }
public static TemplatePart CreateParameter(string name, bool isCatchAll, bool isOptional) public static TemplatePart CreateParameter([NotNull] string name,
bool isCatchAll,
bool isOptional,
object defaultValue,
IRouteConstraint inlineConstraint)
{ {
return new TemplatePart() return new TemplatePart()
{ {
@ -25,6 +29,8 @@ namespace Microsoft.AspNet.Routing.Template
Name = name, Name = name,
IsCatchAll = isCatchAll, IsCatchAll = isCatchAll,
IsOptional = isOptional, IsOptional = isOptional,
DefaultValue = defaultValue,
InlineConstraint = inlineConstraint,
}; };
} }
@ -34,6 +40,8 @@ namespace Microsoft.AspNet.Routing.Template
public bool IsOptional { get; private set; } public bool IsOptional { get; private set; }
public string Name { get; private set; } public string Name { get; private set; }
public string Text { get; private set; } public string Text { get; private set; }
public object DefaultValue { get; private set; }
public IRouteConstraint InlineConstraint { get; private set; }
internal string DebuggerToString() internal string DebuggerToString()
{ {

View File

@ -3,8 +3,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNet.Routing.Constraints;
namespace Microsoft.AspNet.Routing.Template namespace Microsoft.AspNet.Routing.Template
{ {
@ -18,16 +18,17 @@ namespace Microsoft.AspNet.Routing.Template
private readonly TemplateMatcher _matcher; private readonly TemplateMatcher _matcher;
private readonly TemplateBinder _binder; private readonly TemplateBinder _binder;
public TemplateRoute(IRouter target, string routeTemplate) public TemplateRoute(IRouter target, string routeTemplate, IInlineConstraintResolver inlineConstraintResolver)
: this(target, routeTemplate, null, null) : this(target, routeTemplate, null, null, inlineConstraintResolver)
{ {
} }
public TemplateRoute([NotNull] IRouter target, public TemplateRoute([NotNull] IRouter target,
string routeTemplate, string routeTemplate,
IDictionary<string, object> defaults, IDictionary<string, object> defaults,
IDictionary<string, object> constraints) IDictionary<string, object> constraints,
: this(target, null, routeTemplate, defaults, constraints) IInlineConstraintResolver inlineConstraintResolver)
: this(target, null, routeTemplate, defaults, constraints, inlineConstraintResolver)
{ {
} }
@ -35,16 +36,19 @@ namespace Microsoft.AspNet.Routing.Template
string routeName, string routeName,
string routeTemplate, string routeTemplate,
IDictionary<string, object> defaults, IDictionary<string, object> defaults,
IDictionary<string, object> constraints) IDictionary<string, object> constraints,
IInlineConstraintResolver inlineConstraintResolver)
{ {
_target = target; _target = target;
_routeTemplate = routeTemplate ?? string.Empty; _routeTemplate = routeTemplate ?? string.Empty;
Name = routeName; Name = routeName;
_defaults = defaults ?? new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase); _defaults = defaults ?? new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
_constraints = RouteConstraintBuilder.BuildConstraints(constraints, _routeTemplate); _constraints = RouteConstraintBuilder.BuildConstraints(constraints, _routeTemplate) ??
new Dictionary<string, IRouteConstraint>();
// The parser will throw for invalid routes. // The parser will throw for invalid routes.
_parsedTemplate = TemplateParser.Parse(RouteTemplate); _parsedTemplate = TemplateParser.Parse(RouteTemplate, inlineConstraintResolver);
UpdateInlineDefaultValuesAndConstraints();
_matcher = new TemplateMatcher(_parsedTemplate); _matcher = new TemplateMatcher(_parsedTemplate);
_binder = new TemplateBinder(_parsedTemplate, _defaults); _binder = new TemplateBinder(_parsedTemplate, _defaults);
@ -170,5 +174,39 @@ namespace Microsoft.AspNet.Routing.Template
ProvidedValues = providedValues, ProvidedValues = providedValues,
}; };
} }
private void UpdateInlineDefaultValuesAndConstraints()
{
foreach (var parameter in _parsedTemplate.Parameters)
{
if (parameter.InlineConstraint != null)
{
IRouteConstraint constraint;
if (_constraints.TryGetValue(parameter.Name, out constraint))
{
_constraints[parameter.Name] =
new CompositeRouteConstraint(new []{ constraint, parameter.InlineConstraint });
}
else
{
_constraints[parameter.Name] = parameter.InlineConstraint;
}
}
if (parameter.DefaultValue != null)
{
if (_defaults.ContainsKey(parameter.Name))
{
throw new InvalidOperationException(
Resources.
FormatTemplateRoute_CannotHaveDefaultValueSpecifiedInlineAndExplicitly(parameter.Name));
}
else
{
_defaults[parameter.Name] = parameter.DefaultValue;
}
}
}
}
} }
} }

View File

@ -6,6 +6,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Microsoft.AspNet.Http; using Microsoft.AspNet.Http;
using Microsoft.AspNet.Routing.Constraints;
using Microsoft.AspNet.Testing; using Microsoft.AspNet.Testing;
using Moq; using Moq;
using Xunit; using Xunit;

View File

@ -7,6 +7,7 @@ using System.Globalization;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading; using System.Threading;
using Microsoft.AspNet.Http; using Microsoft.AspNet.Http;
using Microsoft.AspNet.Routing.Constraints;
using Moq; using Moq;
using Xunit; using Xunit;

View File

@ -127,7 +127,8 @@ namespace Microsoft.AspNet.Routing.Template.Tests
string expected) string expected)
{ {
// Arrange // Arrange
var binder = new TemplateBinder(TemplateParser.Parse(template), defaults); var binder = new TemplateBinder(TemplateParser.Parse(template, new DefaultInlineConstraintResolver()),
defaults);
// Act & Assert // Act & Assert
var acceptedValues = binder.GetAcceptedValues(null, values); var acceptedValues = binder.GetAcceptedValues(null, values);
@ -960,7 +961,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
string expected) string expected)
{ {
// Arrange // Arrange
var binder = new TemplateBinder(TemplateParser.Parse(template), defaults); var binder = new TemplateBinder(TemplateParser.Parse(template, new DefaultInlineConstraintResolver()), defaults);
// Act & Assert // Act & Assert
var acceptedValues = binder.GetAcceptedValues(ambientValues, values); var acceptedValues = binder.GetAcceptedValues(ambientValues, values);

View File

@ -784,13 +784,13 @@ namespace Microsoft.AspNet.Routing.Template.Tests
private TemplateMatcher CreateMatcher(string template) private TemplateMatcher CreateMatcher(string template)
{ {
return new TemplateMatcher(TemplateParser.Parse(template)); return new TemplateMatcher(TemplateParser.Parse(template, new DefaultInlineConstraintResolver()));
} }
private static void RunTest(string template, string path, IDictionary<string, object> defaults, IDictionary<string, object> expected) private static void RunTest(string template, string path, IDictionary<string, object> defaults, IDictionary<string, object> expected)
{ {
// Arrange // Arrange
var matcher = new TemplateMatcher(TemplateParser.Parse(template)); var matcher = new TemplateMatcher(TemplateParser.Parse(template, new DefaultInlineConstraintResolver()));
// Act // Act
var match = matcher.Match(path, defaults); var match = matcher.Match(path, defaults);

View File

@ -10,6 +10,8 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{ {
public class TemplateRouteParserTests public class TemplateRouteParserTests
{ {
private IInlineConstraintResolver _inlineConstraintResolver = new DefaultInlineConstraintResolver();
[Fact] [Fact]
public void Parse_SingleLiteral() public void Parse_SingleLiteral()
{ {
@ -21,7 +23,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool")); expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool"));
// Act // Act
var actual = TemplateParser.Parse(template); var actual = TemplateParser.Parse(template, _inlineConstraintResolver);
// Assert // Assert
Assert.Equal<Template>(expected, actual, new TemplateEqualityComparer()); Assert.Equal<Template>(expected, actual, new TemplateEqualityComparer());
@ -35,11 +37,11 @@ namespace Microsoft.AspNet.Routing.Template.Tests
var expected = new Template(new List<TemplateSegment>()); var expected = new Template(new List<TemplateSegment>());
expected.Segments.Add(new TemplateSegment()); expected.Segments.Add(new TemplateSegment());
expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p", false, false)); expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p", false, false, defaultValue: null, inlineConstraint: null));
expected.Parameters.Add(expected.Segments[0].Parts[0]); expected.Parameters.Add(expected.Segments[0].Parts[0]);
// Act // Act
var actual = TemplateParser.Parse(template); var actual = TemplateParser.Parse(template, _inlineConstraintResolver);
// Assert // Assert
Assert.Equal<Template>(expected, actual, new TemplateEqualityComparer()); Assert.Equal<Template>(expected, actual, new TemplateEqualityComparer());
@ -53,11 +55,11 @@ namespace Microsoft.AspNet.Routing.Template.Tests
var expected = new Template(new List<TemplateSegment>()); var expected = new Template(new List<TemplateSegment>());
expected.Segments.Add(new TemplateSegment()); expected.Segments.Add(new TemplateSegment());
expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p", false, true)); expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p", false, true, defaultValue: null, inlineConstraint: null));
expected.Parameters.Add(expected.Segments[0].Parts[0]); expected.Parameters.Add(expected.Segments[0].Parts[0]);
// Act // Act
var actual = TemplateParser.Parse(template); var actual = TemplateParser.Parse(template, _inlineConstraintResolver);
// Assert // Assert
Assert.Equal<Template>(expected, actual, new TemplateEqualityComparer()); Assert.Equal<Template>(expected, actual, new TemplateEqualityComparer());
@ -78,7 +80,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
expected.Segments[2].Parts.Add(TemplatePart.CreateLiteral("super")); expected.Segments[2].Parts.Add(TemplatePart.CreateLiteral("super"));
// Act // Act
var actual = TemplateParser.Parse(template); var actual = TemplateParser.Parse(template, _inlineConstraintResolver);
// Assert // Assert
Assert.Equal<Template>(expected, actual, new TemplateEqualityComparer()); Assert.Equal<Template>(expected, actual, new TemplateEqualityComparer());
@ -93,19 +95,31 @@ namespace Microsoft.AspNet.Routing.Template.Tests
var expected = new Template(new List<TemplateSegment>()); var expected = new Template(new List<TemplateSegment>());
expected.Segments.Add(new TemplateSegment()); expected.Segments.Add(new TemplateSegment());
expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", false, false)); expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1",
false,
false,
defaultValue: null,
inlineConstraint: null));
expected.Parameters.Add(expected.Segments[0].Parts[0]); expected.Parameters.Add(expected.Segments[0].Parts[0]);
expected.Segments.Add(new TemplateSegment()); expected.Segments.Add(new TemplateSegment());
expected.Segments[1].Parts.Add(TemplatePart.CreateParameter("p2", false, false)); expected.Segments[1].Parts.Add(TemplatePart.CreateParameter("p2",
false,
false,
defaultValue: null,
inlineConstraint: null));
expected.Parameters.Add(expected.Segments[1].Parts[0]); expected.Parameters.Add(expected.Segments[1].Parts[0]);
expected.Segments.Add(new TemplateSegment()); expected.Segments.Add(new TemplateSegment());
expected.Segments[2].Parts.Add(TemplatePart.CreateParameter("p3", true, false)); expected.Segments[2].Parts.Add(TemplatePart.CreateParameter("p3",
true,
false,
defaultValue: null,
inlineConstraint: null));
expected.Parameters.Add(expected.Segments[2].Parts[0]); expected.Parameters.Add(expected.Segments[2].Parts[0]);
// Act // Act
var actual = TemplateParser.Parse(template); var actual = TemplateParser.Parse(template, _inlineConstraintResolver);
// Assert // Assert
Assert.Equal<Template>(expected, actual, new TemplateEqualityComparer()); Assert.Equal<Template>(expected, actual, new TemplateEqualityComparer());
@ -120,11 +134,15 @@ namespace Microsoft.AspNet.Routing.Template.Tests
var expected = new Template(new List<TemplateSegment>()); var expected = new Template(new List<TemplateSegment>());
expected.Segments.Add(new TemplateSegment()); expected.Segments.Add(new TemplateSegment());
expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool-")); expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool-"));
expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", false, false)); expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1",
false,
false,
defaultValue: null,
inlineConstraint: null));
expected.Parameters.Add(expected.Segments[0].Parts[1]); expected.Parameters.Add(expected.Segments[0].Parts[1]);
// Act // Act
var actual = TemplateParser.Parse(template); var actual = TemplateParser.Parse(template, _inlineConstraintResolver);
// Assert // Assert
Assert.Equal<Template>(expected, actual, new TemplateEqualityComparer()); Assert.Equal<Template>(expected, actual, new TemplateEqualityComparer());
@ -138,12 +156,16 @@ namespace Microsoft.AspNet.Routing.Template.Tests
var expected = new Template(new List<TemplateSegment>()); var expected = new Template(new List<TemplateSegment>());
expected.Segments.Add(new TemplateSegment()); expected.Segments.Add(new TemplateSegment());
expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", false, false)); expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1",
false,
false,
defaultValue: null,
inlineConstraint: null));
expected.Parameters.Add(expected.Segments[0].Parts[0]); expected.Parameters.Add(expected.Segments[0].Parts[0]);
expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool-")); expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool-"));
// Act // Act
var actual = TemplateParser.Parse(template); var actual = TemplateParser.Parse(template, _inlineConstraintResolver);
// Assert // Assert
Assert.Equal<Template>(expected, actual, new TemplateEqualityComparer()); Assert.Equal<Template>(expected, actual, new TemplateEqualityComparer());
@ -157,14 +179,22 @@ namespace Microsoft.AspNet.Routing.Template.Tests
var expected = new Template(new List<TemplateSegment>()); var expected = new Template(new List<TemplateSegment>());
expected.Segments.Add(new TemplateSegment()); expected.Segments.Add(new TemplateSegment());
expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", false, false)); expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1",
false,
false,
defaultValue: null,
inlineConstraint: null));
expected.Parameters.Add(expected.Segments[0].Parts[0]); expected.Parameters.Add(expected.Segments[0].Parts[0]);
expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool-")); expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool-"));
expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p2", false, false)); expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p2",
false,
false,
defaultValue: null,
inlineConstraint: null));
expected.Parameters.Add(expected.Segments[0].Parts[2]); expected.Parameters.Add(expected.Segments[0].Parts[2]);
// Act // Act
var actual = TemplateParser.Parse(template); var actual = TemplateParser.Parse(template, _inlineConstraintResolver);
// Assert // Assert
Assert.Equal<Template>(expected, actual, new TemplateEqualityComparer()); Assert.Equal<Template>(expected, actual, new TemplateEqualityComparer());
@ -179,12 +209,16 @@ namespace Microsoft.AspNet.Routing.Template.Tests
var expected = new Template(new List<TemplateSegment>()); var expected = new Template(new List<TemplateSegment>());
expected.Segments.Add(new TemplateSegment()); expected.Segments.Add(new TemplateSegment());
expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool-")); expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool-"));
expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", false, false)); expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1",
false,
false,
defaultValue: null,
inlineConstraint: null));
expected.Parameters.Add(expected.Segments[0].Parts[1]); expected.Parameters.Add(expected.Segments[0].Parts[1]);
expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("-awesome")); expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("-awesome"));
// Act // Act
var actual = TemplateParser.Parse(template); var actual = TemplateParser.Parse(template, _inlineConstraintResolver);
// Assert // Assert
Assert.Equal<Template>(expected, actual, new TemplateEqualityComparer()); Assert.Equal<Template>(expected, actual, new TemplateEqualityComparer());
@ -194,7 +228,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
public void InvalidTemplate_WithRepeatedParameter() public void InvalidTemplate_WithRepeatedParameter()
{ {
var ex = ExceptionAssert.Throws<ArgumentException>( var ex = ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("{Controller}.mvc/{id}/{controller}"), () => TemplateParser.Parse("{Controller}.mvc/{id}/{controller}", _inlineConstraintResolver),
"The route parameter name 'controller' appears more than one time in the route template." + Environment.NewLine + "Parameter name: routeTemplate"); "The route parameter name 'controller' appears more than one time in the route template." + Environment.NewLine + "Parameter name: routeTemplate");
} }
@ -208,7 +242,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
public void InvalidTemplate_WithMismatchedBraces(string template) public void InvalidTemplate_WithMismatchedBraces(string template)
{ {
ExceptionAssert.Throws<ArgumentException>( ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse(template), () => TemplateParser.Parse(template, _inlineConstraintResolver),
@"There is an incomplete parameter in the route template. Check that each '{' character has a matching '}' character." + Environment.NewLine + @"There is an incomplete parameter in the route template. Check that each '{' character has a matching '}' character." + Environment.NewLine +
"Parameter name: routeTemplate"); "Parameter name: routeTemplate");
} }
@ -217,7 +251,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
public void InvalidTemplate_CannotHaveCatchAllInMultiSegment() public void InvalidTemplate_CannotHaveCatchAllInMultiSegment()
{ {
ExceptionAssert.Throws<ArgumentException>( ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("123{a}abc{*moo}"), () => TemplateParser.Parse("123{a}abc{*moo}", _inlineConstraintResolver),
"A path segment that contains more than one section, such as a literal section or a parameter, cannot contain a catch-all parameter." + Environment.NewLine + "A path segment that contains more than one section, such as a literal section or a parameter, cannot contain a catch-all parameter." + Environment.NewLine +
"Parameter name: routeTemplate"); "Parameter name: routeTemplate");
} }
@ -226,7 +260,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
public void InvalidTemplate_CannotHaveMoreThanOneCatchAll() public void InvalidTemplate_CannotHaveMoreThanOneCatchAll()
{ {
ExceptionAssert.Throws<ArgumentException>( ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("{*p1}/{*p2}"), () => TemplateParser.Parse("{*p1}/{*p2}", _inlineConstraintResolver),
"A catch-all parameter can only appear as the last segment of the route template." + Environment.NewLine + "A catch-all parameter can only appear as the last segment of the route template." + Environment.NewLine +
"Parameter name: routeTemplate"); "Parameter name: routeTemplate");
} }
@ -235,7 +269,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
public void InvalidTemplate_CannotHaveMoreThanOneCatchAllInMultiSegment() public void InvalidTemplate_CannotHaveMoreThanOneCatchAllInMultiSegment()
{ {
ExceptionAssert.Throws<ArgumentException>( ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("{*p1}abc{*p2}"), () => TemplateParser.Parse("{*p1}abc{*p2}", _inlineConstraintResolver),
"A path segment that contains more than one section, such as a literal section or a parameter, cannot contain a catch-all parameter." + Environment.NewLine + "A path segment that contains more than one section, such as a literal section or a parameter, cannot contain a catch-all parameter." + Environment.NewLine +
"Parameter name: routeTemplate"); "Parameter name: routeTemplate");
} }
@ -244,7 +278,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
public void InvalidTemplate_CannotHaveCatchAllWithNoName() public void InvalidTemplate_CannotHaveCatchAllWithNoName()
{ {
ExceptionAssert.Throws<ArgumentException>( ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("foo/{*}"), () => TemplateParser.Parse("foo/{*}", _inlineConstraintResolver),
"The route parameter name '' is invalid. Route parameter names must be non-empty and cannot contain these characters: '{', '}', '/'. " + "The route parameter name '' is invalid. Route parameter names must be non-empty and cannot contain these characters: '{', '}', '/'. " +
"The '?' character marks a parameter as optional, and can only occur at the end of the parameter." + Environment.NewLine + "The '?' character marks a parameter as optional, and can only occur at the end of the parameter." + Environment.NewLine +
"Parameter name: routeTemplate"); "Parameter name: routeTemplate");
@ -254,7 +288,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
public void InvalidTemplate_CannotHaveConsecutiveOpenBrace() public void InvalidTemplate_CannotHaveConsecutiveOpenBrace()
{ {
ExceptionAssert.Throws<ArgumentException>( ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("foo/{{p1}"), () => TemplateParser.Parse("foo/{{p1}", _inlineConstraintResolver),
"There is an incomplete parameter in the route template. Check that each '{' character has a matching '}' character." + Environment.NewLine + "There is an incomplete parameter in the route template. Check that each '{' character has a matching '}' character." + Environment.NewLine +
"Parameter name: routeTemplate"); "Parameter name: routeTemplate");
} }
@ -263,7 +297,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
public void InvalidTemplate_CannotHaveConsecutiveCloseBrace() public void InvalidTemplate_CannotHaveConsecutiveCloseBrace()
{ {
ExceptionAssert.Throws<ArgumentException>( ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("foo/{p1}}"), () => TemplateParser.Parse("foo/{p1}}", _inlineConstraintResolver),
"There is an incomplete parameter in the route template. Check that each '{' character has a matching '}' character." + Environment.NewLine + "There is an incomplete parameter in the route template. Check that each '{' character has a matching '}' character." + Environment.NewLine +
"Parameter name: routeTemplate"); "Parameter name: routeTemplate");
} }
@ -272,7 +306,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
public void InvalidTemplate_SameParameterTwiceThrows() public void InvalidTemplate_SameParameterTwiceThrows()
{ {
ExceptionAssert.Throws<ArgumentException>( ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("{aaa}/{AAA}"), () => TemplateParser.Parse("{aaa}/{AAA}", _inlineConstraintResolver),
"The route parameter name 'AAA' appears more than one time in the route template." + Environment.NewLine + "The route parameter name 'AAA' appears more than one time in the route template." + Environment.NewLine +
"Parameter name: routeTemplate"); "Parameter name: routeTemplate");
} }
@ -281,7 +315,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
public void InvalidTemplate_SameParameterTwiceAndOneCatchAllThrows() public void InvalidTemplate_SameParameterTwiceAndOneCatchAllThrows()
{ {
ExceptionAssert.Throws<ArgumentException>( ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("{aaa}/{*AAA}"), () => TemplateParser.Parse("{aaa}/{*AAA}", _inlineConstraintResolver),
"The route parameter name 'AAA' appears more than one time in the route template." + Environment.NewLine + "The route parameter name 'AAA' appears more than one time in the route template." + Environment.NewLine +
"Parameter name: routeTemplate"); "Parameter name: routeTemplate");
} }
@ -290,7 +324,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
public void InvalidTemplate_InvalidParameterNameWithCloseBracketThrows() public void InvalidTemplate_InvalidParameterNameWithCloseBracketThrows()
{ {
ExceptionAssert.Throws<ArgumentException>( ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("{a}/{aa}a}/{z}"), () => TemplateParser.Parse("{a}/{aa}a}/{z}", _inlineConstraintResolver),
"There is an incomplete parameter in the route template. Check that each '{' character has a matching '}' character." + Environment.NewLine + "There is an incomplete parameter in the route template. Check that each '{' character has a matching '}' character." + Environment.NewLine +
"Parameter name: routeTemplate"); "Parameter name: routeTemplate");
} }
@ -299,7 +333,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
public void InvalidTemplate_InvalidParameterNameWithOpenBracketThrows() public void InvalidTemplate_InvalidParameterNameWithOpenBracketThrows()
{ {
ExceptionAssert.Throws<ArgumentException>( ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("{a}/{a{aa}/{z}"), () => TemplateParser.Parse("{a}/{a{aa}/{z}", _inlineConstraintResolver),
"The route parameter name 'a{aa' is invalid. Route parameter names must be non-empty and cannot contain these characters: '{', '}', '/'. " + "The route parameter name 'a{aa' is invalid. Route parameter names must be non-empty and cannot contain these characters: '{', '}', '/'. " +
"The '?' character marks a parameter as optional, and can only occur at the end of the parameter." + Environment.NewLine + "The '?' character marks a parameter as optional, and can only occur at the end of the parameter." + Environment.NewLine +
"Parameter name: routeTemplate"); "Parameter name: routeTemplate");
@ -309,7 +343,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
public void InvalidTemplate_InvalidParameterNameWithEmptyNameThrows() public void InvalidTemplate_InvalidParameterNameWithEmptyNameThrows()
{ {
ExceptionAssert.Throws<ArgumentException>( ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("{a}/{}/{z}"), () => TemplateParser.Parse("{a}/{}/{z}", _inlineConstraintResolver),
"The route parameter name '' is invalid. Route parameter names must be non-empty and cannot contain these characters: '{', '}', '/'. " + "The route parameter name '' is invalid. Route parameter names must be non-empty and cannot contain these characters: '{', '}', '/'. " +
"The '?' character marks a parameter as optional, and can only occur at the end of the parameter." + Environment.NewLine + "The '?' character marks a parameter as optional, and can only occur at the end of the parameter." + Environment.NewLine +
"Parameter name: routeTemplate"); "Parameter name: routeTemplate");
@ -319,7 +353,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
public void InvalidTemplate_InvalidParameterNameWithQuestionThrows() public void InvalidTemplate_InvalidParameterNameWithQuestionThrows()
{ {
ExceptionAssert.Throws<ArgumentException>( ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("{Controller}.mvc/{?}"), () => TemplateParser.Parse("{Controller}.mvc/{?}", _inlineConstraintResolver),
"The route parameter name '' is invalid. Route parameter names must be non-empty and cannot contain these characters: '{', '}', '/'. " + "The route parameter name '' is invalid. Route parameter names must be non-empty and cannot contain these characters: '{', '}', '/'. " +
"The '?' character marks a parameter as optional, and can only occur at the end of the parameter." + Environment.NewLine + "The '?' character marks a parameter as optional, and can only occur at the end of the parameter." + Environment.NewLine +
"Parameter name: routeTemplate"); "Parameter name: routeTemplate");
@ -329,7 +363,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
public void InvalidTemplate_ConsecutiveSeparatorsSlashSlashThrows() public void InvalidTemplate_ConsecutiveSeparatorsSlashSlashThrows()
{ {
ExceptionAssert.Throws<ArgumentException>( ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("{a}//{z}"), () => TemplateParser.Parse("{a}//{z}", _inlineConstraintResolver),
"The route template separator character '/' cannot appear consecutively. It must be separated by either a parameter or a literal value." + Environment.NewLine + "The route template separator character '/' cannot appear consecutively. It must be separated by either a parameter or a literal value." + Environment.NewLine +
"Parameter name: routeTemplate"); "Parameter name: routeTemplate");
} }
@ -338,7 +372,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
public void InvalidTemplate_WithCatchAllNotAtTheEndThrows() public void InvalidTemplate_WithCatchAllNotAtTheEndThrows()
{ {
ExceptionAssert.Throws<ArgumentException>( ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("foo/{p1}/{*p2}/{p3}"), () => TemplateParser.Parse("foo/{p1}/{*p2}/{p3}", _inlineConstraintResolver),
"A catch-all parameter can only appear as the last segment of the route template." + Environment.NewLine + "A catch-all parameter can only appear as the last segment of the route template." + Environment.NewLine +
"Parameter name: routeTemplate"); "Parameter name: routeTemplate");
} }
@ -347,7 +381,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
public void InvalidTemplate_RepeatedParametersThrows() public void InvalidTemplate_RepeatedParametersThrows()
{ {
ExceptionAssert.Throws<ArgumentException>( ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("foo/aa{p1}{p2}"), () => TemplateParser.Parse("foo/aa{p1}{p2}", _inlineConstraintResolver),
"A path segment cannot contain two consecutive parameters. They must be separated by a '/' or by a literal string." + Environment.NewLine + "A path segment cannot contain two consecutive parameters. They must be separated by a '/' or by a literal string." + Environment.NewLine +
"Parameter name: routeTemplate"); "Parameter name: routeTemplate");
} }
@ -356,7 +390,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
public void InvalidTemplate_CannotStartWithSlash() public void InvalidTemplate_CannotStartWithSlash()
{ {
ExceptionAssert.Throws<ArgumentException>( ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("/foo"), () => TemplateParser.Parse("/foo", _inlineConstraintResolver),
"The route template cannot start with a '/' or '~' character." + Environment.NewLine + "The route template cannot start with a '/' or '~' character." + Environment.NewLine +
"Parameter name: routeTemplate"); "Parameter name: routeTemplate");
} }
@ -365,7 +399,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
public void InvalidTemplate_CannotStartWithTilde() public void InvalidTemplate_CannotStartWithTilde()
{ {
ExceptionAssert.Throws<ArgumentException>( ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("~foo"), () => TemplateParser.Parse("~foo", _inlineConstraintResolver),
"The route template cannot start with a '/' or '~' character." + Environment.NewLine + "The route template cannot start with a '/' or '~' character." + Environment.NewLine +
"Parameter name: routeTemplate"); "Parameter name: routeTemplate");
} }
@ -374,7 +408,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
public void InvalidTemplate_CannotContainQuestionMark() public void InvalidTemplate_CannotContainQuestionMark()
{ {
ExceptionAssert.Throws<ArgumentException>( ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("foor?bar"), () => TemplateParser.Parse("foor?bar", _inlineConstraintResolver),
"The literal section 'foor?bar' is invalid. Literal sections cannot contain the '?' character." + Environment.NewLine + "The literal section 'foor?bar' is invalid. Literal sections cannot contain the '?' character." + Environment.NewLine +
"Parameter name: routeTemplate"); "Parameter name: routeTemplate");
} }
@ -383,7 +417,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
public void InvalidTemplate_ParameterCannotContainQuestionMark_UnlessAtEnd() public void InvalidTemplate_ParameterCannotContainQuestionMark_UnlessAtEnd()
{ {
ExceptionAssert.Throws<ArgumentException>( ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("{foor?b}"), () => TemplateParser.Parse("{foor?b}", _inlineConstraintResolver),
"The route parameter name 'foor?b' is invalid. Route parameter names must be non-empty and cannot contain these characters: '{', '}', '/'. " + "The route parameter name 'foor?b' is invalid. Route parameter names must be non-empty and cannot contain these characters: '{', '}', '/'. " +
"The '?' character marks a parameter as optional, and can only occur at the end of the parameter." + Environment.NewLine + "The '?' character marks a parameter as optional, and can only occur at the end of the parameter." + Environment.NewLine +
"Parameter name: routeTemplate"); "Parameter name: routeTemplate");
@ -393,7 +427,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
public void InvalidTemplate_MultiSegmentParameterCannotContainOptionalParameter() public void InvalidTemplate_MultiSegmentParameterCannotContainOptionalParameter()
{ {
ExceptionAssert.Throws<ArgumentException>( ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("{foorb?}-bar-{z}"), () => TemplateParser.Parse("{foorb?}-bar-{z}", _inlineConstraintResolver),
"A path segment that contains more than one section, such as a literal section or a parameter, cannot contain an optional parameter." + Environment.NewLine + "A path segment that contains more than one section, such as a literal section or a parameter, cannot contain an optional parameter." + Environment.NewLine +
"Parameter name: routeTemplate"); "Parameter name: routeTemplate");
} }
@ -402,7 +436,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
public void InvalidTemplate_CatchAllMarkedOptional() public void InvalidTemplate_CatchAllMarkedOptional()
{ {
ExceptionAssert.Throws<ArgumentException>( ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("{a}/{*b?}"), () => TemplateParser.Parse("{a}/{*b?}", _inlineConstraintResolver),
"A catch-all parameter cannot be marked optional." + Environment.NewLine + "A catch-all parameter cannot be marked optional." + Environment.NewLine +
"Parameter name: routeTemplate"); "Parameter name: routeTemplate");
} }

View File

@ -6,6 +6,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNet.Http; using Microsoft.AspNet.Http;
using Microsoft.AspNet.Routing.Constraints;
using Microsoft.AspNet.Testing; using Microsoft.AspNet.Testing;
using Moq; using Moq;
using Xunit; using Xunit;
@ -463,22 +464,34 @@ namespace Microsoft.AspNet.Routing.Template.Tests
private static TemplateRoute CreateRoute(string template, bool accept = true) private static TemplateRoute CreateRoute(string template, bool accept = true)
{ {
return new TemplateRoute(CreateTarget(accept), template); return new TemplateRoute(CreateTarget(accept), template, new DefaultInlineConstraintResolver());
} }
private static TemplateRoute CreateRoute(string template, object defaults, bool accept = true, IDictionary<string, object> constraints = null) private static TemplateRoute CreateRoute(string template, object defaults, bool accept = true, IDictionary<string, object> constraints = null)
{ {
return new TemplateRoute(CreateTarget(accept), template, new RouteValueDictionary(defaults), constraints); return new TemplateRoute(CreateTarget(accept),
template,
new RouteValueDictionary(defaults),
constraints,
new DefaultInlineConstraintResolver());
} }
private static TemplateRoute CreateRoute(IRouter target, string template) private static TemplateRoute CreateRoute(IRouter target, string template)
{ {
return new TemplateRoute(target, template, new RouteValueDictionary(), constraints: null); return new TemplateRoute(target,
template,
new RouteValueDictionary(),
constraints: null,
inlineConstraintResolver: new DefaultInlineConstraintResolver());
} }
private static TemplateRoute CreateRoute(IRouter target, string template, object defaults) private static TemplateRoute CreateRoute(IRouter target, string template, object defaults)
{ {
return new TemplateRoute(target, template, new RouteValueDictionary(defaults), constraints: null); return new TemplateRoute(target,
template,
new RouteValueDictionary(defaults),
constraints: null,
inlineConstraintResolver: new DefaultInlineConstraintResolver());
} }
private static IRouter CreateTarget(bool accept = true) private static IRouter CreateTarget(bool accept = true)