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

View File

@ -41,7 +41,7 @@ namespace Microsoft.AspNet.Routing.Logging
/// <summary>
/// The values produced by default.
/// </summary>
public IDictionary<string, object> DefaultValues { get; set; }
public IReadOnlyDictionary<string, object> DefaultValues { get; set; }
/// <summary>
/// The values produced from the request.
@ -51,7 +51,7 @@ namespace Microsoft.AspNet.Routing.Logging
/// <summary>
/// The constraints matched on the produced values.
/// </summary>
public IDictionary<string, IRouteConstraint> Constraints { get; set; }
public IReadOnlyDictionary<string, IRouteConstraint> Constraints { get; set; }
/// <summary>
/// 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);
}
/// <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>
/// A path segment that contains more than one section, such as a literal section or a parameter, cannot contain a catch-all parameter.
/// </summary>
@ -363,35 +347,35 @@ namespace Microsoft.AspNet.Routing
}
/// <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>
internal static string TemplateRoute_ValidationMustBeStringOrCustomConstraint
internal static string RouteConstraintBuilder_ValidationMustBeStringOrCustomConstraint
{
get { return GetString("TemplateRoute_ValidationMustBeStringOrCustomConstraint"); }
get { return GetString("RouteConstraintBuilder_ValidationMustBeStringOrCustomConstraint"); }
}
/// <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>
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>
/// 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>
internal static string InlineRouteParser_CouldNotResolveConstraint
internal static string RouteConstraintBuilder_CouldNotResolveConstraint
{
get { return GetString("InlineRouteParser_CouldNotResolveConstraint"); }
get { return GetString("RouteConstraintBuilder_CouldNotResolveConstraint"); }
}
/// <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>
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)

View File

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

View File

@ -7,58 +7,52 @@ using Microsoft.AspNet.Routing.Constraints;
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 static IDictionary<string, IRouteConstraint>
BuildConstraints(IDictionary<string, object> inputConstraints)
private readonly IInlineConstraintResolver _inlineConstraintResolver;
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>
BuildConstraints(IDictionary<string, object> inputConstraints, [NotNull] string routeTemplate)
/// <summary>
/// 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);
}
private static IDictionary<string, IRouteConstraint>
BuildConstraintsCore(IDictionary<string, object> inputConstraints, string routeTemplate)
{
if (inputConstraints == null || inputConstraints.Count == 0)
var constraints = new Dictionary<string, IRouteConstraint>(StringComparer.OrdinalIgnoreCase);
foreach (var kvp in _constraints)
{
return null;
}
var constraints = new Dictionary<string, IRouteConstraint>(inputConstraints.Count,
StringComparer.OrdinalIgnoreCase);
foreach (var kvp in inputConstraints)
{
var constraint = kvp.Value as IRouteConstraint;
if (constraint == null)
IRouteConstraint constraint;
if (kvp.Value.Count == 1)
{
var regexPattern = kvp.Value as string;
if (regexPattern == null)
{
if (routeTemplate != null)
{
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);
constraint = kvp.Value[0];
}
else
{
constraint = new CompositeRouteConstraint(kvp.Value.ToArray());
}
constraints.Add(kvp.Key, constraint);
@ -66,5 +60,79 @@ namespace Microsoft.AspNet.Routing
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 bool Match(IDictionary<string, IRouteConstraint> constraints,
public static bool Match(IReadOnlyDictionary<string, IRouteConstraint> constraints,
[NotNull] IDictionary<string, object> routeValues,
[NotNull] HttpContext httpContext,
[NotNull] IRouter route,

View File

@ -12,8 +12,13 @@ namespace Microsoft.AspNet.Routing
/// <summary>
/// An <see cref="IDictionary{string, object}"/> type for route values.
/// </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;
/// <summary>
@ -139,6 +144,14 @@ namespace Microsoft.AspNet.Routing
}
}
IEnumerable<string> IReadOnlyDictionary<string, object>.Keys
{
get
{
return _dictionary.Keys;
}
}
/// <inheritdoc />
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 />
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
{
private readonly IDictionary<string, object> _defaults;
private readonly IReadOnlyDictionary<string, object> _defaults;
private readonly RouteTemplate _template;
public TemplateBinder(RouteTemplate template, IDictionary<string, object> defaults)
public TemplateBinder(RouteTemplate template, IReadOnlyDictionary<string, object> defaults)
{
if (template == null)
{
@ -336,12 +336,14 @@ namespace Microsoft.AspNet.Routing.Template
[DebuggerDisplay("{DebuggerToString(),nq}")]
private class TemplateBindingContext
{
private readonly IDictionary<string, object> _defaults;
private readonly IReadOnlyDictionary<string, object> _defaults;
private readonly RouteValueDictionary _acceptedValues;
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)
{
@ -355,6 +357,10 @@ namespace Microsoft.AspNet.Routing.Template
if (_defaults != null)
{
_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 };
public TemplateMatcher(RouteTemplate template)
public TemplateMatcher(
[NotNull] RouteTemplate template,
[NotNull] IReadOnlyDictionary<string, object> defaults)
{
if (template == null)
{
throw new ArgumentNullException("template");
}
Template = template;
Defaults = defaults ?? RouteValueDictionary.Empty;
}
public IReadOnlyDictionary<string, object> Defaults { 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);
if (defaults == null)
{
defaults = new RouteValueDictionary();
}
var values = new RouteValueDictionary();
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
object defaultValue;
defaults.TryGetValue(part.Name, out defaultValue);
Defaults.TryGetValue(part.Name, out defaultValue);
values.Add(part.Name, defaultValue);
}
@ -94,7 +89,7 @@ namespace Microsoft.AspNet.Routing.Template
else
{
object defaultValue;
if (defaults.TryGetValue(part.Name, out defaultValue))
if (Defaults.TryGetValue(part.Name, out defaultValue))
{
values.Add(part.Name, defaultValue);
}
@ -114,7 +109,7 @@ namespace Microsoft.AspNet.Routing.Template
}
else
{
if (!MatchComplexSegment(routeSegment, requestSegment, defaults, values))
if (!MatchComplexSegment(routeSegment, requestSegment, Defaults, values))
{
return null;
}
@ -142,7 +137,7 @@ namespace Microsoft.AspNet.Routing.Template
// It's ok for a catch-all to produce a null value
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);
}
@ -158,14 +153,11 @@ namespace Microsoft.AspNet.Routing.Template
}
// 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,
string requestSegment,
IDictionary<string, object> defaults,
IReadOnlyDictionary<string, object> defaults,
RouteValueDictionary values)
{
Contract.Assert(routeSegment != null);
@ -214,7 +206,7 @@ namespace Microsoft.AspNet.Routing.Template
return false;
}
var indexOfLiteral = requestSegment.LastIndexOf(part.Text,
var indexOfLiteral = requestSegment.LastIndexOf(part.Text,
startIndex,
StringComparison.OrdinalIgnoreCase);
if (indexOfLiteral == -1)
@ -237,7 +229,7 @@ namespace Microsoft.AspNet.Routing.Template
newLastIndex = indexOfLiteral;
}
if ((parameterNeedsValue != null) &&
if ((parameterNeedsValue != null) &&
(((lastLiteral != null) && (part.IsLiteral)) || (indexOfLastSegmentUsed == 0)))
{
// 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 Asterisk = '*';
public static RouteTemplate Parse(string routeTemplate, IInlineConstraintResolver constraintResolver)
public static RouteTemplate Parse(string routeTemplate)
{
if (routeTemplate == null)
{
@ -29,7 +29,7 @@ namespace Microsoft.AspNet.Routing.Template
throw new ArgumentException(Resources.TemplateRoute_InvalidRouteTemplate, "routeTemplate");
}
var context = new TemplateParserContext(routeTemplate, constraintResolver);
var context = new TemplateParserContext(routeTemplate);
var segments = new List<TemplateSegment>();
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,
// default values and optional parameters.
var templatePart = InlineRouteParameterParser.ParseRouteParameter(rawParameter,
context.ConstraintResolver);
var templatePart = InlineRouteParameterParser.ParseRouteParameter(rawParameter);
if (templatePart.IsCatchAll && templatePart.IsOptional)
{
@ -406,13 +405,12 @@ namespace Microsoft.AspNet.Routing.Template
private HashSet<string> _parameterNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
public TemplateParserContext(string template, IInlineConstraintResolver constraintResolver)
public TemplateParserContext(string template)
{
Contract.Assert(template != null);
_template = template;
_index = -1;
ConstraintResolver = constraintResolver;
}
public char Current
@ -431,12 +429,6 @@ namespace Microsoft.AspNet.Routing.Template
get { return _parameterNames; }
}
public IInlineConstraintResolver ConstraintResolver
{
get;
private set;
}
public bool Back()
{
return --_index >= 0;

View File

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

View File

@ -14,9 +14,9 @@ namespace Microsoft.AspNet.Routing.Template
{
public class TemplateRoute : INamedRouter
{
private readonly IDictionary<string, object> _defaults;
private readonly IDictionary<string, IRouteConstraint> _constraints;
private readonly IDictionary<string, object> _dataTokens;
private readonly IReadOnlyDictionary<string, IRouteConstraint> _constraints;
private readonly IReadOnlyDictionary<string, object> _dataTokens;
private readonly IReadOnlyDictionary<string, object> _defaults;
private readonly IRouter _target;
private readonly RouteTemplate _parsedTemplate;
private readonly string _routeTemplate;
@ -25,7 +25,10 @@ namespace Microsoft.AspNet.Routing.Template
private ILogger _logger;
private ILogger _constraintLogger;
public TemplateRoute(IRouter target, string routeTemplate, IInlineConstraintResolver inlineConstraintResolver)
public TemplateRoute(
[NotNull] IRouter target,
string routeTemplate,
IInlineConstraintResolver inlineConstraintResolver)
: this(target,
routeTemplate,
defaults: null,
@ -56,27 +59,28 @@ namespace Microsoft.AspNet.Routing.Template
_target = target;
_routeTemplate = routeTemplate ?? string.Empty;
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.
_parsedTemplate = TemplateParser.Parse(RouteTemplate, inlineConstraintResolver);
UpdateInlineDefaultValuesAndConstraints();
_dataTokens = dataTokens == null ? RouteValueDictionary.Empty : new RouteValueDictionary(dataTokens);
_matcher = new TemplateMatcher(_parsedTemplate);
_binder = new TemplateBinder(_parsedTemplate, _defaults);
// Data we parse from the template will be used to fill in the rest of the constraints or
// 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 IDictionary<string, object> Defaults
public IReadOnlyDictionary<string, object> Defaults
{
get { return _defaults; }
}
public IDictionary<string, object> DataTokens
public IReadOnlyDictionary<string, object> DataTokens
{
get { return _dataTokens; }
}
@ -86,7 +90,7 @@ namespace Microsoft.AspNet.Routing.Template
get { return _routeTemplate; }
}
public IDictionary<string, IRouteConstraint> Constraints
public IReadOnlyDictionary<string, IRouteConstraint> Constraints
{
get { return _constraints; }
}
@ -103,7 +107,7 @@ namespace Microsoft.AspNet.Routing.Template
requestPath = requestPath.Substring(1);
}
var values = _matcher.Match(requestPath, Defaults);
var values = _matcher.Match(requestPath);
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)
{
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;
}
}
var constraintBuilder = new RouteConstraintBuilder(inlineConstraintResolver, template);
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 (_defaults.ContainsKey(parameter.Name))
if (result.ContainsKey(parameter.Name))
{
throw new InvalidOperationException(
Resources
.FormatTemplateRoute_CannotHaveDefaultValueSpecifiedInlineAndExplicitly(parameter.Name));
Resources.FormatTemplateRoute_CannotHaveDefaultValueSpecifiedInlineAndExplicitly(
parameter.Name));
}
else
{
_defaults[parameter.Name] = parameter.DefaultValue;
result.Add(parameter.Name, parameter.DefaultValue);
}
}
}
return result;
}
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)
{
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.Linq;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Routing.Constraints;
using Microsoft.AspNet.Routing.Template;
using Microsoft.Framework.DependencyInjection;
using Microsoft.Framework.DependencyInjection.Fallback;
@ -25,7 +24,9 @@ namespace Microsoft.AspNet.Routing.Tests
// Assert
Assert.Equal("param", templatePart.Name);
Assert.Equal("111111", templatePart.DefaultValue);
Assert.IsType<IntRouteConstraint>(templatePart.InlineConstraint);
Assert.Single(templatePart.InlineConstraints);
Assert.Single(templatePart.InlineConstraints, c => c.Constraint == "int");
}
[Fact]
@ -37,8 +38,9 @@ namespace Microsoft.AspNet.Routing.Tests
// Assert
Assert.Equal("param", templatePart.Name);
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]
@ -50,7 +52,9 @@ namespace Microsoft.AspNet.Routing.Tests
// Assert
Assert.Equal("param", templatePart.Name);
Assert.True(templatePart.IsOptional);
Assert.IsType<IntRouteConstraint>(templatePart.InlineConstraint);
Assert.Single(templatePart.InlineConstraints);
Assert.Single(templatePart.InlineConstraints, c => c.Constraint == @"int");
}
[Fact]
@ -62,7 +66,9 @@ namespace Microsoft.AspNet.Routing.Tests
// Assert
Assert.Equal("param", templatePart.Name);
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]
@ -73,10 +79,11 @@ namespace Microsoft.AspNet.Routing.Tests
// Assert
Assert.Equal("param", templatePart.Name);
Assert.IsType<CompositeRouteConstraint>(templatePart.InlineConstraint);
var constraint = (CompositeRouteConstraint)templatePart.InlineConstraint;
Assert.Equal(@"\d+", ((TestRouteConstraint)constraint.Constraints.ElementAt(0)).Pattern);
Assert.Equal(@"\w+", ((TestRouteConstraint)constraint.Constraints.ElementAt(1)).Pattern);
Assert.Equal(2, templatePart.InlineConstraints.Count());
Assert.Single(templatePart.InlineConstraints, c => c.Constraint == @"test(\d+)");
Assert.Single(templatePart.InlineConstraints, c => c.Constraint == @"test(\w+)");
}
[Fact]
@ -92,10 +99,10 @@ namespace Microsoft.AspNet.Routing.Tests
Assert.Equal("p1", param1.Name);
Assert.Equal("hello", param1.DefaultValue);
Assert.False(param1.IsOptional);
Assert.IsType<CompositeRouteConstraint>(param1.InlineConstraint);
var constraint = (CompositeRouteConstraint)param1.InlineConstraint;
Assert.IsType<IntRouteConstraint>(constraint.Constraints.ElementAt(0));
Assert.IsType<TestRouteConstraint>(constraint.Constraints.ElementAt(1));
Assert.Equal(2, param1.InlineConstraints.Count());
Assert.Single(param1.InlineConstraints, c => c.Constraint == "int");
Assert.Single(param1.InlineConstraints, c => c.Constraint == "test(3)");
var param2 = parameters[1];
Assert.Equal("p2", param2.Name);
@ -136,8 +143,9 @@ namespace Microsoft.AspNet.Routing.Tests
// Assert
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]
@ -148,8 +156,9 @@ namespace Microsoft.AspNet.Routing.Tests
// Assert
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]
@ -160,8 +169,9 @@ namespace Microsoft.AspNet.Routing.Tests
// Assert
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]
@ -172,8 +182,9 @@ namespace Microsoft.AspNet.Routing.Tests
// Assert
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]
@ -184,27 +195,10 @@ namespace Microsoft.AspNet.Routing.Tests
// Assert
Assert.Equal("param", templatePart.Name);
var constraint = Assert.IsType<IntRouteConstraint>(templatePart.InlineConstraint);
Assert.Equal("", templatePart.DefaultValue);
}
[Theory]
[InlineData(",")]
[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);
Assert.Single(templatePart.InlineConstraints);
Assert.Single(templatePart.InlineConstraints, c => c.Constraint == @"int");
}
[Fact]
@ -216,8 +210,9 @@ namespace Microsoft.AspNet.Routing.Tests
// Assert
Assert.Equal("param", templatePart.Name);
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]
@ -228,8 +223,9 @@ namespace Microsoft.AspNet.Routing.Tests
// Assert
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]
@ -240,8 +236,9 @@ namespace Microsoft.AspNet.Routing.Tests
// Assert
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]
@ -254,8 +251,9 @@ namespace Microsoft.AspNet.Routing.Tests
Assert.Equal("param", templatePart.Name);
Assert.Null(templatePart.DefaultValue);
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]
@ -275,7 +273,7 @@ namespace Microsoft.AspNet.Routing.Tests
// Assert
Assert.Equal(expectedParameterName, templatePart.Name);
Assert.Null(templatePart.InlineConstraint);
Assert.Empty(templatePart.InlineConstraints);
Assert.Null(templatePart.DefaultValue);
}
@ -283,14 +281,14 @@ namespace Microsoft.AspNet.Routing.Tests
private TemplatePart ParseParameter(string routeParameter)
{
var _constraintResolver = GetConstraintResolver();
var templatePart = InlineRouteParameterParser.ParseRouteParameter(routeParameter, _constraintResolver);
var templatePart = InlineRouteParameterParser.ParseRouteParameter(routeParameter);
return templatePart;
}
private static RouteTemplate ParseRouteTemplate(string template)
{
var _constraintResolver = GetConstraintResolver();
return TemplateParser.Parse(template, _constraintResolver);
return TemplateParser.Parse(template);
}
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")]
public void Binding_WithEmptyAndNull_DefaultValues(
string template,
IDictionary<string, object> defaults,
IReadOnlyDictionary<string, object> defaults,
IDictionary<string, object> values,
string expected)
{
// Arrange
var binder = new TemplateBinder(TemplateParser.Parse(template, _inlineConstraintResolver),
defaults);
var binder = new TemplateBinder(
TemplateParser.Parse(template),
defaults);
// Act & Assert
var result = binder.GetValues(ambientValues: null, values: values);
@ -962,13 +963,13 @@ namespace Microsoft.AspNet.Routing.Template.Tests
private static void RunTest(
string template,
IDictionary<string, object> defaults,
IReadOnlyDictionary<string, object> defaults,
IDictionary<string, object> ambientValues,
IDictionary<string, object> values,
string expected)
{
// Arrange
var binder = new TemplateBinder(TemplateParser.Parse(template, _inlineConstraintResolver), defaults);
var binder = new TemplateBinder(TemplateParser.Parse(template), defaults);
// Act & Assert
var result = binder.GetValues(ambientValues, values);

View File

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

View File

@ -15,8 +15,6 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{
public class TemplateRouteParserTests
{
private IInlineConstraintResolver _inlineConstraintResolver = GetInlineConstraintResolver();
[Fact]
public void Parse_SingleLiteral()
{
@ -28,7 +26,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool"));
// Act
var actual = TemplateParser.Parse(template, _inlineConstraintResolver);
var actual = TemplateParser.Parse(template);
// Assert
Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer());
@ -42,11 +40,11 @@ namespace Microsoft.AspNet.Routing.Template.Tests
var expected = new RouteTemplate(new List<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]);
// Act
var actual = TemplateParser.Parse(template, _inlineConstraintResolver);
var actual = TemplateParser.Parse(template);
// Assert
Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer());
@ -60,11 +58,11 @@ namespace Microsoft.AspNet.Routing.Template.Tests
var expected = new RouteTemplate(new List<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]);
// Act
var actual = TemplateParser.Parse(template, _inlineConstraintResolver);
var actual = TemplateParser.Parse(template);
// Assert
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"));
// Act
var actual = TemplateParser.Parse(template, _inlineConstraintResolver);
var actual = TemplateParser.Parse(template);
// Assert
Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer());
@ -104,7 +102,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
false,
false,
defaultValue: null,
inlineConstraint: null));
inlineConstraints: null));
expected.Parameters.Add(expected.Segments[0].Parts[0]);
expected.Segments.Add(new TemplateSegment());
@ -112,7 +110,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
false,
false,
defaultValue: null,
inlineConstraint: null));
inlineConstraints: null));
expected.Parameters.Add(expected.Segments[1].Parts[0]);
expected.Segments.Add(new TemplateSegment());
@ -120,11 +118,11 @@ namespace Microsoft.AspNet.Routing.Template.Tests
true,
false,
defaultValue: null,
inlineConstraint: null));
inlineConstraints: null));
expected.Parameters.Add(expected.Segments[2].Parts[0]);
// Act
var actual = TemplateParser.Parse(template, _inlineConstraintResolver);
var actual = TemplateParser.Parse(template);
// Assert
Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer());
@ -143,11 +141,11 @@ namespace Microsoft.AspNet.Routing.Template.Tests
false,
false,
defaultValue: null,
inlineConstraint: null));
inlineConstraints: null));
expected.Parameters.Add(expected.Segments[0].Parts[1]);
// Act
var actual = TemplateParser.Parse(template, _inlineConstraintResolver);
var actual = TemplateParser.Parse(template);
// Assert
Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer());
@ -165,12 +163,12 @@ namespace Microsoft.AspNet.Routing.Template.Tests
false,
false,
defaultValue: null,
inlineConstraint: null));
inlineConstraints: null));
expected.Parameters.Add(expected.Segments[0].Parts[0]);
expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool-"));
// Act
var actual = TemplateParser.Parse(template, _inlineConstraintResolver);
var actual = TemplateParser.Parse(template);
// Assert
Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer());
@ -188,18 +186,18 @@ namespace Microsoft.AspNet.Routing.Template.Tests
false,
false,
defaultValue: null,
inlineConstraint: null));
inlineConstraints: null));
expected.Parameters.Add(expected.Segments[0].Parts[0]);
expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool-"));
expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p2",
false,
false,
defaultValue: null,
inlineConstraint: null));
inlineConstraints: null));
expected.Parameters.Add(expected.Segments[0].Parts[2]);
// Act
var actual = TemplateParser.Parse(template, _inlineConstraintResolver);
var actual = TemplateParser.Parse(template);
// Assert
Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer());
@ -218,12 +216,12 @@ namespace Microsoft.AspNet.Routing.Template.Tests
false,
false,
defaultValue: null,
inlineConstraint: null));
inlineConstraints: null));
expected.Parameters.Add(expected.Segments[0].Parts[1]);
expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("-awesome"));
// Act
var actual = TemplateParser.Parse(template, _inlineConstraintResolver);
var actual = TemplateParser.Parse(template);
// Assert
Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer());
@ -233,7 +231,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
public void InvalidTemplate_WithRepeatedParameter()
{
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");
}
@ -247,7 +245,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
public void InvalidTemplate_WithMismatchedBraces(string template)
{
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 +
"Parameter name: routeTemplate");
}
@ -256,7 +254,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
public void InvalidTemplate_CannotHaveCatchAllInMultiSegment()
{
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 +
"Parameter name: routeTemplate");
}
@ -265,7 +263,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
public void InvalidTemplate_CannotHaveMoreThanOneCatchAll()
{
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 +
"Parameter name: routeTemplate");
}
@ -274,7 +272,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
public void InvalidTemplate_CannotHaveMoreThanOneCatchAllInMultiSegment()
{
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 +
"Parameter name: routeTemplate");
}
@ -283,7 +281,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
public void InvalidTemplate_CannotHaveCatchAllWithNoName()
{
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" +
" 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," +
@ -308,7 +306,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
// Act & Assert
ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse(template, _inlineConstraintResolver), expectedMessage + Environment.NewLine +
() => TemplateParser.Parse(template), expectedMessage + Environment.NewLine +
"Parameter name: routeTemplate");
}
@ -316,7 +314,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
public void InvalidTemplate_CannotHaveConsecutiveOpenBrace()
{
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 +
"Parameter name: routeTemplate");
}
@ -325,7 +323,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
public void InvalidTemplate_CannotHaveConsecutiveCloseBrace()
{
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 +
"Parameter name: routeTemplate");
}
@ -334,7 +332,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
public void InvalidTemplate_SameParameterTwiceThrows()
{
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 +
"Parameter name: routeTemplate");
}
@ -343,7 +341,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
public void InvalidTemplate_SameParameterTwiceAndOneCatchAllThrows()
{
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 +
"Parameter name: routeTemplate");
}
@ -352,7 +350,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
public void InvalidTemplate_InvalidParameterNameWithCloseBracketThrows()
{
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 +
"Parameter name: routeTemplate");
}
@ -361,7 +359,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
public void InvalidTemplate_InvalidParameterNameWithOpenBracketThrows()
{
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" +
" 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," +
@ -373,7 +371,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
public void InvalidTemplate_InvalidParameterNameWithEmptyNameThrows()
{
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" +
" 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," +
@ -385,7 +383,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
public void InvalidTemplate_InvalidParameterNameWithQuestionThrows()
{
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" +
" 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," +
@ -397,7 +395,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
public void InvalidTemplate_ConsecutiveSeparatorsSlashSlashThrows()
{
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 +
"Parameter name: routeTemplate");
}
@ -406,7 +404,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
public void InvalidTemplate_WithCatchAllNotAtTheEndThrows()
{
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 +
"Parameter name: routeTemplate");
}
@ -415,7 +413,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
public void InvalidTemplate_RepeatedParametersThrows()
{
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 +
"Parameter name: routeTemplate");
}
@ -424,7 +422,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
public void InvalidTemplate_CannotStartWithSlash()
{
ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("/foo", _inlineConstraintResolver),
() => TemplateParser.Parse("/foo"),
"The route template cannot start with a '/' or '~' character." + Environment.NewLine +
"Parameter name: routeTemplate");
}
@ -433,7 +431,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
public void InvalidTemplate_CannotStartWithTilde()
{
ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("~foo", _inlineConstraintResolver),
() => TemplateParser.Parse("~foo"),
"The route template cannot start with a '/' or '~' character." + Environment.NewLine +
"Parameter name: routeTemplate");
}
@ -442,7 +440,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
public void InvalidTemplate_CannotContainQuestionMark()
{
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 +
"Parameter name: routeTemplate");
}
@ -451,7 +449,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
public void InvalidTemplate_ParameterCannotContainQuestionMark_UnlessAtEnd()
{
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" +
" 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," +
@ -463,7 +461,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
public void InvalidTemplate_MultiSegmentParameterCannotContainOptionalParameter()
{
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 +
"Parameter name: routeTemplate");
}
@ -472,19 +470,11 @@ namespace Microsoft.AspNet.Routing.Template.Tests
public void InvalidTemplate_CatchAllMarkedOptional()
{
ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("{a}/{*b?}", _inlineConstraintResolver),
() => TemplateParser.Parse("{a}/{*b?}"),
"A catch-all parameter cannot be marked optional." + Environment.NewLine +
"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>
{
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"]);
}
[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]
public async Task Match_Fails()
{
@ -1060,11 +1083,12 @@ namespace Microsoft.AspNet.Routing.Template
var routeBuilder = CreateRouteBuilder();
// Assert
ExceptionAssert.Throws<InvalidOperationException>(() => routeBuilder.MapRoute("mockName",
"{controller}/{action}",
defaults: null,
constraints: new { controller = "a.*", action = new Object() }),
"The constraint entry 'action' on the route with route template '{controller}/{action}' " +
ExceptionAssert.Throws<InvalidOperationException>(
() => routeBuilder.MapRoute("mockName",
"{controller}/{action}",
defaults: null,
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 '" +
typeof(IRouteConstraint) + "'.");
}
@ -1195,7 +1219,7 @@ namespace Microsoft.AspNet.Routing.Template
(constraints as IDictionary<string, object>) ??
new RouteValueDictionary(constraints),
(dataTokens as IDictionary<string, object>) ??
new RouteValueDictionary(constraints),
new RouteValueDictionary(dataTokens),
_inlineConstraintResolver);
}