diff --git a/src/Microsoft.AspNet.Routing/InlineRouteParameterParser.cs b/src/Microsoft.AspNet.Routing/InlineRouteParameterParser.cs index a76c808a88..57625f7049 100644 --- a/src/Microsoft.AspNet.Routing/InlineRouteParameterParser.cs +++ b/src/Microsoft.AspNet.Routing/InlineRouteParameterParser.cs @@ -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 GetInlineConstraints(Group constraintGroup) { - var parameterConstraints = new List(); - 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(); - 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; } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Routing/Logging/TemplateRouteRouteAsyncValues.cs b/src/Microsoft.AspNet.Routing/Logging/TemplateRouteRouteAsyncValues.cs index 4163f88391..98a21fd16d 100644 --- a/src/Microsoft.AspNet.Routing/Logging/TemplateRouteRouteAsyncValues.cs +++ b/src/Microsoft.AspNet.Routing/Logging/TemplateRouteRouteAsyncValues.cs @@ -41,7 +41,7 @@ namespace Microsoft.AspNet.Routing.Logging /// /// The values produced by default. /// - public IDictionary DefaultValues { get; set; } + public IReadOnlyDictionary DefaultValues { get; set; } /// /// The values produced from the request. @@ -51,7 +51,7 @@ namespace Microsoft.AspNet.Routing.Logging /// /// The constraints matched on the produced values. /// - public IDictionary Constraints { get; set; } + public IReadOnlyDictionary Constraints { get; set; } /// /// True if the matched. diff --git a/src/Microsoft.AspNet.Routing/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Routing/Properties/Resources.Designer.cs index d5bad451a4..c6d51b59e9 100644 --- a/src/Microsoft.AspNet.Routing/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Routing/Properties/Resources.Designer.cs @@ -138,22 +138,6 @@ namespace Microsoft.AspNet.Routing return string.Format(CultureInfo.CurrentCulture, GetString("DefaultInlineConstraintResolver_TypeNotConstraint"), p0, p1, p2); } - /// - /// The constraint entry '{0}' must have a string value or be of a type which implements '{1}'. - /// - internal static string GeneralConstraints_ValidationMustBeStringOrCustomConstraint - { - get { return GetString("GeneralConstraints_ValidationMustBeStringOrCustomConstraint"); } - } - - /// - /// The constraint entry '{0}' must have a string value or be of a type which implements '{1}'. - /// - internal static string FormatGeneralConstraints_ValidationMustBeStringOrCustomConstraint(object p0, object p1) - { - return string.Format(CultureInfo.CurrentCulture, GetString("GeneralConstraints_ValidationMustBeStringOrCustomConstraint"), p0, p1); - } - /// /// A path segment that contains more than one section, such as a literal section or a parameter, cannot contain a catch-all parameter. /// @@ -363,35 +347,35 @@ namespace Microsoft.AspNet.Routing } /// - /// 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}'. /// - internal static string TemplateRoute_ValidationMustBeStringOrCustomConstraint + internal static string RouteConstraintBuilder_ValidationMustBeStringOrCustomConstraint { - get { return GetString("TemplateRoute_ValidationMustBeStringOrCustomConstraint"); } + get { return GetString("RouteConstraintBuilder_ValidationMustBeStringOrCustomConstraint"); } } /// - /// 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}'. /// - 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); } /// - /// 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}'. /// - internal static string InlineRouteParser_CouldNotResolveConstraint + internal static string RouteConstraintBuilder_CouldNotResolveConstraint { - get { return GetString("InlineRouteParser_CouldNotResolveConstraint"); } + get { return GetString("RouteConstraintBuilder_CouldNotResolveConstraint"); } } /// - /// 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}'. /// - 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) diff --git a/src/Microsoft.AspNet.Routing/Resources.resx b/src/Microsoft.AspNet.Routing/Resources.resx index 7c5e990d81..f45d9ff7ee 100644 --- a/src/Microsoft.AspNet.Routing/Resources.resx +++ b/src/Microsoft.AspNet.Routing/Resources.resx @@ -141,9 +141,6 @@ The constraint type '{0}' which is mapped to constraint key '{1}' must implement the '{2}' interface. - - The constraint entry '{0}' must have a string value or be of a type which implements '{1}'. - A path segment that contains more than one section, such as a literal section or a parameter, cannot contain a catch-all parameter. @@ -183,10 +180,10 @@ The route parameter name '{0}' appears more than one time in the route template. - - 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 the route '{2}' must have a string value or be of a type which implements '{3}'. - - The inline constraint resolver of type '{0}' was unable to resolve the following inline constraint: '{1}'. + + The constraint entry '{0}' - '{1}' on the route '{2}' could not be resolved by the constraint resolver of type '{3}'. \ No newline at end of file diff --git a/src/Microsoft.AspNet.Routing/RouteConstraintBuilder.cs b/src/Microsoft.AspNet.Routing/RouteConstraintBuilder.cs index fb0db9834b..707a9f7e7e 100644 --- a/src/Microsoft.AspNet.Routing/RouteConstraintBuilder.cs +++ b/src/Microsoft.AspNet.Routing/RouteConstraintBuilder.cs @@ -7,58 +7,52 @@ using Microsoft.AspNet.Routing.Constraints; namespace Microsoft.AspNet.Routing { + /// + /// A builder for produding a mapping of keys to see . + /// + /// + /// allows iterative building a set of route constraints, and will + /// merge multiple entries for the same key. + /// public class RouteConstraintBuilder { - public static IDictionary - BuildConstraints(IDictionary inputConstraints) + private readonly IInlineConstraintResolver _inlineConstraintResolver; + private readonly string _displayName; + + private readonly Dictionary> _constraints; + + /// + /// Creates a new instance. + /// + /// The . + /// The display name (for use in error messages). + public RouteConstraintBuilder( + [NotNull] IInlineConstraintResolver inlineConstraintResolver, + [NotNull] string displayName) { - return BuildConstraintsCore(inputConstraints, routeTemplate: null); + _inlineConstraintResolver = inlineConstraintResolver; + _displayName = displayName; + + _constraints = new Dictionary>(StringComparer.OrdinalIgnoreCase); } - public static IDictionary - BuildConstraints(IDictionary inputConstraints, [NotNull] string routeTemplate) + /// + /// Builds a mapping of constraints. + /// + /// An of the constraints. + public IReadOnlyDictionary Build() { - return BuildConstraintsCore(inputConstraints, routeTemplate); - } - - private static IDictionary - BuildConstraintsCore(IDictionary inputConstraints, string routeTemplate) - { - if (inputConstraints == null || inputConstraints.Count == 0) + var constraints = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var kvp in _constraints) { - return null; - } - - var constraints = new Dictionary(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; } + + /// + /// Adds a constraint instance for the given key. + /// + /// The key. + /// + /// The constraint instance. Must either be a string or an instance of . + /// + /// + /// If the is a string, it will be converted to a . + /// + /// For example, the string Product[0-9]+ will be converted to the regular expression + /// ^(Product[0-9]+). See for more details. + /// + 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); + } + + /// + /// Adds a constraint for the given key, resolved by the . + /// + /// The key. + /// The text to be resolved by . + /// + /// The can create instances + /// based on . See to register + /// custom constraint types. + /// + 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 list; + if (!_constraints.TryGetValue(key, out list)) + { + list = new List(); + _constraints.Add(key, list); + } + + list.Add(constraint); + } } } diff --git a/src/Microsoft.AspNet.Routing/RouteConstraintMatcher.cs b/src/Microsoft.AspNet.Routing/RouteConstraintMatcher.cs index 8fb5edcc56..1c04ba4432 100644 --- a/src/Microsoft.AspNet.Routing/RouteConstraintMatcher.cs +++ b/src/Microsoft.AspNet.Routing/RouteConstraintMatcher.cs @@ -11,7 +11,7 @@ namespace Microsoft.AspNet.Routing { public static class RouteConstraintMatcher { - public static bool Match(IDictionary constraints, + public static bool Match(IReadOnlyDictionary constraints, [NotNull] IDictionary routeValues, [NotNull] HttpContext httpContext, [NotNull] IRouter route, diff --git a/src/Microsoft.AspNet.Routing/RouteValueDictionary.cs b/src/Microsoft.AspNet.Routing/RouteValueDictionary.cs index 13a67420b3..bbe738eeab 100644 --- a/src/Microsoft.AspNet.Routing/RouteValueDictionary.cs +++ b/src/Microsoft.AspNet.Routing/RouteValueDictionary.cs @@ -12,8 +12,13 @@ namespace Microsoft.AspNet.Routing /// /// An type for route values. /// - public class RouteValueDictionary : IDictionary + public class RouteValueDictionary : IDictionary, IReadOnlyDictionary { + /// + /// An empty, cached instance of . + /// + internal static readonly IReadOnlyDictionary Empty = new RouteValueDictionary(); + private readonly Dictionary _dictionary; /// @@ -139,6 +144,14 @@ namespace Microsoft.AspNet.Routing } } + IEnumerable IReadOnlyDictionary.Keys + { + get + { + return _dictionary.Keys; + } + } + /// public Dictionary.ValueCollection Values { @@ -157,6 +170,14 @@ namespace Microsoft.AspNet.Routing } } + IEnumerable IReadOnlyDictionary.Values + { + get + { + return _dictionary.Values; + } + } + /// void ICollection>.Add(KeyValuePair item) { diff --git a/src/Microsoft.AspNet.Routing/Template/InlineConstraint.cs b/src/Microsoft.AspNet.Routing/Template/InlineConstraint.cs new file mode 100644 index 0000000000..f526e27408 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Template/InlineConstraint.cs @@ -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 +{ + /// + /// The parsed representation of an inline constraint in a route parameter. + /// + public class InlineConstraint + { + /// + /// Creates a new . + /// + /// The constraint text. + public InlineConstraint([NotNull] string constraint) + { + Constraint = constraint; + } + + /// + /// Gets the constraint text. + /// + public string Constraint { get; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Routing/Template/TemplateBinder.cs b/src/Microsoft.AspNet.Routing/Template/TemplateBinder.cs index aace63e088..2934b6361e 100644 --- a/src/Microsoft.AspNet.Routing/Template/TemplateBinder.cs +++ b/src/Microsoft.AspNet.Routing/Template/TemplateBinder.cs @@ -14,10 +14,10 @@ namespace Microsoft.AspNet.Routing.Template { public class TemplateBinder { - private readonly IDictionary _defaults; + private readonly IReadOnlyDictionary _defaults; private readonly RouteTemplate _template; - public TemplateBinder(RouteTemplate template, IDictionary defaults) + public TemplateBinder(RouteTemplate template, IReadOnlyDictionary defaults) { if (template == null) { @@ -336,12 +336,14 @@ namespace Microsoft.AspNet.Routing.Template [DebuggerDisplay("{DebuggerToString(),nq}")] private class TemplateBindingContext { - private readonly IDictionary _defaults; + private readonly IReadOnlyDictionary _defaults; private readonly RouteValueDictionary _acceptedValues; private readonly RouteValueDictionary _filters; - public TemplateBindingContext(IDictionary defaults, IDictionary values) + public TemplateBindingContext( + IReadOnlyDictionary defaults, + IDictionary 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); + } } } diff --git a/src/Microsoft.AspNet.Routing/Template/TemplateMatcher.cs b/src/Microsoft.AspNet.Routing/Template/TemplateMatcher.cs index 7179ed46a8..58d1e9e0cc 100644 --- a/src/Microsoft.AspNet.Routing/Template/TemplateMatcher.cs +++ b/src/Microsoft.AspNet.Routing/Template/TemplateMatcher.cs @@ -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 defaults) { - if (template == null) - { - throw new ArgumentNullException("template"); - } - Template = template; + Defaults = defaults ?? RouteValueDictionary.Empty; } + public IReadOnlyDictionary Defaults { get; private set; } + public RouteTemplate Template { get; private set; } - public IDictionary Match(string requestPath, IDictionary defaults) + public IDictionary 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 defaults, + IReadOnlyDictionary 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 diff --git a/src/Microsoft.AspNet.Routing/Template/TemplateParser.cs b/src/Microsoft.AspNet.Routing/Template/TemplateParser.cs index 48482902df..ebd32088c6 100644 --- a/src/Microsoft.AspNet.Routing/Template/TemplateParser.cs +++ b/src/Microsoft.AspNet.Routing/Template/TemplateParser.cs @@ -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(); 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 _parameterNames = new HashSet(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; diff --git a/src/Microsoft.AspNet.Routing/Template/TemplatePart.cs b/src/Microsoft.AspNet.Routing/Template/TemplatePart.cs index 1ab442eb60..f5d7953791 100644 --- a/src/Microsoft.AspNet.Routing/Template/TemplatePart.cs +++ b/src/Microsoft.AspNet.Routing/Template/TemplatePart.cs @@ -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 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(), }; } @@ -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 InlineConstraints { get; private set; } internal string DebuggerToString() { diff --git a/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs b/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs index b50aff5b1c..86409a6a5c 100644 --- a/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs +++ b/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs @@ -14,9 +14,9 @@ namespace Microsoft.AspNet.Routing.Template { public class TemplateRoute : INamedRouter { - private readonly IDictionary _defaults; - private readonly IDictionary _constraints; - private readonly IDictionary _dataTokens; + private readonly IReadOnlyDictionary _constraints; + private readonly IReadOnlyDictionary _dataTokens; + private readonly IReadOnlyDictionary _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(); - _dataTokens = dataTokens ?? new Dictionary(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 Defaults + public IReadOnlyDictionary Defaults { get { return _defaults; } } - public IDictionary DataTokens + public IReadOnlyDictionary DataTokens { get { return _dataTokens; } } @@ -86,7 +90,7 @@ namespace Microsoft.AspNet.Routing.Template get { return _routeTemplate; } } - public IDictionary Constraints + public IReadOnlyDictionary 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 GetConstraints( + IInlineConstraintResolver inlineConstraintResolver, + string template, + RouteTemplate parsedTemplate, + IDictionary 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 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 destination, + IReadOnlyDictionary 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) diff --git a/test/Microsoft.AspNet.Routing.Tests/ConstraintsBuilderTests.cs b/test/Microsoft.AspNet.Routing.Tests/ConstraintsBuilderTests.cs deleted file mode 100644 index 20b454f0a4..0000000000 --- a/test/Microsoft.AspNet.Routing.Tests/ConstraintsBuilderTests.cs +++ /dev/null @@ -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 input) - { - var result = RouteConstraintBuilder.BuildConstraints(input); - - Assert.Null(result); - } - - [Theory] - [MemberData("EmptyAndNullDictionary")] - public void ConstraintBuilderWithTemplateReturnsNull_OnNullOrEmptyInput(IDictionary 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(constraint); - } - - [Fact] - public void GetRouteDataWithConstraintsThatIsCustomConstraint_IsPassThrough() - { - // Arrange - var originalConstraint = new Mock().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( - () => 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( - () => 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().Object, - route: new Mock().Object, - routeKey: "controller", - values: dictionary, - routeDirection: RouteDirection.IncomingRequest)); - } - - public static IEnumerable EmptyAndNullDictionary - { - get - { - return new[] - { - new Object[] - { - null, - }, - - new Object[] - { - new Dictionary(), - }, - }; - } - } - } -} -#endif diff --git a/test/Microsoft.AspNet.Routing.Tests/InlineRouteParameterParserTests.cs b/test/Microsoft.AspNet.Routing.Tests/InlineRouteParameterParserTests.cs index dab86f1981..c59a32023c 100644 --- a/test/Microsoft.AspNet.Routing.Tests/InlineRouteParameterParserTests.cs +++ b/test/Microsoft.AspNet.Routing.Tests/InlineRouteParameterParserTests.cs @@ -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(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(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(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(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(param1.InlineConstraint); - var constraint = (CompositeRouteConstraint)param1.InlineConstraint; - Assert.IsType(constraint.Constraints.ElementAt(0)); - Assert.IsType(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(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(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(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(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(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(() => 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(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(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(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(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() diff --git a/test/Microsoft.AspNet.Routing.Tests/RouteConstraintBuilderTest.cs b/test/Microsoft.AspNet.Routing.Tests/RouteConstraintBuilderTest.cs new file mode 100644 index 0000000000..472fb238a9 --- /dev/null +++ b/test/Microsoft.AspNet.Routing.Tests/RouteConstraintBuilderTest.cs @@ -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(Assert.Single(result).Value); + } + + [Fact] + public void AddConstraint_IRouteConstraint() + { + // Arrange + var originalConstraint = Mock.Of(); + + 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(kvp.Value); + } + + [Fact] + public void AddConstraint_InvalidType_Throws() + { + // Arrange + var builder = CreateBuilder("{controller}/{action}"); + + // Act & Assert + ExceptionAssert.Throws( + () => 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( + () => 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().Object, + route: new Mock().Object, + routeKey: "controller", + values: routeValues, + routeDirection: RouteDirection.IncomingRequest)); + } + + private static RouteConstraintBuilder CreateBuilder(string template) + { + var services = new Mock(MockBehavior.Strict); + + var options = new Mock>(MockBehavior.Strict); + options + .SetupGet(o => o.Options) + .Returns(new RouteOptions()); + + var inlineConstraintResolver = new DefaultInlineConstraintResolver(services.Object, options.Object); + return new RouteConstraintBuilder(inlineConstraintResolver, template); + } + } +} +#endif diff --git a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateBinderTests.cs b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateBinderTests.cs index 05300dd6ed..3e462aa45c 100644 --- a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateBinderTests.cs +++ b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateBinderTests.cs @@ -129,13 +129,14 @@ namespace Microsoft.AspNet.Routing.Template.Tests [MemberData("EmptyAndNullDefaultValues")] public void Binding_WithEmptyAndNull_DefaultValues( string template, - IDictionary defaults, + IReadOnlyDictionary defaults, IDictionary 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 defaults, + IReadOnlyDictionary defaults, IDictionary ambientValues, IDictionary 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); diff --git a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateMatcherTests.cs b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateMatcherTests.cs index 586c2c8c27..03bd7bf308 100644 --- a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateMatcherTests.cs +++ b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateMatcherTests.cs @@ -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 defaults, IDictionary expected) + private static void RunTest( + string template, + string path, + IReadOnlyDictionary defaults, + IDictionary 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) diff --git a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateParserTests.cs b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateParserTests.cs index 51a9510fa3..c10847bbf4 100644 --- a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateParserTests.cs +++ b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateParserTests.cs @@ -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(expected, actual, new TemplateEqualityComparer()); @@ -42,11 +40,11 @@ namespace Microsoft.AspNet.Routing.Template.Tests var expected = new RouteTemplate(new List()); 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(expected, actual, new TemplateEqualityComparer()); @@ -60,11 +58,11 @@ namespace Microsoft.AspNet.Routing.Template.Tests var expected = new RouteTemplate(new List()); 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(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(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(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(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(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(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(expected, actual, new TemplateEqualityComparer()); @@ -233,7 +231,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests public void InvalidTemplate_WithRepeatedParameter() { var ex = ExceptionAssert.Throws( - () => 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( - () => 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( - () => 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( - () => 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( - () => 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( - () => 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( - () => 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( - () => 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( - () => 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( - () => 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( - () => 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( - () => 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( - () => 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( - () => 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( - () => 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( - () => 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( - () => 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( - () => 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( - () => 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( - () => 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( - () => 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( - () => 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( - () => 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( - () => 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>(); - return new DefaultInlineConstraintResolver(serviceProvider, accessor); - } - private class TemplateEqualityComparer : IEqualityComparer { public bool Equals(RouteTemplate x, RouteTemplate y) diff --git a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTest.cs b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTest.cs index 97a46bad8a..e5b37e5743 100644 --- a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTest.cs +++ b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTest.cs @@ -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(() => 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( + () => 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) ?? new RouteValueDictionary(constraints), (dataTokens as IDictionary) ?? - new RouteValueDictionary(constraints), + new RouteValueDictionary(dataTokens), _inlineConstraintResolver); }