Fix for #92 - Make Constraints, DataTokens, and Defaults readonly

The properties on TemplateRoute for DataTokens and Defaults are now
readonly. This prevents modifying these collections in a way that
invalidates cached data, or violates thread-safety.

To do the same for constraints, this change includes a substantial refactor
of how we realize inline constraints, and moves the constraint resolver
out of the parsing phase.

This allow creates a builder for the constraint map, that will make it
easier to implement features like optional constraints, and is reusable
for anyone building their own type of routing system.
This commit is contained in:
Ryan Nowak 2014-11-11 14:21:01 -08:00
parent 72604cb327
commit 01345eca91
20 changed files with 625 additions and 490 deletions

View File

@ -27,8 +27,8 @@ namespace Microsoft.AspNet.Routing
private static readonly Regex _parameterRegex = new Regex( private static readonly Regex _parameterRegex = new Regex(
"^" + ParameterNamePattern + ConstraintPattern + DefaultValueParameter + "$", "^" + ParameterNamePattern + ConstraintPattern + DefaultValueParameter + "$",
RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
public static TemplatePart ParseRouteParameter([NotNull] string routeParameter,
[NotNull] IInlineConstraintResolver constraintResolver) public static TemplatePart ParseRouteParameter([NotNull] string routeParameter)
{ {
var isCatchAll = routeParameter.StartsWith("*", StringComparison.Ordinal); var isCatchAll = routeParameter.StartsWith("*", StringComparison.Ordinal);
var isOptional = routeParameter.EndsWith("?", StringComparison.Ordinal); var isOptional = routeParameter.EndsWith("?", StringComparison.Ordinal);
@ -43,7 +43,7 @@ namespace Microsoft.AspNet.Routing
isCatchAll: isCatchAll, isCatchAll: isCatchAll,
isOptional: isOptional, isOptional: isOptional,
defaultValue: null, defaultValue: null,
inlineConstraint: null); inlineConstraints: null);
} }
var parameterName = parameterMatch.Groups["parameterName"].Value; var parameterName = parameterMatch.Groups["parameterName"].Value;
@ -54,13 +54,13 @@ namespace Microsoft.AspNet.Routing
// Register inline constraints if present // Register inline constraints if present
var constraintGroup = parameterMatch.Groups["constraint"]; var constraintGroup = parameterMatch.Groups["constraint"];
var inlineConstraint = GetInlineConstraint(constraintGroup, constraintResolver); var inlineConstraints = GetInlineConstraints(constraintGroup);
return TemplatePart.CreateParameter(parameterName, return TemplatePart.CreateParameter(parameterName,
isCatchAll, isCatchAll,
isOptional, isOptional,
defaultValue, defaultValue,
inlineConstraint); inlineConstraints);
} }
private static string GetDefaultValue(Group defaultValueGroup) private static string GetDefaultValue(Group defaultValueGroup)
@ -77,33 +77,16 @@ namespace Microsoft.AspNet.Routing
return null; return null;
} }
private static IRouteConstraint GetInlineConstraint(Group constraintGroup, private static IEnumerable<InlineConstraint> GetInlineConstraints(Group constraintGroup)
IInlineConstraintResolver _constraintResolver)
{ {
var parameterConstraints = new List<IRouteConstraint>(); var constraints = new List<InlineConstraint>();
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); foreach (Capture capture in constraintGroup.Captures)
{
constraints.Add(new InlineConstraint(capture.Value));
} }
if (parameterConstraints.Count > 0) return constraints;
{
var constraint = parameterConstraints.Count == 1 ?
parameterConstraints[0] :
new CompositeRouteConstraint(parameterConstraints);
return constraint;
}
return null;
} }
} }
} }

View File

@ -41,7 +41,7 @@ namespace Microsoft.AspNet.Routing.Logging
/// <summary> /// <summary>
/// The values produced by default. /// The values produced by default.
/// </summary> /// </summary>
public IDictionary<string, object> DefaultValues { get; set; } public IReadOnlyDictionary<string, object> DefaultValues { get; set; }
/// <summary> /// <summary>
/// The values produced from the request. /// The values produced from the request.
@ -51,7 +51,7 @@ namespace Microsoft.AspNet.Routing.Logging
/// <summary> /// <summary>
/// The constraints matched on the produced values. /// The constraints matched on the produced values.
/// </summary> /// </summary>
public IDictionary<string, IRouteConstraint> Constraints { get; set; } public IReadOnlyDictionary<string, IRouteConstraint> Constraints { get; set; }
/// <summary> /// <summary>
/// True if the <see cref="ProducedValues"/> matched. /// True if the <see cref="ProducedValues"/> matched.

View File

@ -138,22 +138,6 @@ namespace Microsoft.AspNet.Routing
return string.Format(CultureInfo.CurrentCulture, GetString("DefaultInlineConstraintResolver_TypeNotConstraint"), p0, p1, p2); return string.Format(CultureInfo.CurrentCulture, GetString("DefaultInlineConstraintResolver_TypeNotConstraint"), p0, p1, p2);
} }
/// <summary>
/// The constraint entry '{0}' must have a string value or be of a type which implements '{1}'.
/// </summary>
internal static string GeneralConstraints_ValidationMustBeStringOrCustomConstraint
{
get { return GetString("GeneralConstraints_ValidationMustBeStringOrCustomConstraint"); }
}
/// <summary>
/// The constraint entry '{0}' must have a string value or be of a type which implements '{1}'.
/// </summary>
internal static string FormatGeneralConstraints_ValidationMustBeStringOrCustomConstraint(object p0, object p1)
{
return string.Format(CultureInfo.CurrentCulture, GetString("GeneralConstraints_ValidationMustBeStringOrCustomConstraint"), p0, p1);
}
/// <summary> /// <summary>
/// A path segment that contains more than one section, such as a literal section or a parameter, cannot contain a catch-all parameter. /// A path segment that contains more than one section, such as a literal section or a parameter, cannot contain a catch-all parameter.
/// </summary> /// </summary>
@ -363,35 +347,35 @@ namespace Microsoft.AspNet.Routing
} }
/// <summary> /// <summary>
/// The constraint entry '{0}' on the route with route template '{1}' must have a string value or be of a type which implements '{2}'. /// The constraint entry '{0}' - '{1}' on route '{2}' must have a string value or be of a type which implements '{3}'.
/// </summary> /// </summary>
internal static string TemplateRoute_ValidationMustBeStringOrCustomConstraint internal static string RouteConstraintBuilder_ValidationMustBeStringOrCustomConstraint
{ {
get { return GetString("TemplateRoute_ValidationMustBeStringOrCustomConstraint"); } get { return GetString("RouteConstraintBuilder_ValidationMustBeStringOrCustomConstraint"); }
} }
/// <summary> /// <summary>
/// The constraint entry '{0}' on the route with route template '{1}' must have a string value or be of a type which implements '{2}'. /// The constraint entry '{0}' - '{1}' on route '{2}' must have a string value or be of a type which implements '{3}'.
/// </summary> /// </summary>
internal static string FormatTemplateRoute_ValidationMustBeStringOrCustomConstraint(object p0, object p1, object p2) internal static string FormatRouteConstraintBuilder_ValidationMustBeStringOrCustomConstraint(object p0, object p1, object p2, object p3)
{ {
return string.Format(CultureInfo.CurrentCulture, GetString("TemplateRoute_ValidationMustBeStringOrCustomConstraint"), p0, p1, p2); return string.Format(CultureInfo.CurrentCulture, GetString("RouteConstraintBuilder_ValidationMustBeStringOrCustomConstraint"), p0, p1, p2, p3);
} }
/// <summary> /// <summary>
/// The inline constraint resolver of type '{0}' was unable to resolve the following inline constraint: '{1}'. /// The constraint entry '{0}' - '{1}' on route '{2}' could not be resolved by the constraint resolver of type '{3}'.
/// </summary> /// </summary>
internal static string InlineRouteParser_CouldNotResolveConstraint internal static string RouteConstraintBuilder_CouldNotResolveConstraint
{ {
get { return GetString("InlineRouteParser_CouldNotResolveConstraint"); } get { return GetString("RouteConstraintBuilder_CouldNotResolveConstraint"); }
} }
/// <summary> /// <summary>
/// The inline constraint resolver of type '{0}' was unable to resolve the following inline constraint: '{1}'. /// The constraint entry '{0}' - '{1}' on route '{2}' could not be resolved by the constraint resolver of type '{3}'.
/// </summary> /// </summary>
internal static string FormatInlineRouteParser_CouldNotResolveConstraint(object p0, object p1) internal static string FormatRouteConstraintBuilder_CouldNotResolveConstraint(object p0, object p1, object p2, object p3)
{ {
return string.Format(CultureInfo.CurrentCulture, GetString("InlineRouteParser_CouldNotResolveConstraint"), p0, p1); return string.Format(CultureInfo.CurrentCulture, GetString("RouteConstraintBuilder_CouldNotResolveConstraint"), p0, p1, p2, p3);
} }
private static string GetString(string name, params string[] formatterNames) private static string GetString(string name, params string[] formatterNames)

View File

@ -141,9 +141,6 @@
<data name="DefaultInlineConstraintResolver_TypeNotConstraint" xml:space="preserve"> <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> <value>The constraint type '{0}' which is mapped to constraint key '{1}' must implement the '{2}' interface.</value>
</data> </data>
<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>
</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>
@ -183,10 +180,10 @@
<data name="TemplateRoute_RepeatedParameter" xml:space="preserve"> <data name="TemplateRoute_RepeatedParameter" xml:space="preserve">
<value>The route parameter name '{0}' appears more than one time in the route template.</value> <value>The route parameter name '{0}' appears more than one time in the route template.</value>
</data> </data>
<data name="TemplateRoute_ValidationMustBeStringOrCustomConstraint" xml:space="preserve"> <data name="RouteConstraintBuilder_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}' - '{1}' on the route '{2}' must have a string value or be of a type which implements '{3}'.</value>
</data> </data>
<data name="InlineRouteParser_CouldNotResolveConstraint" xml:space="preserve"> <data name="RouteConstraintBuilder_CouldNotResolveConstraint" xml:space="preserve">
<value>The inline constraint resolver of type '{0}' was unable to resolve the following inline constraint: '{1}'.</value> <value>The constraint entry '{0}' - '{1}' on the route '{2}' could not be resolved by the constraint resolver of type '{3}'.</value>
</data> </data>
</root> </root>

View File

@ -7,58 +7,52 @@ using Microsoft.AspNet.Routing.Constraints;
namespace Microsoft.AspNet.Routing namespace Microsoft.AspNet.Routing
{ {
/// <summary>
/// A builder for produding a mapping of keys to see <see cref="IRouteConstraint"/>.
/// </summary>
/// <remarks>
/// <see cref="RouteConstraintBuilder"/> allows iterative building a set of route constraints, and will
/// merge multiple entries for the same key.
/// </remarks>
public class RouteConstraintBuilder public class RouteConstraintBuilder
{ {
public static IDictionary<string, IRouteConstraint> private readonly IInlineConstraintResolver _inlineConstraintResolver;
BuildConstraints(IDictionary<string, object> inputConstraints) private readonly string _displayName;
private readonly Dictionary<string, List<IRouteConstraint>> _constraints;
/// <summary>
/// Creates a new <see cref="RouteConstraintBuilder"/> instance.
/// </summary>
/// <param name="inlineConstraintResolver">The <see cref="IInlineConstraintResolver"/>.</param>
/// <param name="displayName">The display name (for use in error messages).</param>
public RouteConstraintBuilder(
[NotNull] IInlineConstraintResolver inlineConstraintResolver,
[NotNull] string displayName)
{ {
return BuildConstraintsCore(inputConstraints, routeTemplate: null); _inlineConstraintResolver = inlineConstraintResolver;
_displayName = displayName;
_constraints = new Dictionary<string, List<IRouteConstraint>>(StringComparer.OrdinalIgnoreCase);
} }
public static IDictionary<string, IRouteConstraint> /// <summary>
BuildConstraints(IDictionary<string, object> inputConstraints, [NotNull] string routeTemplate) /// Builds a mapping of constraints.
/// </summary>
/// <returns>An <see cref="IReadOnlyDictionary{string, IRouteConstraint}"/> of the constraints.</returns>
public IReadOnlyDictionary<string, IRouteConstraint> Build()
{ {
return BuildConstraintsCore(inputConstraints, routeTemplate); var constraints = new Dictionary<string, IRouteConstraint>(StringComparer.OrdinalIgnoreCase);
} foreach (var kvp in _constraints)
private static IDictionary<string, IRouteConstraint>
BuildConstraintsCore(IDictionary<string, object> inputConstraints, string routeTemplate)
{
if (inputConstraints == null || inputConstraints.Count == 0)
{ {
return null; IRouteConstraint constraint;
} if (kvp.Value.Count == 1)
var constraints = new Dictionary<string, IRouteConstraint>(inputConstraints.Count,
StringComparer.OrdinalIgnoreCase);
foreach (var kvp in inputConstraints)
{
var constraint = kvp.Value as IRouteConstraint;
if (constraint == null)
{ {
var regexPattern = kvp.Value as string; constraint = kvp.Value[0];
}
if (regexPattern == null) else
{ {
if (routeTemplate != null) constraint = new CompositeRouteConstraint(kvp.Value.ToArray());
{
throw new InvalidOperationException(
Resources.FormatTemplateRoute_ValidationMustBeStringOrCustomConstraint(
kvp.Key, routeTemplate, typeof(IRouteConstraint)));
}
else
{
throw new InvalidOperationException(
Resources.FormatGeneralConstraints_ValidationMustBeStringOrCustomConstraint(
kvp.Key, typeof(IRouteConstraint)));
}
}
var constraintsRegEx = "^(" + regexPattern + ")$";
constraint = new RegexRouteConstraint(constraintsRegEx);
} }
constraints.Add(kvp.Key, constraint); constraints.Add(kvp.Key, constraint);
@ -66,5 +60,79 @@ namespace Microsoft.AspNet.Routing
return constraints; return constraints;
} }
/// <summary>
/// Adds a constraint instance for the given key.
/// </summary>
/// <param name="key">The key.</param>
/// <param name="value">
/// The constraint instance. Must either be a string or an instance of <see cref="IRouteConstraint"/>.
/// </param>
/// <remarks>
/// If the <paramref name="value"/> is a string, it will be converted to a <see cref="RegexRouteConstraint"/>.
///
/// For example, the string <code>Product[0-9]+</code> will be converted to the regular expression
/// <code>^(Product[0-9]+)</code>. See <see cref="System.Text.RegularExpressions.Regex"/> for more details.
/// </remarks>
public void AddConstraint([NotNull] string key, [NotNull] object value)
{
var constraint = value as IRouteConstraint;
if (constraint == null)
{
var regexPattern = value as string;
if (regexPattern == null)
{
throw new InvalidOperationException(
Resources.FormatRouteConstraintBuilder_ValidationMustBeStringOrCustomConstraint(
key,
value,
_displayName,
typeof(IRouteConstraint)));
}
var constraintsRegEx = "^(" + regexPattern + ")$";
constraint = new RegexRouteConstraint(constraintsRegEx);
}
Add(key, constraint);
}
/// <summary>
/// Adds a constraint for the given key, resolved by the <see cref="IInlineConstraintResolver"/>.
/// </summary>
/// <param name="key">The key.</param>
/// <param name="constraintText">The text to be resolved by <see cref="IInlineConstraintResolver"/>.</param>
/// <remarks>
/// The <see cref="IInlineConstraintResolver"/> can create <see cref="IRouteConstraint"/> instances
/// based on <paramref name="constraintText"/>. See <see cref="RouteOptions.ConstraintMap"/> to register
/// custom constraint types.
/// </remarks>
public void AddResolvedConstraint([NotNull] string key, [NotNull] string constraintText)
{
var constraint = _inlineConstraintResolver.ResolveConstraint(constraintText);
if (constraint == null)
{
throw new InvalidOperationException(
Resources.FormatRouteConstraintBuilder_CouldNotResolveConstraint(
key,
constraintText,
_displayName,
_inlineConstraintResolver.GetType().Name));
}
Add(key, constraint);
}
private void Add(string key, IRouteConstraint constraint)
{
List<IRouteConstraint> list;
if (!_constraints.TryGetValue(key, out list))
{
list = new List<IRouteConstraint>();
_constraints.Add(key, list);
}
list.Add(constraint);
}
} }
} }

View File

@ -11,7 +11,7 @@ namespace Microsoft.AspNet.Routing
{ {
public static class RouteConstraintMatcher public static class RouteConstraintMatcher
{ {
public static bool Match(IDictionary<string, IRouteConstraint> constraints, public static bool Match(IReadOnlyDictionary<string, IRouteConstraint> constraints,
[NotNull] IDictionary<string, object> routeValues, [NotNull] IDictionary<string, object> routeValues,
[NotNull] HttpContext httpContext, [NotNull] HttpContext httpContext,
[NotNull] IRouter route, [NotNull] IRouter route,

View File

@ -12,8 +12,13 @@ namespace Microsoft.AspNet.Routing
/// <summary> /// <summary>
/// An <see cref="IDictionary{string, object}"/> type for route values. /// An <see cref="IDictionary{string, object}"/> type for route values.
/// </summary> /// </summary>
public class RouteValueDictionary : IDictionary<string, object> public class RouteValueDictionary : IDictionary<string, object>, IReadOnlyDictionary<string, object>
{ {
/// <summary>
/// An empty, cached instance of <see cref="RouteValueDictionary"/>.
/// </summary>
internal static readonly IReadOnlyDictionary<string, object> Empty = new RouteValueDictionary();
private readonly Dictionary<string, object> _dictionary; private readonly Dictionary<string, object> _dictionary;
/// <summary> /// <summary>
@ -139,6 +144,14 @@ namespace Microsoft.AspNet.Routing
} }
} }
IEnumerable<string> IReadOnlyDictionary<string, object>.Keys
{
get
{
return _dictionary.Keys;
}
}
/// <inheritdoc /> /// <inheritdoc />
public Dictionary<string, object>.ValueCollection Values public Dictionary<string, object>.ValueCollection Values
{ {
@ -157,6 +170,14 @@ namespace Microsoft.AspNet.Routing
} }
} }
IEnumerable<object> IReadOnlyDictionary<string, object>.Values
{
get
{
return _dictionary.Values;
}
}
/// <inheritdoc /> /// <inheritdoc />
void ICollection<KeyValuePair<string, object>>.Add(KeyValuePair<string, object> item) void ICollection<KeyValuePair<string, object>>.Add(KeyValuePair<string, object> item)
{ {

View File

@ -0,0 +1,25 @@
// 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.Template
{
/// <summary>
/// The parsed representation of an inline constraint in a route parameter.
/// </summary>
public class InlineConstraint
{
/// <summary>
/// Creates a new <see cref="InlineConstraint"/>.
/// </summary>
/// <param name="constraint">The constraint text.</param>
public InlineConstraint([NotNull] string constraint)
{
Constraint = constraint;
}
/// <summary>
/// Gets the constraint text.
/// </summary>
public string Constraint { get; }
}
}

View File

@ -14,10 +14,10 @@ namespace Microsoft.AspNet.Routing.Template
{ {
public class TemplateBinder public class TemplateBinder
{ {
private readonly IDictionary<string, object> _defaults; private readonly IReadOnlyDictionary<string, object> _defaults;
private readonly RouteTemplate _template; private readonly RouteTemplate _template;
public TemplateBinder(RouteTemplate template, IDictionary<string, object> defaults) public TemplateBinder(RouteTemplate template, IReadOnlyDictionary<string, object> defaults)
{ {
if (template == null) if (template == null)
{ {
@ -336,12 +336,14 @@ namespace Microsoft.AspNet.Routing.Template
[DebuggerDisplay("{DebuggerToString(),nq}")] [DebuggerDisplay("{DebuggerToString(),nq}")]
private class TemplateBindingContext private class TemplateBindingContext
{ {
private readonly IDictionary<string, object> _defaults; private readonly IReadOnlyDictionary<string, object> _defaults;
private readonly RouteValueDictionary _acceptedValues; private readonly RouteValueDictionary _acceptedValues;
private readonly RouteValueDictionary _filters; private readonly RouteValueDictionary _filters;
public TemplateBindingContext(IDictionary<string, object> defaults, IDictionary<string, object> values) public TemplateBindingContext(
IReadOnlyDictionary<string, object> defaults,
IDictionary<string, object> values)
{ {
if (values == null) if (values == null)
{ {
@ -355,6 +357,10 @@ namespace Microsoft.AspNet.Routing.Template
if (_defaults != null) if (_defaults != null)
{ {
_filters = new RouteValueDictionary(defaults); _filters = new RouteValueDictionary(defaults);
foreach (var kvp in _defaults)
{
_filters.Add(kvp.Key, kvp.Value);
}
} }
} }

View File

@ -14,27 +14,22 @@ namespace Microsoft.AspNet.Routing.Template
private static readonly char[] Delimiters = new char[] { SeparatorChar }; private static readonly char[] Delimiters = new char[] { SeparatorChar };
public TemplateMatcher(RouteTemplate template) public TemplateMatcher(
[NotNull] RouteTemplate template,
[NotNull] IReadOnlyDictionary<string, object> defaults)
{ {
if (template == null)
{
throw new ArgumentNullException("template");
}
Template = template; Template = template;
Defaults = defaults ?? RouteValueDictionary.Empty;
} }
public IReadOnlyDictionary<string, object> Defaults { get; private set; }
public RouteTemplate Template { get; private set; } public RouteTemplate Template { get; private set; }
public IDictionary<string, object> Match(string requestPath, IDictionary<string, object> defaults) public IDictionary<string, object> Match([NotNull] string requestPath)
{ {
var requestSegments = requestPath.Split(Delimiters); var requestSegments = requestPath.Split(Delimiters);
if (defaults == null)
{
defaults = new RouteValueDictionary();
}
var values = new RouteValueDictionary(); var values = new RouteValueDictionary();
for (var i = 0; i < requestSegments.Length; i++) for (var i = 0; i < requestSegments.Length; i++)
@ -77,7 +72,7 @@ namespace Microsoft.AspNet.Routing.Template
{ {
// It's ok for a catch-all to produce a null value // It's ok for a catch-all to produce a null value
object defaultValue; object defaultValue;
defaults.TryGetValue(part.Name, out defaultValue); Defaults.TryGetValue(part.Name, out defaultValue);
values.Add(part.Name, defaultValue); values.Add(part.Name, defaultValue);
} }
@ -94,7 +89,7 @@ namespace Microsoft.AspNet.Routing.Template
else else
{ {
object defaultValue; object defaultValue;
if (defaults.TryGetValue(part.Name, out defaultValue)) if (Defaults.TryGetValue(part.Name, out defaultValue))
{ {
values.Add(part.Name, defaultValue); values.Add(part.Name, defaultValue);
} }
@ -114,7 +109,7 @@ namespace Microsoft.AspNet.Routing.Template
} }
else else
{ {
if (!MatchComplexSegment(routeSegment, requestSegment, defaults, values)) if (!MatchComplexSegment(routeSegment, requestSegment, Defaults, values))
{ {
return null; return null;
} }
@ -142,7 +137,7 @@ namespace Microsoft.AspNet.Routing.Template
// It's ok for a catch-all to produce a null value // It's ok for a catch-all to produce a null value
object defaultValue; object defaultValue;
if (defaults.TryGetValue(part.Name, out defaultValue) || part.IsCatchAll) if (Defaults.TryGetValue(part.Name, out defaultValue) || part.IsCatchAll)
{ {
values.Add(part.Name, defaultValue); values.Add(part.Name, defaultValue);
} }
@ -158,14 +153,11 @@ namespace Microsoft.AspNet.Routing.Template
} }
// Copy all remaining default values to the route data // Copy all remaining default values to the route data
if (defaults != null) foreach (var kvp in Defaults)
{ {
foreach (var kvp in defaults) if (!values.ContainsKey(kvp.Key))
{ {
if (!values.ContainsKey(kvp.Key)) values.Add(kvp.Key, kvp.Value);
{
values.Add(kvp.Key, kvp.Value);
}
} }
} }
@ -174,7 +166,7 @@ namespace Microsoft.AspNet.Routing.Template
private bool MatchComplexSegment(TemplateSegment routeSegment, private bool MatchComplexSegment(TemplateSegment routeSegment,
string requestSegment, string requestSegment,
IDictionary<string, object> defaults, IReadOnlyDictionary<string, object> defaults,
RouteValueDictionary values) RouteValueDictionary values)
{ {
Contract.Assert(routeSegment != null); Contract.Assert(routeSegment != null);
@ -214,7 +206,7 @@ namespace Microsoft.AspNet.Routing.Template
return false; return false;
} }
var indexOfLiteral = requestSegment.LastIndexOf(part.Text, var indexOfLiteral = requestSegment.LastIndexOf(part.Text,
startIndex, startIndex,
StringComparison.OrdinalIgnoreCase); StringComparison.OrdinalIgnoreCase);
if (indexOfLiteral == -1) if (indexOfLiteral == -1)
@ -237,7 +229,7 @@ namespace Microsoft.AspNet.Routing.Template
newLastIndex = indexOfLiteral; newLastIndex = indexOfLiteral;
} }
if ((parameterNeedsValue != null) && if ((parameterNeedsValue != null) &&
(((lastLiteral != null) && (part.IsLiteral)) || (indexOfLastSegmentUsed == 0))) (((lastLiteral != null) && (part.IsLiteral)) || (indexOfLastSegmentUsed == 0)))
{ {
// If we have a pending parameter that needs a value, grab that value // If we have a pending parameter that needs a value, grab that value

View File

@ -17,7 +17,7 @@ namespace Microsoft.AspNet.Routing.Template
private const char QuestionMark = '?'; private const char QuestionMark = '?';
private const char Asterisk = '*'; private const char Asterisk = '*';
public static RouteTemplate Parse(string routeTemplate, IInlineConstraintResolver constraintResolver) public static RouteTemplate Parse(string routeTemplate)
{ {
if (routeTemplate == null) if (routeTemplate == null)
{ {
@ -29,7 +29,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, constraintResolver); var context = new TemplateParserContext(routeTemplate);
var segments = new List<TemplateSegment>(); var segments = new List<TemplateSegment>();
while (context.Next()) while (context.Next())
@ -178,8 +178,7 @@ namespace Microsoft.AspNet.Routing.Template
// At this point, we need to parse the raw name for inline constraint, // At this point, we need to parse the raw name for inline constraint,
// default values and optional parameters. // default values and optional parameters.
var templatePart = InlineRouteParameterParser.ParseRouteParameter(rawParameter, var templatePart = InlineRouteParameterParser.ParseRouteParameter(rawParameter);
context.ConstraintResolver);
if (templatePart.IsCatchAll && templatePart.IsOptional) if (templatePart.IsCatchAll && templatePart.IsOptional)
{ {
@ -406,13 +405,12 @@ 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, IInlineConstraintResolver constraintResolver) public TemplateParserContext(string template)
{ {
Contract.Assert(template != null); Contract.Assert(template != null);
_template = template; _template = template;
_index = -1; _index = -1;
ConstraintResolver = constraintResolver;
} }
public char Current public char Current
@ -431,12 +429,6 @@ 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

@ -1,7 +1,9 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq;
namespace Microsoft.AspNet.Routing.Template namespace Microsoft.AspNet.Routing.Template
{ {
@ -21,7 +23,7 @@ namespace Microsoft.AspNet.Routing.Template
bool isCatchAll, bool isCatchAll,
bool isOptional, bool isOptional,
object defaultValue, object defaultValue,
IRouteConstraint inlineConstraint) IEnumerable<InlineConstraint> inlineConstraints)
{ {
return new TemplatePart() return new TemplatePart()
{ {
@ -30,7 +32,7 @@ namespace Microsoft.AspNet.Routing.Template
IsCatchAll = isCatchAll, IsCatchAll = isCatchAll,
IsOptional = isOptional, IsOptional = isOptional,
DefaultValue = defaultValue, DefaultValue = defaultValue,
InlineConstraint = inlineConstraint, InlineConstraints = inlineConstraints ?? Enumerable.Empty<InlineConstraint>(),
}; };
} }
@ -41,7 +43,7 @@ namespace Microsoft.AspNet.Routing.Template
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 object DefaultValue { get; private set; }
public IRouteConstraint InlineConstraint { get; private set; } public IEnumerable<InlineConstraint> InlineConstraints { get; private set; }
internal string DebuggerToString() internal string DebuggerToString()
{ {

View File

@ -14,9 +14,9 @@ namespace Microsoft.AspNet.Routing.Template
{ {
public class TemplateRoute : INamedRouter public class TemplateRoute : INamedRouter
{ {
private readonly IDictionary<string, object> _defaults; private readonly IReadOnlyDictionary<string, IRouteConstraint> _constraints;
private readonly IDictionary<string, IRouteConstraint> _constraints; private readonly IReadOnlyDictionary<string, object> _dataTokens;
private readonly IDictionary<string, object> _dataTokens; private readonly IReadOnlyDictionary<string, object> _defaults;
private readonly IRouter _target; private readonly IRouter _target;
private readonly RouteTemplate _parsedTemplate; private readonly RouteTemplate _parsedTemplate;
private readonly string _routeTemplate; private readonly string _routeTemplate;
@ -25,7 +25,10 @@ namespace Microsoft.AspNet.Routing.Template
private ILogger _logger; private ILogger _logger;
private ILogger _constraintLogger; private ILogger _constraintLogger;
public TemplateRoute(IRouter target, string routeTemplate, IInlineConstraintResolver inlineConstraintResolver) public TemplateRoute(
[NotNull] IRouter target,
string routeTemplate,
IInlineConstraintResolver inlineConstraintResolver)
: this(target, : this(target,
routeTemplate, routeTemplate,
defaults: null, defaults: null,
@ -56,27 +59,28 @@ namespace Microsoft.AspNet.Routing.Template
_target = target; _target = target;
_routeTemplate = routeTemplate ?? string.Empty; _routeTemplate = routeTemplate ?? string.Empty;
Name = routeName; Name = routeName;
_defaults = defaults ?? new RouteValueDictionary();
_constraints = RouteConstraintBuilder.BuildConstraints(constraints, _routeTemplate) ??
new Dictionary<string, IRouteConstraint>();
_dataTokens = dataTokens ?? new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
// The parser will throw for invalid routes. _dataTokens = dataTokens == null ? RouteValueDictionary.Empty : new RouteValueDictionary(dataTokens);
_parsedTemplate = TemplateParser.Parse(RouteTemplate, inlineConstraintResolver);
UpdateInlineDefaultValuesAndConstraints();
_matcher = new TemplateMatcher(_parsedTemplate); // Data we parse from the template will be used to fill in the rest of the constraints or
_binder = new TemplateBinder(_parsedTemplate, _defaults); // defaults. The parser will throw for invalid routes.
_parsedTemplate = TemplateParser.Parse(RouteTemplate);
_constraints = GetConstraints(inlineConstraintResolver, RouteTemplate, _parsedTemplate, constraints);
_defaults = GetDefaults(_parsedTemplate, defaults);
_matcher = new TemplateMatcher(_parsedTemplate, Defaults);
_binder = new TemplateBinder(_parsedTemplate, Defaults);
} }
public string Name { get; private set; } public string Name { get; private set; }
public IDictionary<string, object> Defaults public IReadOnlyDictionary<string, object> Defaults
{ {
get { return _defaults; } get { return _defaults; }
} }
public IDictionary<string, object> DataTokens public IReadOnlyDictionary<string, object> DataTokens
{ {
get { return _dataTokens; } get { return _dataTokens; }
} }
@ -86,7 +90,7 @@ namespace Microsoft.AspNet.Routing.Template
get { return _routeTemplate; } get { return _routeTemplate; }
} }
public IDictionary<string, IRouteConstraint> Constraints public IReadOnlyDictionary<string, IRouteConstraint> Constraints
{ {
get { return _constraints; } get { return _constraints; }
} }
@ -103,7 +107,7 @@ namespace Microsoft.AspNet.Routing.Template
requestPath = requestPath.Substring(1); requestPath = requestPath.Substring(1);
} }
var values = _matcher.Match(requestPath, Defaults); var values = _matcher.Match(requestPath);
if (values == null) if (values == null)
{ {
@ -252,38 +256,59 @@ namespace Microsoft.AspNet.Routing.Template
}; };
} }
private void UpdateInlineDefaultValuesAndConstraints() private static IReadOnlyDictionary<string, IRouteConstraint> GetConstraints(
IInlineConstraintResolver inlineConstraintResolver,
string template,
RouteTemplate parsedTemplate,
IDictionary<string, object> constraints)
{ {
foreach (var parameter in _parsedTemplate.Parameters) var constraintBuilder = new RouteConstraintBuilder(inlineConstraintResolver, template);
{
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 (constraints != null)
{
foreach (var kvp in constraints)
{
constraintBuilder.AddConstraint(kvp.Key, kvp.Value);
}
}
foreach (var parameter in parsedTemplate.Parameters)
{
foreach (var inlineConstraint in parameter.InlineConstraints)
{
constraintBuilder.AddResolvedConstraint(parameter.Name, inlineConstraint.Constraint);
}
}
return constraintBuilder.Build();
}
private static RouteValueDictionary GetDefaults(
RouteTemplate parsedTemplate,
IDictionary<string, object> defaults)
{
// Do not use RouteValueDictionary.Empty for defaults, it might be modified inside
// UpdateInlineDefaultValuesAndConstraints()
var result = defaults == null ? new RouteValueDictionary() : new RouteValueDictionary(defaults);
foreach (var parameter in parsedTemplate.Parameters)
{
if (parameter.DefaultValue != null) if (parameter.DefaultValue != null)
{ {
if (_defaults.ContainsKey(parameter.Name)) if (result.ContainsKey(parameter.Name))
{ {
throw new InvalidOperationException( throw new InvalidOperationException(
Resources Resources.FormatTemplateRoute_CannotHaveDefaultValueSpecifiedInlineAndExplicitly(
.FormatTemplateRoute_CannotHaveDefaultValueSpecifiedInlineAndExplicitly(parameter.Name)); parameter.Name));
} }
else else
{ {
_defaults[parameter.Name] = parameter.DefaultValue; result.Add(parameter.Name, parameter.DefaultValue);
} }
} }
} }
return result;
} }
private TemplateRouteRouteAsyncValues CreateRouteAsyncValues( private TemplateRouteRouteAsyncValues CreateRouteAsyncValues(
@ -320,6 +345,20 @@ namespace Microsoft.AspNet.Routing.Template
} }
} }
// Needed because IDictionary<> is not an IReadOnlyDictionary<>
private static void MergeValues(
IDictionary<string, object> destination,
IReadOnlyDictionary<string, object> values)
{
foreach (var kvp in values)
{
// This will replace the original value for the specified key.
// Values from the matched route will take preference over previous
// data in the route context.
destination[kvp.Key] = kvp.Value;
}
}
private void EnsureLoggers(HttpContext context) private void EnsureLoggers(HttpContext context)
{ {
if (_logger == null) if (_logger == null)

View File

@ -1,142 +0,0 @@
// 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.
#if ASPNET50
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Routing.Constraints;
using Microsoft.AspNet.Testing;
using Moq;
using Xunit;
namespace Microsoft.AspNet.Routing.Tests
{
public class ConstraintsBuilderTests
{
[Theory]
[MemberData("EmptyAndNullDictionary")]
public void ConstraintBuilderReturnsNull_OnNullOrEmptyInput(IDictionary<string, object> input)
{
var result = RouteConstraintBuilder.BuildConstraints(input);
Assert.Null(result);
}
[Theory]
[MemberData("EmptyAndNullDictionary")]
public void ConstraintBuilderWithTemplateReturnsNull_OnNullOrEmptyInput(IDictionary<string, object> input)
{
var result = RouteConstraintBuilder.BuildConstraints(input, "{controller}");
Assert.Null(result);
}
[Fact]
public void GetRouteDataWithConstraintsThatIsAStringCreatesARegex()
{
// Arrange
var dictionary = new RouteValueDictionary(new { controller = "abc" });
var constraintDictionary = RouteConstraintBuilder.BuildConstraints(dictionary);
// Assert
Assert.Equal(1, constraintDictionary.Count);
Assert.Equal("controller", constraintDictionary.First().Key);
var constraint = constraintDictionary["controller"];
Assert.IsType<RegexRouteConstraint>(constraint);
}
[Fact]
public void GetRouteDataWithConstraintsThatIsCustomConstraint_IsPassThrough()
{
// Arrange
var originalConstraint = new Mock<IRouteConstraint>().Object;
var dictionary = new RouteValueDictionary(new { controller = originalConstraint });
var constraintDictionary = RouteConstraintBuilder.BuildConstraints(dictionary);
// Assert
Assert.Equal(1, constraintDictionary.Count);
Assert.Equal("controller", constraintDictionary.First().Key);
var constraint = constraintDictionary["controller"];
Assert.Equal(originalConstraint, constraint);
}
[Fact]
public void GetRouteDataWithConstraintsThatIsNotStringOrCustomConstraint_Throws()
{
// Arrange
var dictionary = new RouteValueDictionary(new { controller = new RouteValueDictionary() });
ExceptionAssert.Throws<InvalidOperationException>(
() => RouteConstraintBuilder.BuildConstraints(dictionary),
"The constraint entry 'controller' must have a string value or be of a type which implements '" +
typeof(IRouteConstraint) + "'.");
}
[Fact]
public void RouteTemplateGetRouteDataWithConstraintsThatIsNotStringOrCustomConstraint_Throws()
{
// Arrange
var dictionary = new RouteValueDictionary(new { controller = new RouteValueDictionary() });
ExceptionAssert.Throws<InvalidOperationException>(
() => RouteConstraintBuilder.BuildConstraints(dictionary, "{controller}/{action}"),
"The constraint entry 'controller' on the route with route template " +
"'{controller}/{action}' must have a string value or be of a type which implements '" +
typeof(IRouteConstraint) + "'.");
}
[Theory]
[InlineData("abc", "abc", true)] // simple case
[InlineData("abc", "bbb|abc", true)] // Regex or
[InlineData("Abc", "abc", true)] // Case insensitive
[InlineData("Abc ", "abc", false)] // Matches whole (but no trimming)
[InlineData("Abcd", "abc", false)] // Matches whole (additional non whitespace char)
[InlineData("Abc", " abc", false)] // Matches whole (less one char)
public void StringConstraintsMatchingScenarios(string routeValue,
string constraintValue,
bool shouldMatch)
{
// Arrange
var dictionary = new RouteValueDictionary(new { controller = routeValue });
var constraintDictionary = RouteConstraintBuilder.BuildConstraints(
new RouteValueDictionary(new { controller = constraintValue }));
var constraint = constraintDictionary["controller"];
Assert.Equal(shouldMatch,
constraint.Match(
httpContext: new Mock<HttpContext>().Object,
route: new Mock<IRouter>().Object,
routeKey: "controller",
values: dictionary,
routeDirection: RouteDirection.IncomingRequest));
}
public static IEnumerable<object> EmptyAndNullDictionary
{
get
{
return new[]
{
new Object[]
{
null,
},
new Object[]
{
new Dictionary<string, object>(),
},
};
}
}
}
}
#endif

View File

@ -5,7 +5,6 @@ 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.Routing.Template; using Microsoft.AspNet.Routing.Template;
using Microsoft.Framework.DependencyInjection; using Microsoft.Framework.DependencyInjection;
using Microsoft.Framework.DependencyInjection.Fallback; using Microsoft.Framework.DependencyInjection.Fallback;
@ -25,7 +24,9 @@ namespace Microsoft.AspNet.Routing.Tests
// Assert // Assert
Assert.Equal("param", templatePart.Name); Assert.Equal("param", templatePart.Name);
Assert.Equal("111111", templatePart.DefaultValue); Assert.Equal("111111", templatePart.DefaultValue);
Assert.IsType<IntRouteConstraint>(templatePart.InlineConstraint);
Assert.Single(templatePart.InlineConstraints);
Assert.Single(templatePart.InlineConstraints, c => c.Constraint == "int");
} }
[Fact] [Fact]
@ -37,8 +38,9 @@ namespace Microsoft.AspNet.Routing.Tests
// Assert // Assert
Assert.Equal("param", templatePart.Name); Assert.Equal("param", templatePart.Name);
Assert.Equal("111111", templatePart.DefaultValue); Assert.Equal("111111", templatePart.DefaultValue);
Assert.IsType<TestRouteConstraint>(templatePart.InlineConstraint);
Assert.Equal(@"\d+", ((TestRouteConstraint)templatePart.InlineConstraint).Pattern); Assert.Single(templatePart.InlineConstraints);
Assert.Single(templatePart.InlineConstraints, c => c.Constraint == @"test(\d+)");
} }
[Fact] [Fact]
@ -50,7 +52,9 @@ namespace Microsoft.AspNet.Routing.Tests
// Assert // Assert
Assert.Equal("param", templatePart.Name); Assert.Equal("param", templatePart.Name);
Assert.True(templatePart.IsOptional); Assert.True(templatePart.IsOptional);
Assert.IsType<IntRouteConstraint>(templatePart.InlineConstraint);
Assert.Single(templatePart.InlineConstraints);
Assert.Single(templatePart.InlineConstraints, c => c.Constraint == @"int");
} }
[Fact] [Fact]
@ -62,7 +66,9 @@ namespace Microsoft.AspNet.Routing.Tests
// Assert // Assert
Assert.Equal("param", templatePart.Name); Assert.Equal("param", templatePart.Name);
Assert.True(templatePart.IsOptional); Assert.True(templatePart.IsOptional);
Assert.Equal(@"\d+", ((TestRouteConstraint)templatePart.InlineConstraint).Pattern);
Assert.Single(templatePart.InlineConstraints);
Assert.Single(templatePart.InlineConstraints, c => c.Constraint == @"test(\d+)");
} }
[Fact] [Fact]
@ -73,10 +79,11 @@ namespace Microsoft.AspNet.Routing.Tests
// Assert // Assert
Assert.Equal("param", templatePart.Name); Assert.Equal("param", templatePart.Name);
Assert.IsType<CompositeRouteConstraint>(templatePart.InlineConstraint);
var constraint = (CompositeRouteConstraint)templatePart.InlineConstraint; Assert.Equal(2, templatePart.InlineConstraints.Count());
Assert.Equal(@"\d+", ((TestRouteConstraint)constraint.Constraints.ElementAt(0)).Pattern);
Assert.Equal(@"\w+", ((TestRouteConstraint)constraint.Constraints.ElementAt(1)).Pattern); Assert.Single(templatePart.InlineConstraints, c => c.Constraint == @"test(\d+)");
Assert.Single(templatePart.InlineConstraints, c => c.Constraint == @"test(\w+)");
} }
[Fact] [Fact]
@ -92,10 +99,10 @@ namespace Microsoft.AspNet.Routing.Tests
Assert.Equal("p1", param1.Name); Assert.Equal("p1", param1.Name);
Assert.Equal("hello", param1.DefaultValue); Assert.Equal("hello", param1.DefaultValue);
Assert.False(param1.IsOptional); Assert.False(param1.IsOptional);
Assert.IsType<CompositeRouteConstraint>(param1.InlineConstraint);
var constraint = (CompositeRouteConstraint)param1.InlineConstraint; Assert.Equal(2, param1.InlineConstraints.Count());
Assert.IsType<IntRouteConstraint>(constraint.Constraints.ElementAt(0)); Assert.Single(param1.InlineConstraints, c => c.Constraint == "int");
Assert.IsType<TestRouteConstraint>(constraint.Constraints.ElementAt(1)); Assert.Single(param1.InlineConstraints, c => c.Constraint == "test(3)");
var param2 = parameters[1]; var param2 = parameters[1];
Assert.Equal("p2", param2.Name); Assert.Equal("p2", param2.Name);
@ -136,8 +143,9 @@ namespace Microsoft.AspNet.Routing.Tests
// Assert // Assert
Assert.Equal("param", templatePart.Name); Assert.Equal("param", templatePart.Name);
Assert.IsType<TestRouteConstraint>(templatePart.InlineConstraint);
Assert.Equal(@"\}", ((TestRouteConstraint)templatePart.InlineConstraint).Pattern); Assert.Single(templatePart.InlineConstraints);
Assert.Single(templatePart.InlineConstraints, c => c.Constraint == @"test(\})");
} }
[Fact] [Fact]
@ -148,8 +156,9 @@ namespace Microsoft.AspNet.Routing.Tests
// Assert // Assert
Assert.Equal("param", templatePart.Name); Assert.Equal("param", templatePart.Name);
Assert.IsType<TestRouteConstraint>(templatePart.InlineConstraint);
Assert.Equal(@"\)", ((TestRouteConstraint)templatePart.InlineConstraint).Pattern); Assert.Single(templatePart.InlineConstraints);
Assert.Single(templatePart.InlineConstraints, c => c.Constraint == @"test(\))");
} }
[Fact] [Fact]
@ -160,8 +169,9 @@ namespace Microsoft.AspNet.Routing.Tests
// Assert // Assert
Assert.Equal("param", templatePart.Name); Assert.Equal("param", templatePart.Name);
Assert.IsType<TestRouteConstraint>(templatePart.InlineConstraint);
Assert.Equal(@":", ((TestRouteConstraint)templatePart.InlineConstraint).Pattern); Assert.Single(templatePart.InlineConstraints);
Assert.Single(templatePart.InlineConstraints, c => c.Constraint == @"test(:)");
} }
[Fact] [Fact]
@ -172,8 +182,9 @@ namespace Microsoft.AspNet.Routing.Tests
// Assert // Assert
Assert.Equal("param", templatePart.Name); Assert.Equal("param", templatePart.Name);
Assert.IsType<TestRouteConstraint>(templatePart.InlineConstraint);
Assert.Equal(@"\w,\w", ((TestRouteConstraint)templatePart.InlineConstraint).Pattern); Assert.Single(templatePart.InlineConstraints);
Assert.Single(templatePart.InlineConstraints, c => c.Constraint == @"test(\w,\w)");
} }
[Fact] [Fact]
@ -184,27 +195,10 @@ namespace Microsoft.AspNet.Routing.Tests
// Assert // Assert
Assert.Equal("param", templatePart.Name); Assert.Equal("param", templatePart.Name);
var constraint = Assert.IsType<IntRouteConstraint>(templatePart.InlineConstraint);
Assert.Equal("", templatePart.DefaultValue); Assert.Equal("", templatePart.DefaultValue);
}
[Theory] Assert.Single(templatePart.InlineConstraints);
[InlineData(",")] Assert.Single(templatePart.InlineConstraints, c => c.Constraint == @"int");
[InlineData("(")]
[InlineData(")")]
[InlineData("}")]
[InlineData("{")]
public void ParseRouteParameter_MisplacedSpecialCharacterInParameter_Throws(string character)
{
// Arrange
var unresolvedConstraint = character + @"test(\w,\w)";
var parameter = "param:" + unresolvedConstraint;
// Act & Assert
var ex = Assert.Throws<InvalidOperationException>(() => ParseParameter(parameter));
Assert.Equal(@"The inline constraint resolver of type 'DefaultInlineConstraintResolver'"+
" was unable to resolve the following inline constraint: '"+ unresolvedConstraint + "'.",
ex.Message);
} }
[Fact] [Fact]
@ -216,8 +210,9 @@ namespace Microsoft.AspNet.Routing.Tests
// Assert // Assert
Assert.Equal("param", templatePart.Name); Assert.Equal("param", templatePart.Name);
Assert.Null(templatePart.DefaultValue); Assert.Null(templatePart.DefaultValue);
Assert.IsType<TestRouteConstraint>(templatePart.InlineConstraint);
Assert.Equal(@"=", ((TestRouteConstraint)templatePart.InlineConstraint).Pattern); Assert.Single(templatePart.InlineConstraints);
Assert.Single(templatePart.InlineConstraints, c => c.Constraint == @"test(=)");
} }
[Fact] [Fact]
@ -228,8 +223,9 @@ namespace Microsoft.AspNet.Routing.Tests
// Assert // Assert
Assert.Equal("param", templatePart.Name); Assert.Equal("param", templatePart.Name);
Assert.IsType<TestRouteConstraint>(templatePart.InlineConstraint);
Assert.Equal(@"\{", ((TestRouteConstraint)templatePart.InlineConstraint).Pattern); Assert.Single(templatePart.InlineConstraints);
Assert.Single(templatePart.InlineConstraints, c => c.Constraint == @"test(\{)");
} }
[Fact] [Fact]
@ -240,8 +236,9 @@ namespace Microsoft.AspNet.Routing.Tests
// Assert // Assert
Assert.Equal("param", templatePart.Name); Assert.Equal("param", templatePart.Name);
Assert.IsType<TestRouteConstraint>(templatePart.InlineConstraint);
Assert.Equal(@"\(", ((TestRouteConstraint)templatePart.InlineConstraint).Pattern); Assert.Single(templatePart.InlineConstraints);
Assert.Single(templatePart.InlineConstraints, c => c.Constraint == @"test(\()");
} }
[Fact] [Fact]
@ -254,8 +251,9 @@ namespace Microsoft.AspNet.Routing.Tests
Assert.Equal("param", templatePart.Name); Assert.Equal("param", templatePart.Name);
Assert.Null(templatePart.DefaultValue); Assert.Null(templatePart.DefaultValue);
Assert.False(templatePart.IsOptional); Assert.False(templatePart.IsOptional);
Assert.IsType<TestRouteConstraint>(templatePart.InlineConstraint);
Assert.Equal(@"\?", ((TestRouteConstraint)templatePart.InlineConstraint).Pattern); Assert.Single(templatePart.InlineConstraints);
Assert.Single(templatePart.InlineConstraints, c => c.Constraint == @"test(\?)");
} }
[Theory] [Theory]
@ -275,7 +273,7 @@ namespace Microsoft.AspNet.Routing.Tests
// Assert // Assert
Assert.Equal(expectedParameterName, templatePart.Name); Assert.Equal(expectedParameterName, templatePart.Name);
Assert.Null(templatePart.InlineConstraint); Assert.Empty(templatePart.InlineConstraints);
Assert.Null(templatePart.DefaultValue); Assert.Null(templatePart.DefaultValue);
} }
@ -283,14 +281,14 @@ namespace Microsoft.AspNet.Routing.Tests
private TemplatePart ParseParameter(string routeParameter) private TemplatePart ParseParameter(string routeParameter)
{ {
var _constraintResolver = GetConstraintResolver(); var _constraintResolver = GetConstraintResolver();
var templatePart = InlineRouteParameterParser.ParseRouteParameter(routeParameter, _constraintResolver); var templatePart = InlineRouteParameterParser.ParseRouteParameter(routeParameter);
return templatePart; return templatePart;
} }
private static RouteTemplate ParseRouteTemplate(string template) private static RouteTemplate ParseRouteTemplate(string template)
{ {
var _constraintResolver = GetConstraintResolver(); var _constraintResolver = GetConstraintResolver();
return TemplateParser.Parse(template, _constraintResolver); return TemplateParser.Parse(template);
} }
private static IInlineConstraintResolver GetConstraintResolver() private static IInlineConstraintResolver GetConstraintResolver()

View File

@ -0,0 +1,147 @@
// 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.
#if ASPNET50
using System;
using System.Linq;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Routing.Constraints;
using Microsoft.AspNet.Testing;
using Microsoft.Framework.OptionsModel;
using Moq;
using Xunit;
namespace Microsoft.AspNet.Routing
{
public class RouteConstraintBuilderTest
{
[Fact]
public void AddConstraint_String_CreatesARegex()
{
// Arrange
var builder = CreateBuilder("{controller}/{action}");
builder.AddConstraint("controller", "abc");
// Act
var result = builder.Build();
// Assert
Assert.Equal(1, result.Count);
Assert.Equal("controller", result.First().Key);
Assert.IsType<RegexRouteConstraint>(Assert.Single(result).Value);
}
[Fact]
public void AddConstraint_IRouteConstraint()
{
// Arrange
var originalConstraint = Mock.Of<IRouteConstraint>();
var builder = CreateBuilder("{controller}/{action}");
builder.AddConstraint("controller", originalConstraint);
// Act
var result = builder.Build();
// Assert
Assert.Equal(1, result.Count);
var kvp = Assert.Single(result);
Assert.Equal("controller", kvp.Key);
Assert.Same(originalConstraint, kvp.Value);
}
[Fact]
public void AddResolvedConstraint_IRouteConstraint()
{
// Arrange
var builder = CreateBuilder("{controller}/{action}");
builder.AddResolvedConstraint("controller", "int");
// Act
var result = builder.Build();
// Assert
Assert.Equal(1, result.Count);
var kvp = Assert.Single(result);
Assert.Equal("controller", kvp.Key);
Assert.IsType<IntRouteConstraint>(kvp.Value);
}
[Fact]
public void AddConstraint_InvalidType_Throws()
{
// Arrange
var builder = CreateBuilder("{controller}/{action}");
// Act & Assert
ExceptionAssert.Throws<InvalidOperationException>(
() => builder.AddConstraint("controller", 5),
"The constraint entry 'controller' - '5' on the route " +
"'{controller}/{action}' must have a string value or be of a type which implements '" +
typeof(IRouteConstraint) + "'.");
}
[Fact]
public void AddResolvedConstraint_NotFound_Throws()
{
// Arrange
var unresolvedConstraint = @"test";
var builder = CreateBuilder("{controller}/{action}");
// Act & Assert
ExceptionAssert.Throws<InvalidOperationException>(
() => builder.AddResolvedConstraint("controller", unresolvedConstraint),
@"The constraint entry 'controller' - '" + unresolvedConstraint + "' on the route " +
"'{controller}/{action}' could not be resolved by the constraint resolver " +
"of type 'DefaultInlineConstraintResolver'.");
}
[Theory]
[InlineData("abc", "abc", true)] // simple case
[InlineData("abc", "bbb|abc", true)] // Regex or
[InlineData("Abc", "abc", true)] // Case insensitive
[InlineData("Abc ", "abc", false)] // Matches whole (but no trimming)
[InlineData("Abcd", "abc", false)] // Matches whole (additional non whitespace char)
[InlineData("Abc", " abc", false)] // Matches whole (less one char)
public void StringConstraintsMatchingScenarios(string routeValue,
string constraintValue,
bool shouldMatch)
{
// Arrange
var routeValues = new RouteValueDictionary(new { controller = routeValue });
var builder = CreateBuilder("{controller}/{action}");
builder.AddConstraint("controller", constraintValue);
var constraint = Assert.Single(builder.Build()).Value;
Assert.Equal(shouldMatch,
constraint.Match(
httpContext: new Mock<HttpContext>().Object,
route: new Mock<IRouter>().Object,
routeKey: "controller",
values: routeValues,
routeDirection: RouteDirection.IncomingRequest));
}
private static RouteConstraintBuilder CreateBuilder(string template)
{
var services = new Mock<IServiceProvider>(MockBehavior.Strict);
var options = new Mock<IOptions<RouteOptions>>(MockBehavior.Strict);
options
.SetupGet(o => o.Options)
.Returns(new RouteOptions());
var inlineConstraintResolver = new DefaultInlineConstraintResolver(services.Object, options.Object);
return new RouteConstraintBuilder(inlineConstraintResolver, template);
}
}
}
#endif

View File

@ -129,13 +129,14 @@ namespace Microsoft.AspNet.Routing.Template.Tests
[MemberData("EmptyAndNullDefaultValues")] [MemberData("EmptyAndNullDefaultValues")]
public void Binding_WithEmptyAndNull_DefaultValues( public void Binding_WithEmptyAndNull_DefaultValues(
string template, string template,
IDictionary<string, object> defaults, IReadOnlyDictionary<string, object> defaults,
IDictionary<string, object> values, IDictionary<string, object> values,
string expected) string expected)
{ {
// Arrange // Arrange
var binder = new TemplateBinder(TemplateParser.Parse(template, _inlineConstraintResolver), var binder = new TemplateBinder(
defaults); TemplateParser.Parse(template),
defaults);
// Act & Assert // Act & Assert
var result = binder.GetValues(ambientValues: null, values: values); var result = binder.GetValues(ambientValues: null, values: values);
@ -962,13 +963,13 @@ namespace Microsoft.AspNet.Routing.Template.Tests
private static void RunTest( private static void RunTest(
string template, string template,
IDictionary<string, object> defaults, IReadOnlyDictionary<string, object> defaults,
IDictionary<string, object> ambientValues, IDictionary<string, object> ambientValues,
IDictionary<string, object> values, IDictionary<string, object> values,
string expected) string expected)
{ {
// Arrange // Arrange
var binder = new TemplateBinder(TemplateParser.Parse(template, _inlineConstraintResolver), defaults); var binder = new TemplateBinder(TemplateParser.Parse(template), defaults);
// Act & Assert // Act & Assert
var result = binder.GetValues(ambientValues, values); var result = binder.GetValues(ambientValues, values);

View File

@ -23,7 +23,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
var matcher = CreateMatcher("{controller}/{action}/{id}"); var matcher = CreateMatcher("{controller}/{action}/{id}");
// Act // Act
var match = matcher.Match("Bank/DoAction/123", null); var match = matcher.Match("Bank/DoAction/123");
// Assert // Assert
Assert.NotNull(match); Assert.NotNull(match);
@ -39,7 +39,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
var matcher = CreateMatcher("{controller}/{action}/{id}"); var matcher = CreateMatcher("{controller}/{action}/{id}");
// Act // Act
var match = matcher.Match("Bank/DoAction", null); var match = matcher.Match("Bank/DoAction");
// Assert // Assert
Assert.Null(match); Assert.Null(match);
@ -49,10 +49,10 @@ namespace Microsoft.AspNet.Routing.Template.Tests
public void MatchSingleRouteWithDefaults() public void MatchSingleRouteWithDefaults()
{ {
// Arrange // Arrange
var matcher = CreateMatcher("{controller}/{action}/{id}"); var matcher = CreateMatcher("{controller}/{action}/{id}", new { id = "default id" });
// Act // Act
var rd = matcher.Match("Bank/DoAction", new RouteValueDictionary(new { id = "default id" })); var rd = matcher.Match("Bank/DoAction");
// Assert // Assert
Assert.Equal("Bank", rd["controller"]); Assert.Equal("Bank", rd["controller"]);
@ -64,10 +64,10 @@ namespace Microsoft.AspNet.Routing.Template.Tests
public void NoMatchSingleRouteWithDefaults() public void NoMatchSingleRouteWithDefaults()
{ {
// Arrange // Arrange
var matcher = CreateMatcher("{controller}/{action}/{id}"); var matcher = CreateMatcher("{controller}/{action}/{id}", new { id = "default id" });
// Act // Act
var rd = matcher.Match("Bank", new RouteValueDictionary(new { id = "default id" })); var rd = matcher.Match("Bank");
// Assert // Assert
Assert.Null(rd); Assert.Null(rd);
@ -77,10 +77,10 @@ namespace Microsoft.AspNet.Routing.Template.Tests
public void MatchRouteWithLiterals() public void MatchRouteWithLiterals()
{ {
// Arrange // Arrange
var matcher = CreateMatcher("moo/{p1}/bar/{p2}"); var matcher = CreateMatcher("moo/{p1}/bar/{p2}", new { p2 = "default p2" });
// Act // Act
var rd = matcher.Match("moo/111/bar/222", new RouteValueDictionary(new { p2 = "default p2" })); var rd = matcher.Match("moo/111/bar/222");
// Assert // Assert
Assert.Equal("111", rd["p1"]); Assert.Equal("111", rd["p1"]);
@ -91,10 +91,10 @@ namespace Microsoft.AspNet.Routing.Template.Tests
public void MatchRouteWithLiteralsAndDefaults() public void MatchRouteWithLiteralsAndDefaults()
{ {
// Arrange // Arrange
var matcher = CreateMatcher("moo/{p1}/bar/{p2}"); var matcher = CreateMatcher("moo/{p1}/bar/{p2}", new { p2 = "default p2" });
// Act // Act
var rd = matcher.Match("moo/111/bar/", new RouteValueDictionary(new { p2 = "default p2" })); var rd = matcher.Match("moo/111/bar/");
// Assert // Assert
Assert.Equal("111", rd["p1"]); Assert.Equal("111", rd["p1"]);
@ -108,7 +108,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
var matcher = CreateMatcher("moo/bar"); var matcher = CreateMatcher("moo/bar");
// Act // Act
var rd = matcher.Match("moo/bar", null); var rd = matcher.Match("moo/bar");
// Assert // Assert
Assert.NotNull(rd); Assert.NotNull(rd);
@ -122,7 +122,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
var matcher = CreateMatcher("moo/bars"); var matcher = CreateMatcher("moo/bars");
// Act // Act
var rd = matcher.Match("moo/bar", null); var rd = matcher.Match("moo/bar");
// Assert // Assert
Assert.Null(rd); Assert.Null(rd);
@ -135,7 +135,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
var matcher = CreateMatcher("moo/bar"); var matcher = CreateMatcher("moo/bar");
// Act // Act
var rd = matcher.Match("moo/bar/", null); var rd = matcher.Match("moo/bar/");
// Assert // Assert
Assert.NotNull(rd); Assert.NotNull(rd);
@ -149,7 +149,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
var matcher = CreateMatcher("moo/bar/"); var matcher = CreateMatcher("moo/bar/");
// Act // Act
var rd = matcher.Match("moo/bar", null); var rd = matcher.Match("moo/bar");
// Assert // Assert
Assert.NotNull(rd); Assert.NotNull(rd);
@ -163,7 +163,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
var matcher = CreateMatcher("{p1}/{p2}/"); var matcher = CreateMatcher("{p1}/{p2}/");
// Act // Act
var rd = matcher.Match("moo/bar", null); var rd = matcher.Match("moo/bar");
// Assert // Assert
Assert.NotNull(rd); Assert.NotNull(rd);
@ -178,7 +178,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
var matcher = CreateMatcher("{p1}/{p2}/baz"); var matcher = CreateMatcher("{p1}/{p2}/baz");
// Act // Act
var rd = matcher.Match("moo/bar/boo", null); var rd = matcher.Match("moo/bar/boo");
// Assert // Assert
Assert.Null(rd); Assert.Null(rd);
@ -191,7 +191,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
var matcher = CreateMatcher("{p1}"); var matcher = CreateMatcher("{p1}");
// Act // Act
var rd = matcher.Match("moo/bar", null); var rd = matcher.Match("moo/bar");
// Assert // Assert
Assert.Null(rd); Assert.Null(rd);
@ -204,7 +204,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
var matcher = CreateMatcher("DEFAULT.ASPX"); var matcher = CreateMatcher("DEFAULT.ASPX");
// Act // Act
var rd = matcher.Match("default.aspx", null); var rd = matcher.Match("default.aspx");
// Assert // Assert
Assert.NotNull(rd); Assert.NotNull(rd);
@ -224,7 +224,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
var matcher = CreateMatcher(template); var matcher = CreateMatcher(template);
// Act // Act
var rd = matcher.Match(path, null); var rd = matcher.Match(path);
// Assert // Assert
Assert.NotNull(rd); Assert.NotNull(rd);
@ -234,10 +234,10 @@ namespace Microsoft.AspNet.Routing.Template.Tests
public void MatchRouteWithExtraDefaultValues() public void MatchRouteWithExtraDefaultValues()
{ {
// Arrange // Arrange
var matcher = CreateMatcher("{p1}/{p2}"); var matcher = CreateMatcher("{p1}/{p2}", new { p2 = (string)null, foo = "bar" });
// Act // Act
var rd = matcher.Match("v1", new RouteValueDictionary(new { p2 = (string)null, foo = "bar" })); var rd = matcher.Match("v1");
// Assert // Assert
Assert.NotNull(rd); Assert.NotNull(rd);
@ -251,10 +251,12 @@ namespace Microsoft.AspNet.Routing.Template.Tests
public void MatchPrettyRouteWithExtraDefaultValues() public void MatchPrettyRouteWithExtraDefaultValues()
{ {
// Arrange // Arrange
var matcher = CreateMatcher("date/{y}/{m}/{d}"); var matcher = CreateMatcher(
"date/{y}/{m}/{d}",
new { controller = "blog", action = "showpost", m = (string)null, d = (string)null });
// Act // Act
var rd = matcher.Match("date/2007/08", new RouteValueDictionary(new { controller = "blog", action = "showpost", m = (string)null, d = (string)null })); var rd = matcher.Match("date/2007/08");
// Assert // Assert
Assert.NotNull(rd); Assert.NotNull(rd);
@ -540,7 +542,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
var matcher = CreateMatcher("{p1}/{*p2}"); var matcher = CreateMatcher("{p1}/{*p2}");
// Act // Act
var rd = matcher.Match("v1/v2/v3", null); var rd = matcher.Match("v1/v2/v3");
// Assert // Assert
Assert.NotNull(rd); Assert.NotNull(rd);
@ -556,7 +558,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
var matcher = CreateMatcher("{p1}/{*p2}"); var matcher = CreateMatcher("{p1}/{*p2}");
// Act // Act
var rd = matcher.Match("v1/", null); var rd = matcher.Match("v1/");
// Assert // Assert
Assert.NotNull(rd); Assert.NotNull(rd);
@ -572,7 +574,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
var matcher = CreateMatcher("{p1}/{*p2}"); var matcher = CreateMatcher("{p1}/{*p2}");
// Act // Act
var rd = matcher.Match("v1", null); var rd = matcher.Match("v1");
// Assert // Assert
Assert.NotNull(rd); Assert.NotNull(rd);
@ -585,10 +587,10 @@ namespace Microsoft.AspNet.Routing.Template.Tests
public void RouteWithCatchAllClauseUsesDefaultValueForEmptyContent() public void RouteWithCatchAllClauseUsesDefaultValueForEmptyContent()
{ {
// Arrange // Arrange
var matcher = CreateMatcher("{p1}/{*p2}"); var matcher = CreateMatcher("{p1}/{*p2}", new { p2 = "catchall" });
// Act // Act
var rd = matcher.Match("v1", new RouteValueDictionary(new { p2 = "catchall" })); var rd = matcher.Match("v1");
// Assert // Assert
Assert.NotNull(rd); Assert.NotNull(rd);
@ -601,10 +603,10 @@ namespace Microsoft.AspNet.Routing.Template.Tests
public void RouteWithCatchAllClauseIgnoresDefaultValueForNonEmptyContent() public void RouteWithCatchAllClauseIgnoresDefaultValueForNonEmptyContent()
{ {
// Arrange // Arrange
var matcher = CreateMatcher("{p1}/{*p2}"); var matcher = CreateMatcher("{p1}/{*p2}", new { p2 = "catchall" });
// Act // Act
var rd = matcher.Match("v1/hello/whatever", new RouteValueDictionary(new { p2 = "catchall" })); var rd = matcher.Match("v1/hello/whatever");
// Assert // Assert
Assert.NotNull(rd); Assert.NotNull(rd);
@ -715,7 +717,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
var url = "Home/Index"; var url = "Home/Index";
// Act // Act
var match = route.Match(url, null); var match = route.Match(url);
// Assert // Assert
Assert.NotNull(match); Assert.NotNull(match);
@ -732,7 +734,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
var url = "Home"; var url = "Home";
// Act // Act
var match = route.Match(url, null); var match = route.Match(url);
// Assert // Assert
Assert.NotNull(match); Assert.NotNull(match);
@ -749,7 +751,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
var url = ""; var url = "";
// Act // Act
var match = route.Match(url, null); var match = route.Match(url);
// Assert // Assert
Assert.NotNull(match); Assert.NotNull(match);
@ -765,7 +767,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
var url = ""; var url = "";
// Act // Act
var match = route.Match(url, null); var match = route.Match(url);
// Assert // Assert
Assert.NotNull(match); Assert.NotNull(match);
@ -780,7 +782,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
var url = "Home/Index"; var url = "Home/Index";
// Act // Act
var match = route.Match(url, null); var match = route.Match(url);
// Assert // Assert
Assert.NotNull(match); Assert.NotNull(match);
@ -790,18 +792,24 @@ namespace Microsoft.AspNet.Routing.Template.Tests
Assert.False(match.ContainsKey("id")); Assert.False(match.ContainsKey("id"));
} }
private TemplateMatcher CreateMatcher(string template) private TemplateMatcher CreateMatcher(string template, object defaults = null)
{ {
return new TemplateMatcher(TemplateParser.Parse(template, _inlineConstraintResolver)); return new TemplateMatcher(
TemplateParser.Parse(template),
new RouteValueDictionary(defaults));
} }
private static void RunTest(string template, string path, IDictionary<string, object> defaults, IDictionary<string, object> expected) private static void RunTest(
string template,
string path,
IReadOnlyDictionary<string, object> defaults,
IDictionary<string, object> expected)
{ {
// Arrange // Arrange
var matcher = new TemplateMatcher(TemplateParser.Parse(template, _inlineConstraintResolver)); var matcher = new TemplateMatcher(TemplateParser.Parse(template), defaults);
// Act // Act
var match = matcher.Match(path, defaults); var match = matcher.Match(path);
// Assert // Assert
if (expected == null) if (expected == null)

View File

@ -15,8 +15,6 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{ {
public class TemplateRouteParserTests public class TemplateRouteParserTests
{ {
private IInlineConstraintResolver _inlineConstraintResolver = GetInlineConstraintResolver();
[Fact] [Fact]
public void Parse_SingleLiteral() public void Parse_SingleLiteral()
{ {
@ -28,7 +26,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, _inlineConstraintResolver); var actual = TemplateParser.Parse(template);
// Assert // Assert
Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer()); Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer());
@ -42,11 +40,11 @@ namespace Microsoft.AspNet.Routing.Template.Tests
var expected = new RouteTemplate(new List<TemplateSegment>()); var expected = new RouteTemplate(new List<TemplateSegment>());
expected.Segments.Add(new TemplateSegment()); expected.Segments.Add(new TemplateSegment());
expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p", false, false, defaultValue: null, inlineConstraint: null)); expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p", false, false, defaultValue: null, inlineConstraints: null));
expected.Parameters.Add(expected.Segments[0].Parts[0]); expected.Parameters.Add(expected.Segments[0].Parts[0]);
// Act // Act
var actual = TemplateParser.Parse(template, _inlineConstraintResolver); var actual = TemplateParser.Parse(template);
// Assert // Assert
Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer()); Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer());
@ -60,11 +58,11 @@ namespace Microsoft.AspNet.Routing.Template.Tests
var expected = new RouteTemplate(new List<TemplateSegment>()); var expected = new RouteTemplate(new List<TemplateSegment>());
expected.Segments.Add(new TemplateSegment()); expected.Segments.Add(new TemplateSegment());
expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p", false, true, defaultValue: null, inlineConstraint: null)); expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p", false, true, defaultValue: null, inlineConstraints: null));
expected.Parameters.Add(expected.Segments[0].Parts[0]); expected.Parameters.Add(expected.Segments[0].Parts[0]);
// Act // Act
var actual = TemplateParser.Parse(template, _inlineConstraintResolver); var actual = TemplateParser.Parse(template);
// Assert // Assert
Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer()); Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer());
@ -85,7 +83,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, _inlineConstraintResolver); var actual = TemplateParser.Parse(template);
// Assert // Assert
Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer()); Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer());
@ -104,7 +102,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
false, false,
false, false,
defaultValue: null, defaultValue: null,
inlineConstraint: null)); inlineConstraints: 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());
@ -112,7 +110,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
false, false,
false, false,
defaultValue: null, defaultValue: null,
inlineConstraint: null)); inlineConstraints: 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());
@ -120,11 +118,11 @@ namespace Microsoft.AspNet.Routing.Template.Tests
true, true,
false, false,
defaultValue: null, defaultValue: null,
inlineConstraint: null)); inlineConstraints: null));
expected.Parameters.Add(expected.Segments[2].Parts[0]); expected.Parameters.Add(expected.Segments[2].Parts[0]);
// Act // Act
var actual = TemplateParser.Parse(template, _inlineConstraintResolver); var actual = TemplateParser.Parse(template);
// Assert // Assert
Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer()); Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer());
@ -143,11 +141,11 @@ namespace Microsoft.AspNet.Routing.Template.Tests
false, false,
false, false,
defaultValue: null, defaultValue: null,
inlineConstraint: null)); inlineConstraints: null));
expected.Parameters.Add(expected.Segments[0].Parts[1]); expected.Parameters.Add(expected.Segments[0].Parts[1]);
// Act // Act
var actual = TemplateParser.Parse(template, _inlineConstraintResolver); var actual = TemplateParser.Parse(template);
// Assert // Assert
Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer()); Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer());
@ -165,12 +163,12 @@ namespace Microsoft.AspNet.Routing.Template.Tests
false, false,
false, false,
defaultValue: null, defaultValue: null,
inlineConstraint: null)); inlineConstraints: 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, _inlineConstraintResolver); var actual = TemplateParser.Parse(template);
// Assert // Assert
Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer()); Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer());
@ -188,18 +186,18 @@ namespace Microsoft.AspNet.Routing.Template.Tests
false, false,
false, false,
defaultValue: null, defaultValue: null,
inlineConstraint: null)); inlineConstraints: 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", expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p2",
false, false,
false, false,
defaultValue: null, defaultValue: null,
inlineConstraint: null)); inlineConstraints: null));
expected.Parameters.Add(expected.Segments[0].Parts[2]); expected.Parameters.Add(expected.Segments[0].Parts[2]);
// Act // Act
var actual = TemplateParser.Parse(template, _inlineConstraintResolver); var actual = TemplateParser.Parse(template);
// Assert // Assert
Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer()); Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer());
@ -218,12 +216,12 @@ namespace Microsoft.AspNet.Routing.Template.Tests
false, false,
false, false,
defaultValue: null, defaultValue: null,
inlineConstraint: null)); inlineConstraints: 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, _inlineConstraintResolver); var actual = TemplateParser.Parse(template);
// Assert // Assert
Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer()); Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer());
@ -233,7 +231,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}", _inlineConstraintResolver), () => TemplateParser.Parse("{Controller}.mvc/{id}/{controller}"),
"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");
} }
@ -247,7 +245,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, _inlineConstraintResolver), () => TemplateParser.Parse(template),
@"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");
} }
@ -256,7 +254,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}", _inlineConstraintResolver), () => TemplateParser.Parse("123{a}abc{*moo}"),
"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");
} }
@ -265,7 +263,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}", _inlineConstraintResolver), () => TemplateParser.Parse("{*p1}/{*p2}"),
"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");
} }
@ -274,7 +272,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}", _inlineConstraintResolver), () => TemplateParser.Parse("{*p1}abc{*p2}"),
"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");
} }
@ -283,7 +281,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
public void InvalidTemplate_CannotHaveCatchAllWithNoName() public void InvalidTemplate_CannotHaveCatchAllWithNoName()
{ {
ExceptionAssert.Throws<ArgumentException>( ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("foo/{*}", _inlineConstraintResolver), () => TemplateParser.Parse("foo/{*}"),
"The route parameter name '' is invalid. Route parameter names must be non-empty and cannot" + "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," + " contain these characters: '{', '}', '/'. The '?' character marks a parameter as optional," +
" and can occur only at the end of the parameter. The '*' character marks a parameter as catch-all," + " and can occur only at the end of the parameter. The '*' character marks a parameter as catch-all," +
@ -308,7 +306,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
// Act & Assert // Act & Assert
ExceptionAssert.Throws<ArgumentException>( ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse(template, _inlineConstraintResolver), expectedMessage + Environment.NewLine + () => TemplateParser.Parse(template), expectedMessage + Environment.NewLine +
"Parameter name: routeTemplate"); "Parameter name: routeTemplate");
} }
@ -316,7 +314,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}", _inlineConstraintResolver), () => TemplateParser.Parse("foo/{{p1}"),
"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");
} }
@ -325,7 +323,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}}", _inlineConstraintResolver), () => TemplateParser.Parse("foo/{p1}}"),
"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");
} }
@ -334,7 +332,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}", _inlineConstraintResolver), () => TemplateParser.Parse("{aaa}/{AAA}"),
"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");
} }
@ -343,7 +341,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}", _inlineConstraintResolver), () => TemplateParser.Parse("{aaa}/{*AAA}"),
"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");
} }
@ -352,7 +350,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}", _inlineConstraintResolver), () => TemplateParser.Parse("{a}/{aa}a}/{z}"),
"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");
} }
@ -361,7 +359,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}", _inlineConstraintResolver), () => TemplateParser.Parse("{a}/{a{aa}/{z}"),
"The route parameter name 'a{aa' is invalid. Route parameter names must be non-empty and cannot" + "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" + " contain these characters: '{', '}', '/'. The '?' character marks a parameter as optional, and" +
" can occur only at the end of the parameter. The '*' character marks a parameter as catch-all," + " can occur only at the end of the parameter. The '*' character marks a parameter as catch-all," +
@ -373,7 +371,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}", _inlineConstraintResolver), () => TemplateParser.Parse("{a}/{}/{z}"),
"The route parameter name '' is invalid. Route parameter names must be non-empty and cannot" + "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" + " contain these characters: '{', '}', '/'. The '?' character marks a parameter as optional, and" +
" can occur only at the end of the parameter. The '*' character marks a parameter as catch-all," + " can occur only at the end of the parameter. The '*' character marks a parameter as catch-all," +
@ -385,7 +383,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/{?}", _inlineConstraintResolver), () => TemplateParser.Parse("{Controller}.mvc/{?}"),
"The route parameter name '' is invalid. Route parameter names must be non-empty and cannot" + "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" + " contain these characters: '{', '}', '/'. The '?' character marks a parameter as optional, and" +
" can occur only at the end of the parameter. The '*' character marks a parameter as catch-all," + " can occur only at the end of the parameter. The '*' character marks a parameter as catch-all," +
@ -397,7 +395,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}", _inlineConstraintResolver), () => TemplateParser.Parse("{a}//{z}"),
"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");
} }
@ -406,7 +404,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}", _inlineConstraintResolver), () => TemplateParser.Parse("foo/{p1}/{*p2}/{p3}"),
"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");
} }
@ -415,7 +413,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}", _inlineConstraintResolver), () => TemplateParser.Parse("foo/aa{p1}{p2}"),
"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");
} }
@ -424,7 +422,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
public void InvalidTemplate_CannotStartWithSlash() public void InvalidTemplate_CannotStartWithSlash()
{ {
ExceptionAssert.Throws<ArgumentException>( ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("/foo", _inlineConstraintResolver), () => TemplateParser.Parse("/foo"),
"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");
} }
@ -433,7 +431,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
public void InvalidTemplate_CannotStartWithTilde() public void InvalidTemplate_CannotStartWithTilde()
{ {
ExceptionAssert.Throws<ArgumentException>( ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("~foo", _inlineConstraintResolver), () => TemplateParser.Parse("~foo"),
"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");
} }
@ -442,7 +440,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", _inlineConstraintResolver), () => TemplateParser.Parse("foor?bar"),
"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");
} }
@ -451,7 +449,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}", _inlineConstraintResolver), () => TemplateParser.Parse("{foor?b}"),
"The route parameter name 'foor?b' is invalid. Route parameter names must be non-empty and cannot" + "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" + " contain these characters: '{', '}', '/'. The '?' character marks a parameter as optional, and" +
" can occur only at the end of the parameter. The '*' character marks a parameter as catch-all," + " can occur only at the end of the parameter. The '*' character marks a parameter as catch-all," +
@ -463,7 +461,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}", _inlineConstraintResolver), () => TemplateParser.Parse("{foorb?}-bar-{z}"),
"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");
} }
@ -472,19 +470,11 @@ namespace Microsoft.AspNet.Routing.Template.Tests
public void InvalidTemplate_CatchAllMarkedOptional() public void InvalidTemplate_CatchAllMarkedOptional()
{ {
ExceptionAssert.Throws<ArgumentException>( ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("{a}/{*b?}", _inlineConstraintResolver), () => TemplateParser.Parse("{a}/{*b?}"),
"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");
} }
private static IInlineConstraintResolver GetInlineConstraintResolver()
{
var services = new ServiceCollection { OptionsServices.GetDefaultServices() };
var serviceProvider = services.BuildServiceProvider();
var accessor = serviceProvider.GetRequiredService<IOptions<RouteOptions>>();
return new DefaultInlineConstraintResolver(serviceProvider, accessor);
}
private class TemplateEqualityComparer : IEqualityComparer<RouteTemplate> private class TemplateEqualityComparer : IEqualityComparer<RouteTemplate>
{ {
public bool Equals(RouteTemplate x, RouteTemplate y) public bool Equals(RouteTemplate x, RouteTemplate y)

View File

@ -515,6 +515,29 @@ namespace Microsoft.AspNet.Routing.Template
Assert.Equal("Index", context.RouteData.Values["action"]); Assert.Equal("Index", context.RouteData.Values["action"]);
} }
[Fact]
public async Task Match_Success_CopiesDataTokens()
{
// Arrange
var route = CreateRoute(
"{controller}/{action}",
defaults: new { action = "Index" },
dataTokens: new { culture = "en-CA" });
var context = CreateRouteContext("/Home");
// Act
await route.RouteAsync(context);
Assert.True(context.IsHandled);
// This should not affect the route - RouteData.DataTokens is a copy
context.RouteData.DataTokens.Add("company", "contoso");
// Assert
Assert.Single(route.DataTokens);
Assert.Single(route.DataTokens, kvp => kvp.Key == "culture" && ((string)kvp.Value) == "en-CA");
}
[Fact] [Fact]
public async Task Match_Fails() public async Task Match_Fails()
{ {
@ -1060,11 +1083,12 @@ namespace Microsoft.AspNet.Routing.Template
var routeBuilder = CreateRouteBuilder(); var routeBuilder = CreateRouteBuilder();
// Assert // Assert
ExceptionAssert.Throws<InvalidOperationException>(() => routeBuilder.MapRoute("mockName", ExceptionAssert.Throws<InvalidOperationException>(
"{controller}/{action}", () => routeBuilder.MapRoute("mockName",
defaults: null, "{controller}/{action}",
constraints: new { controller = "a.*", action = new Object() }), defaults: null,
"The constraint entry 'action' on the route with route template '{controller}/{action}' " + constraints: new { controller = "a.*", action = 17 }),
"The constraint entry 'action' - '17' on the route '{controller}/{action}' " +
"must have a string value or be of a type which implements '" + "must have a string value or be of a type which implements '" +
typeof(IRouteConstraint) + "'."); typeof(IRouteConstraint) + "'.");
} }
@ -1195,7 +1219,7 @@ namespace Microsoft.AspNet.Routing.Template
(constraints as IDictionary<string, object>) ?? (constraints as IDictionary<string, object>) ??
new RouteValueDictionary(constraints), new RouteValueDictionary(constraints),
(dataTokens as IDictionary<string, object>) ?? (dataTokens as IDictionary<string, object>) ??
new RouteValueDictionary(constraints), new RouteValueDictionary(dataTokens),
_inlineConstraintResolver); _inlineConstraintResolver);
} }