Introduce RoutePattern (#585)

* Introduce RoutePattern

Introduces RoutePattern - a new parser and representation for routing
templates, defaults, and constraints.

This is a new representation for all of the 'inputs' to routing that is
immutable and captures 'out of line' information for defaults and
constraints.

This will allow us to unify the handling of constraints and values from
attribute style routes and conventional style routes.
This commit is contained in:
Ryan Nowak 2018-07-13 18:01:46 -07:00 committed by GitHub
parent bc79a47959
commit 9e114b547d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 5503 additions and 927 deletions

View File

@ -0,0 +1,248 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Linq;
namespace Microsoft.AspNetCore.Routing.Patterns
{
internal static class RouteParameterParser
{
// This code parses the inside of the route parameter
//
// Ex: {hello} - this method is responsible for parsing 'hello'
// The factoring between this class and RoutePatternParser is due to legacy.
public static RoutePatternParameterPart ParseRouteParameter(string parameter)
{
if (parameter == null)
{
throw new ArgumentNullException(nameof(parameter));
}
if (parameter.Length == 0)
{
return new RoutePatternParameterPart(string.Empty, null, RoutePatternParameterKind.Standard, Array.Empty<RoutePatternConstraintReference>());
}
var startIndex = 0;
var endIndex = parameter.Length - 1;
var parameterKind = RoutePatternParameterKind.Standard;
if (parameter[0] == '*')
{
parameterKind = RoutePatternParameterKind.CatchAll;
startIndex++;
}
if (parameter[endIndex] == '?')
{
parameterKind = RoutePatternParameterKind.Optional;
endIndex--;
}
var currentIndex = startIndex;
// Parse parameter name
var parameterName = string.Empty;
while (currentIndex <= endIndex)
{
var currentChar = parameter[currentIndex];
if ((currentChar == ':' || currentChar == '=') && startIndex != currentIndex)
{
// Parameter names are allowed to start with delimiters used to denote constraints or default values.
// i.e. "=foo" or ":bar" would be treated as parameter names rather than default value or constraint
// specifications.
parameterName = parameter.Substring(startIndex, currentIndex - startIndex);
// Roll the index back and move to the constraint parsing stage.
currentIndex--;
break;
}
else if (currentIndex == endIndex)
{
parameterName = parameter.Substring(startIndex, currentIndex - startIndex + 1);
}
currentIndex++;
}
var parseResults = ParseConstraints(parameter, parameterName, currentIndex, endIndex);
currentIndex = parseResults.CurrentIndex;
string defaultValue = null;
if (currentIndex <= endIndex &&
parameter[currentIndex] == '=')
{
defaultValue = parameter.Substring(currentIndex + 1, endIndex - currentIndex);
}
return new RoutePatternParameterPart(parameterName, defaultValue, parameterKind, parseResults.Constraints.ToArray());
}
private static ConstraintParseResults ParseConstraints(
string text,
string parameterName,
int currentIndex,
int endIndex)
{
var constraints = new List<RoutePatternConstraintReference>();
var state = ParseState.Start;
var startIndex = currentIndex;
do
{
var currentChar = currentIndex > endIndex ? null : (char?)text[currentIndex];
switch (state)
{
case ParseState.Start:
switch (currentChar)
{
case null:
state = ParseState.End;
break;
case ':':
state = ParseState.ParsingName;
startIndex = currentIndex + 1;
break;
case '(':
state = ParseState.InsideParenthesis;
break;
case '=':
state = ParseState.End;
currentIndex--;
break;
}
break;
case ParseState.InsideParenthesis:
switch (currentChar)
{
case null:
state = ParseState.End;
var constraintText = text.Substring(startIndex, currentIndex - startIndex);
constraints.Add(RoutePatternFactory.Constraint(parameterName, constraintText));
break;
case ')':
// Only consume a ')' token if
// (a) it is the last token
// (b) the next character is the start of the new constraint ':'
// (c) the next character is the start of the default value.
var nextChar = currentIndex + 1 > endIndex ? null : (char?)text[currentIndex + 1];
switch (nextChar)
{
case null:
state = ParseState.End;
constraintText = text.Substring(startIndex, currentIndex - startIndex + 1);
constraints.Add(RoutePatternFactory.Constraint(parameterName, constraintText));
break;
case ':':
state = ParseState.Start;
constraintText = text.Substring(startIndex, currentIndex - startIndex + 1);
constraints.Add(RoutePatternFactory.Constraint(parameterName, constraintText));
startIndex = currentIndex + 1;
break;
case '=':
state = ParseState.End;
constraintText = text.Substring(startIndex, currentIndex - startIndex + 1);
constraints.Add(RoutePatternFactory.Constraint(parameterName, constraintText));
break;
}
break;
case ':':
case '=':
// In the original implementation, the Regex would've backtracked if it encountered an
// unbalanced opening bracket followed by (not necessarily immediatiely) a delimiter.
// Simply verifying that the parantheses will eventually be closed should suffice to
// determine if the terminator needs to be consumed as part of the current constraint
// specification.
var indexOfClosingParantheses = text.IndexOf(')', currentIndex + 1);
if (indexOfClosingParantheses == -1)
{
constraintText = text.Substring(startIndex, currentIndex - startIndex);
constraints.Add(RoutePatternFactory.Constraint(parameterName, constraintText));
if (currentChar == ':')
{
state = ParseState.ParsingName;
startIndex = currentIndex + 1;
}
else
{
state = ParseState.End;
currentIndex--;
}
}
else
{
currentIndex = indexOfClosingParantheses;
}
break;
}
break;
case ParseState.ParsingName:
switch (currentChar)
{
case null:
state = ParseState.End;
var constraintText = text.Substring(startIndex, currentIndex - startIndex);
if (constraintText.Length > 0)
{
constraints.Add(RoutePatternFactory.Constraint(parameterName, constraintText));
}
break;
case ':':
constraintText = text.Substring(startIndex, currentIndex - startIndex);
if (constraintText.Length > 0)
{
constraints.Add(RoutePatternFactory.Constraint(parameterName, constraintText));
}
startIndex = currentIndex + 1;
break;
case '(':
state = ParseState.InsideParenthesis;
break;
case '=':
state = ParseState.End;
constraintText = text.Substring(startIndex, currentIndex - startIndex);
if (constraintText.Length > 0)
{
constraints.Add(RoutePatternFactory.Constraint(parameterName, constraintText));
}
currentIndex--;
break;
}
break;
}
currentIndex++;
} while (state != ParseState.End);
return new ConstraintParseResults(currentIndex, constraints);
}
private enum ParseState
{
Start,
ParsingName,
InsideParenthesis,
End
}
private readonly struct ConstraintParseResults
{
public readonly int CurrentIndex;
public readonly IReadOnlyList<RoutePatternConstraintReference> Constraints;
public ConstraintParseResults(int currentIndex, IReadOnlyList<RoutePatternConstraintReference> constraints)
{
CurrentIndex = currentIndex;
Constraints = constraints;
}
}
}
}

View File

@ -0,0 +1,74 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
namespace Microsoft.AspNetCore.Routing.Patterns
{
[DebuggerDisplay("{DebuggerToString()}")]
public sealed class RoutePattern
{
private const string SeparatorString = "/";
internal RoutePattern(
string rawText,
Dictionary<string, object> defaults,
Dictionary<string, IReadOnlyList<RoutePatternConstraintReference>> constraints,
RoutePatternParameterPart[] parameters,
RoutePatternPathSegment[] pathSegments)
{
Debug.Assert(defaults != null);
Debug.Assert(constraints != null);
Debug.Assert(parameters != null);
Debug.Assert(pathSegments != null);
RawText = rawText;
Defaults = defaults;
Constraints = constraints;
Parameters = parameters;
PathSegments = pathSegments;
}
public IReadOnlyDictionary<string, object> Defaults { get; }
public IReadOnlyDictionary<string, IReadOnlyList<RoutePatternConstraintReference>> Constraints { get; }
public string RawText { get; }
public IReadOnlyList<RoutePatternParameterPart> Parameters { get; }
public IReadOnlyList<RoutePatternPathSegment> PathSegments { get; }
/// <summary>
/// Gets the parameter matching the given name.
/// </summary>
/// <param name="name">The name of the parameter to match.</param>
/// <returns>The matching parameter or <c>null</c> if no parameter matches the given name.</returns>
public RoutePatternParameterPart GetParameter(string name)
{
if (name == null)
{
throw new ArgumentNullException(nameof(name));
}
for (var i = 0; i < Parameters.Count; i++)
{
var parameter = Parameters[i];
if (string.Equals(parameter.Name, name, StringComparison.OrdinalIgnoreCase))
{
return parameter;
}
}
return null;
}
private string DebuggerToString()
{
return RawText ?? string.Join(SeparatorString, PathSegments.Select(s => s.DebuggerToString()));
}
}
}

View File

@ -0,0 +1,46 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Diagnostics;
namespace Microsoft.AspNetCore.Routing.Patterns
{
/// <summary>
/// The parsed representation of a constraint in a <see cref="RoutePattern"/> parameter.
/// </summary>
[DebuggerDisplay("{DebuggerToString()}")]
public sealed class RoutePatternConstraintReference
{
internal RoutePatternConstraintReference(string parameterName, string content)
{
ParameterName = parameterName;
Content = content;
}
internal RoutePatternConstraintReference(string parameterName, IRouteConstraint constraint)
{
ParameterName = parameterName;
Constraint = constraint;
}
/// <summary>
/// Gets the constraint text.
/// </summary>
public string Content { get; }
/// <summary>
/// Gets a pre-existing <see cref="IRouteConstraint"/> that was used to construct this reference.
/// </summary>
public IRouteConstraint Constraint { get; }
/// <summary>
/// Gets the parameter name associated with the constraint.
/// </summary>
public string ParameterName { get; }
private string DebuggerToString()
{
return Content;
}
}
}

View File

@ -0,0 +1,42 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Runtime.Serialization;
namespace Microsoft.AspNetCore.Routing.Patterns
{
[Serializable]
public sealed class RoutePatternException : Exception
{
private RoutePatternException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
Pattern = (string)info.GetValue(nameof(Pattern), typeof(string));
}
public RoutePatternException(string pattern, string message)
: base(message)
{
if (pattern == null)
{
throw new ArgumentNullException(nameof(pattern));
}
if (message == null)
{
throw new ArgumentNullException(nameof(message));
}
Pattern = pattern;
}
public string Pattern { get; }
public override void GetObjectData(SerializationInfo info, StreamingContext context)
{
info.AddValue(nameof(Pattern), Pattern);
base.GetObjectData(info, context);
}
}
}

View File

@ -0,0 +1,504 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Routing.Constraints;
namespace Microsoft.AspNetCore.Routing.Patterns
{
public static class RoutePatternFactory
{
public static RoutePattern Parse(string pattern)
{
if (pattern == null)
{
throw new ArgumentNullException(nameof(pattern));
}
return RoutePatternParser.Parse(pattern);
}
public static RoutePattern Parse(string pattern, object defaults, object constraints)
{
if (pattern == null)
{
throw new ArgumentNullException(nameof(pattern));
}
var original = RoutePatternParser.Parse(pattern);
return Pattern(original.RawText, defaults, constraints, original.PathSegments);
}
public static RoutePattern Pattern(IEnumerable<RoutePatternPathSegment> segments)
{
if (segments == null)
{
throw new ArgumentNullException(nameof(segments));
}
return PatternCore(null, null, null, segments);
}
public static RoutePattern Pattern(string rawText, IEnumerable<RoutePatternPathSegment> segments)
{
if (segments == null)
{
throw new ArgumentNullException(nameof(segments));
}
return PatternCore(rawText, null, null, segments);
}
public static RoutePattern Pattern(
object defaults,
object constraints,
IEnumerable<RoutePatternPathSegment> segments)
{
if (segments == null)
{
throw new ArgumentNullException(nameof(segments));
}
return PatternCore(null, new RouteValueDictionary(defaults), new RouteValueDictionary(constraints), segments);
}
public static RoutePattern Pattern(
string rawText,
object defaults,
object constraints,
IEnumerable<RoutePatternPathSegment> segments)
{
if (segments == null)
{
throw new ArgumentNullException(nameof(segments));
}
return PatternCore(rawText, new RouteValueDictionary(defaults), new RouteValueDictionary(constraints), segments);
}
public static RoutePattern Pattern(params RoutePatternPathSegment[] segments)
{
if (segments == null)
{
throw new ArgumentNullException(nameof(segments));
}
return PatternCore(null, null, null, segments);
}
public static RoutePattern Pattern(string rawText, params RoutePatternPathSegment[] segments)
{
if (segments == null)
{
throw new ArgumentNullException(nameof(segments));
}
return PatternCore(rawText, null, null, segments);
}
public static RoutePattern Pattern(
object defaults,
object constraints,
params RoutePatternPathSegment[] segments)
{
if (segments == null)
{
throw new ArgumentNullException(nameof(segments));
}
return PatternCore(null, new RouteValueDictionary(defaults), new RouteValueDictionary(constraints), segments);
}
public static RoutePattern Pattern(
string rawText,
object defaults,
object constraints,
params RoutePatternPathSegment[] segments)
{
if (segments == null)
{
throw new ArgumentNullException(nameof(segments));
}
return PatternCore(rawText, new RouteValueDictionary(defaults), new RouteValueDictionary(constraints), segments);
}
private static RoutePattern PatternCore(
string rawText,
IDictionary<string, object> defaults,
IDictionary<string, object> constraints,
IEnumerable<RoutePatternPathSegment> segments)
{
// We want to merge the segment data with the 'out of line' defaults and constraints.
//
// This means that for parameters that have 'out of line' defaults we will modify
// the parameter to contain the default (same story for constraints).
//
// We also maintain a collection of defaults and constraints that will also
// contain the values that don't match a parameter.
//
// It's important that these two views of the data are consistent. We don't want
// values specified out of line to have a different behavior.
var updatedDefaults = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
if (defaults != null)
{
foreach (var kvp in defaults)
{
updatedDefaults.Add(kvp.Key, kvp.Value);
}
}
var updatedConstraints = new Dictionary<string, List<RoutePatternConstraintReference>>(StringComparer.OrdinalIgnoreCase);
if (constraints != null)
{
foreach (var kvp in constraints)
{
updatedConstraints.Add(kvp.Key, new List<RoutePatternConstraintReference>()
{
Constraint(kvp.Key, kvp.Value),
});
}
}
var parameters = new List<RoutePatternParameterPart>();
var updatedSegments = segments.ToArray();
for (var i = 0; i < updatedSegments.Length; i++)
{
var segment = VisitSegment(updatedSegments[i]);
updatedSegments[i] = segment;
for (var j = 0; j < segment.Parts.Count; j++)
{
if (segment.Parts[j] is RoutePatternParameterPart parameter)
{
parameters.Add(parameter);
}
}
}
return new RoutePattern(
rawText,
updatedDefaults,
updatedConstraints.ToDictionary(kvp => kvp.Key, kvp => (IReadOnlyList<RoutePatternConstraintReference>)kvp.Value.ToArray()),
parameters.ToArray(),
updatedSegments.ToArray());
RoutePatternPathSegment VisitSegment(RoutePatternPathSegment segment)
{
var updatedParts = new RoutePatternPart[segment.Parts.Count];
for (var i = 0; i < segment.Parts.Count; i++)
{
var part = segment.Parts[i];
updatedParts[i] = VisitPart(part);
}
return SegmentCore(updatedParts);
}
RoutePatternPart VisitPart(RoutePatternPart part)
{
if (!part.IsParameter)
{
return part;
}
var parameter = (RoutePatternParameterPart)part;
var @default = parameter.Default;
if (updatedDefaults.TryGetValue(parameter.Name, out var newDefault))
{
if (parameter.Default != null)
{
var message = Resources.FormatTemplateRoute_CannotHaveDefaultValueSpecifiedInlineAndExplicitly(parameter.Name);
throw new InvalidOperationException(message);
}
if (parameter.IsOptional)
{
var message = Resources.TemplateRoute_OptionalCannotHaveDefaultValue;
throw new InvalidOperationException(message);
}
@default = newDefault;
}
if (parameter.Default != null)
{
updatedDefaults.Add(parameter.Name, parameter.Default);
}
if (!updatedConstraints.TryGetValue(parameter.Name, out var parameterConstraints) &&
parameter.Constraints.Count > 0)
{
parameterConstraints = new List<RoutePatternConstraintReference>();
updatedConstraints.Add(parameter.Name, parameterConstraints);
}
if (parameter.Constraints.Count > 0)
{
parameterConstraints.AddRange(parameter.Constraints);
}
return ParameterPartCore(
parameter.Name,
@default,
parameter.ParameterKind,
(IEnumerable<RoutePatternConstraintReference>)parameterConstraints ?? Array.Empty<RoutePatternConstraintReference>());
}
}
public static RoutePatternPathSegment Segment(IEnumerable<RoutePatternPart> parts)
{
if (parts == null)
{
throw new ArgumentNullException(nameof(parts));
}
return SegmentCore(parts);
}
public static RoutePatternPathSegment Segment(params RoutePatternPart[] parts)
{
if (parts == null)
{
throw new ArgumentNullException(nameof(parts));
}
return SegmentCore(parts);
}
private static RoutePatternPathSegment SegmentCore(IEnumerable<RoutePatternPart> parts)
{
return new RoutePatternPathSegment(parts.ToArray());
}
public static RoutePatternLiteralPart LiteralPart(string content)
{
if (string.IsNullOrEmpty(content))
{
throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(content));
}
if (content.IndexOf('?') >= 0)
{
throw new ArgumentException(Resources.FormatTemplateRoute_InvalidLiteral(content));
}
return LiteralPartCore(content);
}
private static RoutePatternLiteralPart LiteralPartCore(string content)
{
return new RoutePatternLiteralPart(content);
}
public static RoutePatternSeparatorPart SeparatorPart(string content)
{
if (string.IsNullOrEmpty(content))
{
throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(content));
}
return SeparatorPartCore(content);
}
private static RoutePatternSeparatorPart SeparatorPartCore(string content)
{
return new RoutePatternSeparatorPart(content);
}
public static RoutePatternParameterPart ParameterPart(string parameterName)
{
if (string.IsNullOrEmpty(parameterName))
{
throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(parameterName));
}
if (parameterName.IndexOfAny(RoutePatternParser.InvalidParameterNameChars) >= 0)
{
throw new ArgumentException(Resources.FormatTemplateRoute_InvalidParameterName(parameterName));
}
return ParameterPartCore(
parameterName: parameterName,
@default: null,
parameterKind: RoutePatternParameterKind.Standard,
constraints: Array.Empty<RoutePatternConstraintReference>());
}
public static RoutePatternParameterPart ParameterPart(string parameterName, object @default)
{
if (string.IsNullOrEmpty(parameterName))
{
throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(parameterName));
}
if (parameterName.IndexOfAny(RoutePatternParser.InvalidParameterNameChars) >= 0)
{
throw new ArgumentException(Resources.FormatTemplateRoute_InvalidParameterName(parameterName));
}
return ParameterPartCore(
parameterName: parameterName,
@default: @default,
parameterKind: RoutePatternParameterKind.Standard,
constraints: Array.Empty<RoutePatternConstraintReference>());
}
public static RoutePatternParameterPart ParameterPart(
string parameterName,
object @default,
RoutePatternParameterKind parameterKind)
{
if (string.IsNullOrEmpty(parameterName))
{
throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(parameterName));
}
if (parameterName.IndexOfAny(RoutePatternParser.InvalidParameterNameChars) >= 0)
{
throw new ArgumentException(Resources.FormatTemplateRoute_InvalidParameterName(parameterName));
}
if (@default != null && parameterKind == RoutePatternParameterKind.Optional)
{
throw new ArgumentNullException(Resources.TemplateRoute_OptionalCannotHaveDefaultValue, nameof(parameterKind));
}
return ParameterPartCore(
parameterName: parameterName,
@default: @default,
parameterKind: parameterKind,
constraints: Array.Empty<RoutePatternConstraintReference>());
}
public static RoutePatternParameterPart ParameterPart(
string parameterName,
object @default,
RoutePatternParameterKind parameterKind,
IEnumerable<RoutePatternConstraintReference> constraints)
{
if (string.IsNullOrEmpty(parameterName))
{
throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(parameterName));
}
if (parameterName.IndexOfAny(RoutePatternParser.InvalidParameterNameChars) >= 0)
{
throw new ArgumentException(Resources.FormatTemplateRoute_InvalidParameterName(parameterName));
}
if (@default != null && parameterKind == RoutePatternParameterKind.Optional)
{
throw new ArgumentNullException(Resources.TemplateRoute_OptionalCannotHaveDefaultValue, nameof(parameterKind));
}
if (constraints == null)
{
throw new ArgumentNullException(nameof(constraints));
}
return ParameterPartCore(
parameterName: parameterName,
@default: @default,
parameterKind: parameterKind,
constraints: constraints);
}
public static RoutePatternParameterPart ParameterPart(
string parameterName,
object @default,
RoutePatternParameterKind parameterKind,
params RoutePatternConstraintReference[] constraints)
{
if (string.IsNullOrEmpty(parameterName))
{
throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(parameterName));
}
if (parameterName.IndexOfAny(RoutePatternParser.InvalidParameterNameChars) >= 0)
{
throw new ArgumentException(Resources.FormatTemplateRoute_InvalidParameterName(parameterName));
}
if (@default != null && parameterKind == RoutePatternParameterKind.Optional)
{
throw new ArgumentNullException(Resources.TemplateRoute_OptionalCannotHaveDefaultValue, nameof(parameterKind));
}
if (constraints == null)
{
throw new ArgumentNullException(nameof(constraints));
}
return ParameterPartCore(
parameterName: parameterName,
@default: @default,
parameterKind: parameterKind,
constraints: constraints);
}
private static RoutePatternParameterPart ParameterPartCore(
string parameterName,
object @default,
RoutePatternParameterKind parameterKind,
IEnumerable<RoutePatternConstraintReference> constraints)
{
return new RoutePatternParameterPart(parameterName, @default, parameterKind, constraints.ToArray());
}
public static RoutePatternConstraintReference Constraint(string parameterName, object constraint)
{
// Similar to RouteConstraintBuilder
if (constraint is IRouteConstraint routeConstraint)
{
return ConstraintCore(parameterName, routeConstraint);
}
else if (constraint is string content)
{
return ConstraintCore(parameterName, new RegexRouteConstraint("^(" + content + ")$"));
}
else
{
throw new InvalidOperationException(Resources.FormatConstraintMustBeStringOrConstraint(
parameterName,
constraint,
typeof(IRouteConstraint)));
}
}
public static RoutePatternConstraintReference Constraint(string parameterName, IRouteConstraint constraint)
{
if (constraint == null)
{
throw new ArgumentNullException(nameof(constraint));
}
return ConstraintCore(parameterName, constraint);
}
public static RoutePatternConstraintReference Constraint(string parameterName, string constraint)
{
if (string.IsNullOrEmpty(constraint))
{
throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(constraint));
}
return ConstraintCore(parameterName, constraint);
}
private static RoutePatternConstraintReference ConstraintCore(string parameterName, IRouteConstraint constraint)
{
return new RoutePatternConstraintReference(parameterName, constraint);
}
private static RoutePatternConstraintReference ConstraintCore(string parameterName, string constraint)
{
return new RoutePatternConstraintReference(parameterName, constraint);
}
}
}

View File

@ -0,0 +1,25 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Diagnostics;
namespace Microsoft.AspNetCore.Routing.Patterns
{
[DebuggerDisplay("{DebuggerToString()}")]
public sealed class RoutePatternLiteralPart : RoutePatternPart
{
internal RoutePatternLiteralPart(string content)
: base(RoutePatternPartKind.Literal)
{
Debug.Assert(!string.IsNullOrEmpty(content));
Content = content;
}
public string Content { get; }
internal override string DebuggerToString()
{
return Content;
}
}
}

View File

@ -0,0 +1,511 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using Microsoft.AspNetCore.Routing.Internal;
using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNetCore.Routing
{
public class RoutePatternMatcher
{
private const string SeparatorString = "/";
private const char SeparatorChar = '/';
// Perf: This is a cache to avoid looking things up in 'Defaults' each request.
private readonly bool[] _hasDefaultValue;
private readonly object[] _defaultValues;
private static readonly char[] Delimiters = new char[] { SeparatorChar };
public RoutePatternMatcher(
RoutePattern pattern,
RouteValueDictionary defaults)
{
if (pattern == null)
{
throw new ArgumentNullException(nameof(pattern));
}
RoutePattern = pattern;
Defaults = defaults ?? new RouteValueDictionary();
// Perf: cache the default value for each parameter (other than complex segments).
_hasDefaultValue = new bool[RoutePattern.PathSegments.Count];
_defaultValues = new object[RoutePattern.PathSegments.Count];
for (var i = 0; i < RoutePattern.PathSegments.Count; i++)
{
var segment = RoutePattern.PathSegments[i];
if (!segment.IsSimple)
{
continue;
}
var part = segment.Parts[0];
if (!part.IsParameter)
{
continue;
}
var parameter = (RoutePatternParameterPart)part;
if (Defaults.TryGetValue(parameter.Name, out var value))
{
_hasDefaultValue[i] = true;
_defaultValues[i] = value;
}
}
}
public RouteValueDictionary Defaults { get; }
public RoutePattern RoutePattern { get; }
public bool TryMatch(PathString path, RouteValueDictionary values)
{
if (values == null)
{
throw new ArgumentNullException(nameof(values));
}
var i = 0;
var pathTokenizer = new PathTokenizer(path);
// Perf: We do a traversal of the request-segments + route-segments twice.
//
// For most segment-types, we only really need to any work on one of the two passes.
//
// On the first pass, we're just looking to see if there's anything that would disqualify us from matching.
// The most common case would be a literal segment that doesn't match.
//
// On the second pass, we're almost certainly going to match the URL, so go ahead and allocate the 'values'
// and start capturing strings.
foreach (var stringSegment in pathTokenizer)
{
if (stringSegment.Length == 0)
{
return false;
}
var pathSegment = i >= RoutePattern.PathSegments.Count ? null : RoutePattern.PathSegments[i];
if (pathSegment == null && stringSegment.Length > 0)
{
// If pathSegment is null, then we're out of route segments. All we can match is the empty
// string.
return false;
}
else if (pathSegment.IsSimple && pathSegment.Parts[0] is RoutePatternParameterPart parameter && parameter.IsCatchAll)
{
// Nothing to validate for a catch-all - it can match any string, including the empty string.
//
// Also, a catch-all has to be the last part, so we're done.
break;
}
if (!TryMatchLiterals(i++, stringSegment, pathSegment))
{
return false;
}
}
for (; i < RoutePattern.PathSegments.Count; i++)
{
// We've matched the request path so far, but still have remaining route segments. These need
// to be all single-part parameter segments with default values or else they won't match.
var pathSegment = RoutePattern.PathSegments[i];
Debug.Assert(pathSegment != null);
if (!pathSegment.IsSimple)
{
// If the segment is a complex segment, it MUST contain literals, and we've parsed the full
// path so far, so it can't match.
return false;
}
var part = pathSegment.Parts[0];
if (part.IsLiteral || part.IsSeparator)
{
// If the segment is a simple literal - which need the URL to provide a value, so we don't match.
return false;
}
var parameter = (RoutePatternParameterPart)part;
if (parameter.IsCatchAll)
{
// Nothing to validate for a catch-all - it can match any string, including the empty string.
//
// Also, a catch-all has to be the last part, so we're done.
break;
}
// If we get here, this is a simple segment with a parameter. We need it to be optional, or for the
// defaults to have a value.
if (!_hasDefaultValue[i] && !parameter.IsOptional)
{
// There's no default for this (non-optional) parameter so it can't match.
return false;
}
}
// At this point we've very likely got a match, so start capturing values for real.
i = 0;
foreach (var requestSegment in pathTokenizer)
{
var pathSegment = RoutePattern.PathSegments[i++];
if (SavePathSegmentsAsValues(i, values, requestSegment, pathSegment))
{
break;
}
if (!pathSegment.IsSimple)
{
if (!MatchComplexSegment(pathSegment, requestSegment.ToString(), Defaults, values))
{
return false;
}
}
}
for (; i < RoutePattern.PathSegments.Count; i++)
{
// We've matched the request path so far, but still have remaining route segments. We already know these
// are simple parameters that either have a default, or don't need to produce a value.
var pathSegment = RoutePattern.PathSegments[i];
Debug.Assert(pathSegment != null);
Debug.Assert(pathSegment.IsSimple);
var part = pathSegment.Parts[0];
Debug.Assert(part.IsParameter);
// It's ok for a catch-all to produce a null value
if (part is RoutePatternParameterPart parameter && (parameter.IsCatchAll || _hasDefaultValue[i]))
{
// Don't replace an existing value with a null.
var defaultValue = _defaultValues[i];
if (defaultValue != null || !values.ContainsKey(parameter.Name))
{
values[parameter.Name] = defaultValue;
}
}
}
// Copy all remaining default values to the route data
foreach (var kvp in Defaults)
{
if (!values.ContainsKey(kvp.Key))
{
values.Add(kvp.Key, kvp.Value);
}
}
return true;
}
private bool TryMatchLiterals(int index, StringSegment stringSegment, RoutePatternPathSegment pathSegment)
{
if (pathSegment.IsSimple && !pathSegment.Parts[0].IsParameter)
{
// This is a literal segment, so we need to match the text, or the route isn't a match.
if (pathSegment.Parts[0].IsLiteral)
{
var part = (RoutePatternLiteralPart)pathSegment.Parts[0];
if (!stringSegment.Equals(part.Content, StringComparison.OrdinalIgnoreCase))
{
return false;
}
}
else
{
var part = (RoutePatternSeparatorPart)pathSegment.Parts[0];
if (!stringSegment.Equals(part.Content, StringComparison.OrdinalIgnoreCase))
{
return false;
}
}
}
else if (pathSegment.IsSimple && pathSegment.Parts[0].IsParameter)
{
// For a parameter, validate that it's a has some length, or we have a default, or it's optional.
var part = (RoutePatternParameterPart)pathSegment.Parts[0];
if (stringSegment.Length == 0 &&
!_hasDefaultValue[index] &&
!part.IsOptional)
{
// There's no value for this parameter, the route can't match.
return false;
}
}
else
{
Debug.Assert(!pathSegment.IsSimple);
// Don't attempt to validate a complex segment at this point other than being non-emtpy,
// do it in the second pass.
}
return true;
}
private bool SavePathSegmentsAsValues(int index, RouteValueDictionary values, StringSegment requestSegment, RoutePatternPathSegment pathSegment)
{
if (pathSegment.IsSimple && pathSegment.Parts[0] is RoutePatternParameterPart parameter && parameter.IsCatchAll)
{
// A catch-all captures til the end of the string.
var captured = requestSegment.Buffer.Substring(requestSegment.Offset);
if (captured.Length > 0)
{
values[parameter.Name] = captured;
}
else
{
// It's ok for a catch-all to produce a null value, so we don't check _hasDefaultValue.
values[parameter.Name] = _defaultValues[index];
}
// A catch-all has to be the last part, so we're done.
return true;
}
else if (pathSegment.IsSimple && pathSegment.Parts[0].IsParameter)
{
// A simple parameter captures the whole segment, or a default value if nothing was
// provided.
parameter = (RoutePatternParameterPart)pathSegment.Parts[0];
if (requestSegment.Length > 0)
{
values[parameter.Name] = requestSegment.ToString();
}
else
{
if (_hasDefaultValue[index])
{
values[parameter.Name] = _defaultValues[index];
}
}
}
return false;
}
private bool MatchComplexSegment(
RoutePatternPathSegment routeSegment,
string requestSegment,
IReadOnlyDictionary<string, object> defaults,
RouteValueDictionary values)
{
var indexOfLastSegment = routeSegment.Parts.Count - 1;
// We match the request to the template starting at the rightmost parameter
// If the last segment of template is optional, then request can match the
// template with or without the last parameter. So we start with regular matching,
// but if it doesn't match, we start with next to last parameter. Example:
// Template: {p1}/{p2}.{p3?}. If the request is one/two.three it will match right away
// giving p3 value of three. But if the request is one/two, we start matching from the
// rightmost giving p3 the value of two, then we end up not matching the segment.
// In this case we start again from p2 to match the request and we succeed giving
// the value two to p2
if (routeSegment.Parts[indexOfLastSegment] is RoutePatternParameterPart parameter && parameter.IsOptional &&
routeSegment.Parts[indexOfLastSegment - 1].IsSeparator)
{
if (MatchComplexSegmentCore(routeSegment, requestSegment, Defaults, values, indexOfLastSegment))
{
return true;
}
else
{
var separator = (RoutePatternSeparatorPart)routeSegment.Parts[indexOfLastSegment - 1];
if (requestSegment.EndsWith(
separator.Content,
StringComparison.OrdinalIgnoreCase))
return false;
return MatchComplexSegmentCore(
routeSegment,
requestSegment,
Defaults,
values,
indexOfLastSegment - 2);
}
}
else
{
return MatchComplexSegmentCore(routeSegment, requestSegment, Defaults, values, indexOfLastSegment);
}
}
private bool MatchComplexSegmentCore(
RoutePatternPathSegment routeSegment,
string requestSegment,
IReadOnlyDictionary<string, object> defaults,
RouteValueDictionary values,
int indexOfLastSegmentUsed)
{
Debug.Assert(routeSegment != null);
Debug.Assert(routeSegment.Parts.Count > 1);
// Find last literal segment and get its last index in the string
var lastIndex = requestSegment.Length;
RoutePatternParameterPart parameterNeedsValue = null; // Keeps track of a parameter segment that is pending a value
RoutePatternPart lastLiteral = null; // Keeps track of the left-most literal we've encountered
var outValues = new RouteValueDictionary();
while (indexOfLastSegmentUsed >= 0)
{
var newLastIndex = lastIndex;
var part = routeSegment.Parts[indexOfLastSegmentUsed];
if (part.IsParameter)
{
// Hold on to the parameter so that we can fill it in when we locate the next literal
parameterNeedsValue = (RoutePatternParameterPart)part;
}
else
{
Debug.Assert(part.IsLiteral || part.IsSeparator);
lastLiteral = part;
var startIndex = lastIndex - 1;
// If we have a pending parameter subsegment, we must leave at least one character for that
if (parameterNeedsValue != null)
{
startIndex--;
}
if (startIndex < 0)
{
return false;
}
int indexOfLiteral;
if (part.IsLiteral)
{
var literal = (RoutePatternLiteralPart)part;
indexOfLiteral = requestSegment.LastIndexOf(
literal.Content,
startIndex,
StringComparison.OrdinalIgnoreCase);
}
else
{
var literal = (RoutePatternSeparatorPart)part;
indexOfLiteral = requestSegment.LastIndexOf(
literal.Content,
startIndex,
StringComparison.OrdinalIgnoreCase);
}
if (indexOfLiteral == -1)
{
// If we couldn't find this literal index, this segment cannot match
return false;
}
// If the first subsegment is a literal, it must match at the right-most extent of the request URI.
// Without this check if your route had "/Foo/" we'd match the request URI "/somethingFoo/".
// This check is related to the check we do at the very end of this function.
if (indexOfLastSegmentUsed == (routeSegment.Parts.Count - 1))
{
if (part is RoutePatternLiteralPart literal && ((indexOfLiteral + literal.Content.Length) != requestSegment.Length))
{
return false;
}
else if (part is RoutePatternSeparatorPart separator && ((indexOfLiteral + separator.Content.Length) != requestSegment.Length))
{
return false;
}
}
newLastIndex = indexOfLiteral;
}
if ((parameterNeedsValue != null) &&
(((lastLiteral != null) && !part.IsParameter) || (indexOfLastSegmentUsed == 0)))
{
// If we have a pending parameter that needs a value, grab that value
int parameterStartIndex;
int parameterTextLength;
if (lastLiteral == null)
{
if (indexOfLastSegmentUsed == 0)
{
parameterStartIndex = 0;
}
else
{
parameterStartIndex = newLastIndex;
Debug.Assert(false, "indexOfLastSegementUsed should always be 0 from the check above");
}
parameterTextLength = lastIndex;
}
else
{
// If we're getting a value for a parameter that is somewhere in the middle of the segment
if ((indexOfLastSegmentUsed == 0) && (part.IsParameter))
{
parameterStartIndex = 0;
parameterTextLength = lastIndex;
}
else
{
if (lastLiteral.IsLiteral)
{
var literal = (RoutePatternLiteralPart)lastLiteral;
parameterStartIndex = newLastIndex + literal.Content.Length;
}
else
{
var separator = (RoutePatternSeparatorPart)lastLiteral;
parameterStartIndex = newLastIndex + separator.Content.Length;
}
parameterTextLength = lastIndex - parameterStartIndex;
}
}
var parameterValueString = requestSegment.Substring(parameterStartIndex, parameterTextLength);
if (string.IsNullOrEmpty(parameterValueString))
{
// If we're here that means we have a segment that contains multiple sub-segments.
// For these segments all parameters must have non-empty values. If the parameter
// has an empty value it's not a match.
return false;
}
else
{
// If there's a value in the segment for this parameter, use the subsegment value
outValues.Add(parameterNeedsValue.Name, parameterValueString);
}
parameterNeedsValue = null;
lastLiteral = null;
}
lastIndex = newLastIndex;
indexOfLastSegmentUsed--;
}
// If the last subsegment is a parameter, it's OK that we didn't parse all the way to the left extent of
// the string since the parameter will have consumed all the remaining text anyway. If the last subsegment
// is a literal then we *must* have consumed the entire text in that literal. Otherwise we end up matching
// the route "Foo" to the request URI "somethingFoo". Thus we have to check that we parsed the *entire*
// request URI in order for it to be a match.
// This check is related to the check we do earlier in this function for LiteralSubsegments.
if (lastIndex == 0 || routeSegment.Parts[0].IsParameter)
{
foreach (var item in outValues)
{
values.Add(item.Key, item.Value);
}
return true;
}
return false;
}
}
}

View File

@ -0,0 +1,12 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.Routing.Patterns
{
public enum RoutePatternParameterKind
{
Standard,
Optional,
CatchAll,
}
}

View File

@ -0,0 +1,73 @@
// Copyright (c) .NET Foundation. 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.Text;
namespace Microsoft.AspNetCore.Routing.Patterns
{
[DebuggerDisplay("{DebuggerToString()}")]
public class RoutePatternParameterPart : RoutePatternPart
{
internal RoutePatternParameterPart(
string parameterName,
object @default,
RoutePatternParameterKind parameterKind,
RoutePatternConstraintReference[] constraints)
: base(RoutePatternPartKind.Parameter)
{
// See #475 - this code should have some asserts, but it can't because of the design of RouteParameterParser.
Name = parameterName;
Default = @default;
ParameterKind = parameterKind;
Constraints = constraints;
}
public IReadOnlyList<RoutePatternConstraintReference> Constraints { get; }
public object Default { get; }
public bool IsCatchAll => ParameterKind == RoutePatternParameterKind.CatchAll;
public bool IsOptional => ParameterKind == RoutePatternParameterKind.Optional;
public RoutePatternParameterKind ParameterKind { get; }
public string Name { get; }
internal override string DebuggerToString()
{
var builder = new StringBuilder();
builder.Append("{");
if (IsCatchAll)
{
builder.Append("*");
}
builder.Append(Name);
foreach (var constraint in Constraints)
{
builder.Append(":");
builder.Append(constraint.Constraint);
}
if (Default != null)
{
builder.Append("=");
builder.Append(Default);
}
if (IsOptional)
{
builder.Append("?");
}
builder.Append("}");
return builder.ToString();
}
}
}

View File

@ -0,0 +1,577 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Diagnostics;
namespace Microsoft.AspNetCore.Routing.Patterns
{
internal static class RoutePatternParser
{
private const char Separator = '/';
private const char OpenBrace = '{';
private const char CloseBrace = '}';
private const char EqualsSign = '=';
private const char QuestionMark = '?';
private const char Asterisk = '*';
private const string PeriodString = ".";
internal static readonly char[] InvalidParameterNameChars = new char[]
{
Separator,
OpenBrace,
CloseBrace,
QuestionMark,
Asterisk
};
public static RoutePattern Parse(string pattern)
{
if (pattern == null)
{
throw new ArgumentNullException(nameof(pattern));
}
var trimmedPattern = TrimPrefix(pattern);
var context = new Context(trimmedPattern);
var segments = new List<RoutePatternPathSegment>();
while (context.MoveNext())
{
var i = context.Index;
if (context.Current == Separator)
{
// If we get here is means that there's a consecutive '/' character.
// Templates don't start with a '/' and parsing a segment consumes the separator.
throw new RoutePatternException(pattern, Resources.TemplateRoute_CannotHaveConsecutiveSeparators);
}
if (!ParseSegment(context, segments))
{
throw new RoutePatternException(pattern, context.Error);
}
// A successful parse should always result in us being at the end or at a separator.
Debug.Assert(context.AtEnd() || context.Current == Separator);
if (context.Index <= i)
{
// This shouldn't happen, but we want to crash if it does.
var message = "Infinite loop detected in the parser. Please open an issue.";
throw new InvalidProgramException(message);
}
}
if (IsAllValid(context, segments))
{
return RoutePatternFactory.Pattern(pattern, segments);
}
else
{
throw new RoutePatternException(pattern, context.Error);
}
}
private static bool ParseSegment(Context context, List<RoutePatternPathSegment> segments)
{
Debug.Assert(context != null);
Debug.Assert(segments != null);
var parts = new List<RoutePatternPart>();
while (true)
{
var i = context.Index;
if (context.Current == OpenBrace)
{
if (!context.MoveNext())
{
// This is a dangling open-brace, which is not allowed
context.Error = Resources.TemplateRoute_MismatchedParameter;
return false;
}
if (context.Current == OpenBrace)
{
// This is an 'escaped' brace in a literal, like "{{foo"
context.Back();
if (!ParseLiteral(context, parts))
{
return false;
}
}
else
{
// This is a parameter
context.Back();
if (!ParseParameter(context, parts))
{
return false;
}
}
}
else
{
if (!ParseLiteral(context, parts))
{
return false;
}
}
if (context.Current == Separator || context.AtEnd())
{
// We've reached the end of the segment
break;
}
if (context.Index <= i)
{
// This shouldn't happen, but we want to crash if it does.
var message = "Infinite loop detected in the parser. Please open an issue.";
throw new InvalidProgramException(message);
}
}
if (IsSegmentValid(context, parts))
{
segments.Add(new RoutePatternPathSegment(parts.ToArray()));
return true;
}
else
{
return false;
}
}
private static bool ParseParameter(Context context, List<RoutePatternPart> parts)
{
Debug.Assert(context.Current == OpenBrace);
context.Mark();
context.MoveNext();
while (true)
{
if (context.Current == OpenBrace)
{
// This is an open brace inside of a parameter, it has to be escaped
if (context.MoveNext())
{
if (context.Current != OpenBrace)
{
// If we see something like "{p1:regex(^\d{3", we will come here.
context.Error = Resources.TemplateRoute_UnescapedBrace;
return false;
}
}
else
{
// This is a dangling open-brace, which is not allowed
// Example: "{p1:regex(^\d{"
context.Error = Resources.TemplateRoute_MismatchedParameter;
return false;
}
}
else if (context.Current == CloseBrace)
{
// When we encounter Closed brace here, it either means end of the parameter or it is a closed
// brace in the parameter, in that case it needs to be escaped.
// Example: {p1:regex(([}}])\w+}. First pair is escaped one and last marks end of the parameter
if (!context.MoveNext())
{
// This is the end of the string -and we have a valid parameter
break;
}
if (context.Current == CloseBrace)
{
// This is an 'escaped' brace in a parameter name
}
else
{
// This is the end of the parameter
break;
}
}
if (!context.MoveNext())
{
// This is a dangling open-brace, which is not allowed
context.Error = Resources.TemplateRoute_MismatchedParameter;
return false;
}
}
var text = context.Capture();
if (text == "{}")
{
context.Error = Resources.FormatTemplateRoute_InvalidParameterName(string.Empty);
return false;
}
var inside = text.Substring(1, text.Length - 2);
var decoded = inside.Replace("}}", "}").Replace("{{", "{");
// At this point, we need to parse the raw name for inline constraint,
// default values and optional parameters.
var templatePart = RouteParameterParser.ParseRouteParameter(decoded);
// See #475 - this is here because InlineRouteParameterParser can't return errors
if (decoded.StartsWith("*", StringComparison.Ordinal) && decoded.EndsWith("?", StringComparison.Ordinal))
{
context.Error = Resources.TemplateRoute_CatchAllCannotBeOptional;
return false;
}
if (templatePart.IsOptional && templatePart.Default != null)
{
// Cannot be optional and have a default value.
// The only way to declare an optional parameter is to have a ? at the end,
// hence we cannot have both default value and optional parameter within the template.
// A workaround is to add it as a separate entry in the defaults argument.
context.Error = Resources.TemplateRoute_OptionalCannotHaveDefaultValue;
return false;
}
var parameterName = templatePart.Name;
if (IsValidParameterName(context, parameterName))
{
parts.Add(templatePart);
return true;
}
else
{
return false;
}
}
private static bool ParseLiteral(Context context, List<RoutePatternPart> parts)
{
context.Mark();
while (true)
{
if (context.Current == Separator)
{
// End of the segment
break;
}
else if (context.Current == OpenBrace)
{
if (!context.MoveNext())
{
// This is a dangling open-brace, which is not allowed
context.Error = Resources.TemplateRoute_MismatchedParameter;
return false;
}
if (context.Current == OpenBrace)
{
// This is an 'escaped' brace in a literal, like "{{foo" - keep going.
}
else
{
// We've just seen the start of a parameter, so back up.
context.Back();
break;
}
}
else if (context.Current == CloseBrace)
{
if (!context.MoveNext())
{
// This is a dangling close-brace, which is not allowed
context.Error = Resources.TemplateRoute_MismatchedParameter;
return false;
}
if (context.Current == CloseBrace)
{
// This is an 'escaped' brace in a literal, like "{{foo" - keep going.
}
else
{
// This is an unbalanced close-brace, which is not allowed
context.Error = Resources.TemplateRoute_MismatchedParameter;
return false;
}
}
if (!context.MoveNext())
{
break;
}
}
var encoded = context.Capture();
var decoded = encoded.Replace("}}", "}").Replace("{{", "{");
if (IsValidLiteral(context, decoded))
{
parts.Add(RoutePatternFactory.LiteralPart(decoded));
return true;
}
else
{
return false;
}
}
private static bool IsAllValid(Context context, List<RoutePatternPathSegment> segments)
{
// A catch-all parameter must be the last part of the last segment
for (var i = 0; i < segments.Count; i++)
{
var segment = segments[i];
for (var j = 0; j < segment.Parts.Count; j++)
{
var part = segment.Parts[j];
if (part is RoutePatternParameterPart parameter
&& parameter.IsCatchAll &&
(i != segments.Count - 1 || j != segment.Parts.Count - 1))
{
context.Error = Resources.TemplateRoute_CatchAllMustBeLast;
return false;
}
}
}
return true;
}
private static bool IsSegmentValid(Context context, List<RoutePatternPart> parts)
{
// If a segment has multiple parts, then it can't contain a catch all.
for (var i = 0; i < parts.Count; i++)
{
var part = parts[i];
if (part is RoutePatternParameterPart parameter && parameter.IsCatchAll && parts.Count > 1)
{
context.Error = Resources.TemplateRoute_CannotHaveCatchAllInMultiSegment;
return false;
}
}
// if a segment has multiple parts, then only the last one parameter can be optional
// if it is following a optional seperator.
for (var i = 0; i < parts.Count; i++)
{
var part = parts[i];
if (part is RoutePatternParameterPart parameter && parameter.IsOptional && parts.Count > 1)
{
// This optional parameter is the last part in the segment
if (i == parts.Count - 1)
{
var previousPart = parts[i - 1];
if (!previousPart.IsLiteral && !previousPart.IsSeparator)
{
// The optional parameter is preceded by something that is not a literal or separator
// Example of error message:
// "In the segment '{RouteValue}{param?}', the optional parameter 'param' is preceded
// by an invalid segment '{RouteValue}'. Only a period (.) can precede an optional parameter.
context.Error = string.Format(
Resources.TemplateRoute_OptionalParameterCanbBePrecededByPeriod,
RoutePatternPathSegment.DebuggerToString(parts),
parameter.Name,
parts[i - 1].DebuggerToString());
return false;
}
else if (previousPart is RoutePatternLiteralPart literal && literal.Content != PeriodString)
{
// The optional parameter is preceded by a literal other than period.
// Example of error message:
// "In the segment '{RouteValue}-{param?}', the optional parameter 'param' is preceded
// by an invalid segment '-'. Only a period (.) can precede an optional parameter.
context.Error = string.Format(
Resources.TemplateRoute_OptionalParameterCanbBePrecededByPeriod,
RoutePatternPathSegment.DebuggerToString(parts),
parameter.Name,
parts[i - 1].DebuggerToString());
return false;
}
parts[i - 1] = RoutePatternFactory.SeparatorPart(((RoutePatternLiteralPart)previousPart).Content);
}
else
{
// This optional parameter is not the last one in the segment
// Example:
// An optional parameter must be at the end of the segment. In the segment '{RouteValue?})',
// optional parameter 'RouteValue' is followed by ')'
context.Error = string.Format(
Resources.TemplateRoute_OptionalParameterHasTobeTheLast,
RoutePatternPathSegment.DebuggerToString(parts),
parameter.Name,
parts[i + 1].DebuggerToString());
return false;
}
}
}
// A segment cannot contain two consecutive parameters
var isLastSegmentParameter = false;
for (var i = 0; i < parts.Count; i++)
{
var part = parts[i];
if (part.IsParameter && isLastSegmentParameter)
{
context.Error = Resources.TemplateRoute_CannotHaveConsecutiveParameters;
return false;
}
isLastSegmentParameter = part.IsParameter;
}
return true;
}
private static bool IsValidParameterName(Context context, string parameterName)
{
if (parameterName.Length == 0 || parameterName.IndexOfAny(InvalidParameterNameChars) >= 0)
{
context.Error = Resources.FormatTemplateRoute_InvalidParameterName(parameterName);
return false;
}
if (!context.ParameterNames.Add(parameterName))
{
context.Error = Resources.FormatTemplateRoute_RepeatedParameter(parameterName);
return false;
}
return true;
}
private static bool IsValidLiteral(Context context, string literal)
{
Debug.Assert(context != null);
Debug.Assert(literal != null);
if (literal.IndexOf(QuestionMark) != -1)
{
context.Error = Resources.FormatTemplateRoute_InvalidLiteral(literal);
return false;
}
return true;
}
private static string TrimPrefix(string routePattern)
{
if (routePattern.StartsWith("~/", StringComparison.Ordinal))
{
return routePattern.Substring(2);
}
else if (routePattern.StartsWith("/", StringComparison.Ordinal))
{
return routePattern.Substring(1);
}
else if (routePattern.StartsWith("~", StringComparison.Ordinal))
{
throw new RoutePatternException(routePattern, Resources.TemplateRoute_InvalidRouteTemplate);
}
return routePattern;
}
[DebuggerDisplay("{DebuggerToString()}")]
private class Context
{
private readonly string _template;
private int _index;
private int? _mark;
private readonly HashSet<string> _parameterNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
public Context(string template)
{
Debug.Assert(template != null);
_template = template;
_index = -1;
}
public char Current
{
get { return (_index < _template.Length && _index >= 0) ? _template[_index] : (char)0; }
}
public int Index => _index;
public string Error
{
get;
set;
}
public HashSet<string> ParameterNames
{
get { return _parameterNames; }
}
public bool Back()
{
return --_index >= 0;
}
public bool AtEnd()
{
return _index >= _template.Length;
}
public bool MoveNext()
{
return ++_index < _template.Length;
}
public void Mark()
{
Debug.Assert(_index >= 0);
// Index is always the index of the character *past* Current - we want to 'mark' Current.
_mark = _index;
}
public string Capture()
{
if (_mark.HasValue)
{
var value = _template.Substring(_mark.Value, _index - _mark.Value);
_mark = null;
return value;
}
else
{
return null;
}
}
private string DebuggerToString()
{
if (_index == -1)
{
return _template;
}
else if (_mark.HasValue)
{
return _template.Substring(0, _mark.Value) +
"|" +
_template.Substring(_mark.Value, _index - _mark.Value) +
"|" +
_template.Substring(_index);
}
else
{
return _template.Substring(0, _index) + "|" + _template.Substring(_index);
}
}
}
}
}

View File

@ -0,0 +1,27 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.Routing.Patterns
{
public abstract class RoutePatternPart
{
// This class is **not** an extensibility point - every part of the routing system
// needs to be aware of what kind of parts we support.
//
// It is abstract so we can add semantics later inside the library.
private protected RoutePatternPart(RoutePatternPartKind partKind)
{
PartKind = partKind;
}
public RoutePatternPartKind PartKind { get; }
public bool IsLiteral => PartKind == RoutePatternPartKind.Literal;
public bool IsParameter => PartKind == RoutePatternPartKind.Parameter;
public bool IsSeparator => PartKind == RoutePatternPartKind.Separator;
internal abstract string DebuggerToString();
}
}

View File

@ -0,0 +1,12 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.Routing.Patterns
{
public enum RoutePatternPartKind
{
Literal,
Parameter,
Separator,
}
}

View File

@ -0,0 +1,32 @@
// Copyright (c) .NET Foundation. 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.AspNetCore.Routing.Patterns
{
[DebuggerDisplay("{DebuggerToString()}")]
public sealed class RoutePatternPathSegment
{
internal RoutePatternPathSegment(RoutePatternPart[] parts)
{
Parts = parts;
}
public bool IsSimple => Parts.Count == 1;
public IReadOnlyList<RoutePatternPart> Parts { get; }
internal string DebuggerToString()
{
return DebuggerToString(Parts);
}
internal static string DebuggerToString(IReadOnlyList<RoutePatternPart> parts)
{
return string.Join(string.Empty, parts.Select(p => p.DebuggerToString()));
}
}
}

View File

@ -0,0 +1,26 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Diagnostics;
namespace Microsoft.AspNetCore.Routing.Patterns
{
[DebuggerDisplay("{DebuggerToString()}")]
public sealed class RoutePatternSeparatorPart : RoutePatternPart
{
internal RoutePatternSeparatorPart(string content)
: base(RoutePatternPartKind.Separator)
{
Debug.Assert(!string.IsNullOrEmpty(content));
Content = content;
}
public string Content { get; }
internal override string DebuggerToString()
{
return Content;
}
}
}

View File

@ -416,6 +416,48 @@ namespace Microsoft.AspNetCore.Routing
internal static string FormatAmbiguousEndpoints(object p0, object p1)
=> string.Format(CultureInfo.CurrentCulture, GetString("AmbiguousEndpoints"), p0, p1);
/// <summary>
/// Value cannot be null or empty.
/// </summary>
internal static string Argument_NullOrEmpty
{
get => GetString("Argument_NullOrEmpty");
}
/// <summary>
/// Value cannot be null or empty.
/// </summary>
internal static string FormatArgument_NullOrEmpty()
=> GetString("Argument_NullOrEmpty");
/// <summary>
/// The collection cannot be empty.
/// </summary>
internal static string RoutePatternBuilder_CollectionCannotBeEmpty
{
get => GetString("RoutePatternBuilder_CollectionCannotBeEmpty");
}
/// <summary>
/// The collection cannot be empty.
/// </summary>
internal static string FormatRoutePatternBuilder_CollectionCannotBeEmpty()
=> GetString("RoutePatternBuilder_CollectionCannotBeEmpty");
/// <summary>
/// The constraint entry '{0}' - '{1}' must have a string value or be of a type which implements '{2}'.
/// </summary>
internal static string ConstraintMustBeStringOrConstraint
{
get => GetString("ConstraintMustBeStringOrConstraint");
}
/// <summary>
/// The constraint entry '{0}' - '{1}' must have a string value or be of a type which implements '{2}'.
/// </summary>
internal static string FormatConstraintMustBeStringOrConstraint(object p0, object p1, object p2)
=> string.Format(CultureInfo.CurrentCulture, GetString("ConstraintMustBeStringOrConstraint"), p0, p1, p2);
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);

View File

@ -204,4 +204,13 @@
<data name="AmbiguousEndpoints" xml:space="preserve">
<value>The request matched multiple endpoints. Matches: {0}{0}{1}</value>
</data>
<data name="Argument_NullOrEmpty" xml:space="preserve">
<value>Value cannot be null or empty.</value>
</data>
<data name="RoutePatternBuilder_CollectionCannotBeEmpty" xml:space="preserve">
<value>The collection cannot be empty.</value>
</data>
<data name="ConstraintMustBeStringOrConstraint" xml:space="preserve">
<value>The constraint entry '{0}' - '{1}' must have a string value or be of a type which implements '{2}'.</value>
</data>
</root>

View File

@ -161,7 +161,7 @@ namespace Microsoft.AspNetCore.Routing
queryString = queryString.ToLowerInvariant();
}
if (_options.AppendTrailingSlash && !urlWithoutQueryString.EndsWith("/"))
if (_options.AppendTrailingSlash && !urlWithoutQueryString.EndsWith("/", StringComparison.Ordinal))
{
urlWithoutQueryString += "/";
}

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.Routing.Patterns;
namespace Microsoft.AspNetCore.Routing.Template
{
@ -24,6 +25,16 @@ namespace Microsoft.AspNetCore.Routing.Template
Constraint = constraint;
}
public InlineConstraint(RoutePatternConstraintReference other)
{
if (other == null)
{
throw new ArgumentNullException(nameof(other));
}
Constraint = other.Content;
}
/// <summary>
/// Gets the constraint text.
/// </summary>

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Microsoft.AspNetCore.Routing.Patterns;
namespace Microsoft.AspNetCore.Routing.Template
{
@ -13,6 +14,25 @@ namespace Microsoft.AspNetCore.Routing.Template
{
private const string SeparatorString = "/";
public RouteTemplate(RoutePattern other)
{
TemplateText = other.RawText;
Segments = new List<TemplateSegment>(other.PathSegments.Select(p => new TemplateSegment(p)));
Parameters = new List<TemplatePart>();
for (var i = 0; i < Segments.Count; i++)
{
var segment = Segments[i];
for (var j = 0; j < segment.Parts.Count; j++)
{
var part = segment.Parts[j];
if (part.IsParameter)
{
Parameters.Add(part);
}
}
}
}
public RouteTemplate(string template, List<TemplateSegment> segments)
{
if (segments == null)
@ -78,5 +98,16 @@ namespace Microsoft.AspNetCore.Routing.Template
return null;
}
/// <summary>
/// Converts the <see cref="RouteTemplate"/> to the equivalent
/// <see cref="RoutePattern"/>
/// </summary>
/// <returns>A <see cref="RoutePattern"/>.</returns>
public RoutePattern ToRoutePattern()
{
var segments = Segments.Select(s => s.ToRoutePatternPathSegment());
return RoutePatternFactory.Pattern(TemplateText, segments);
}
}
}

View File

@ -2,10 +2,8 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing.Internal;
namespace Microsoft.AspNetCore.Routing.Template
{
@ -19,6 +17,7 @@ namespace Microsoft.AspNetCore.Routing.Template
private readonly object[] _defaultValues;
private static readonly char[] Delimiters = new char[] { SeparatorChar };
private RoutePatternMatcher _routePatternMatcher;
public TemplateMatcher(
RouteTemplate template,
@ -57,6 +56,9 @@ namespace Microsoft.AspNetCore.Routing.Template
_defaultValues[i] = value;
}
}
var routePattern = Template.ToRoutePattern();
_routePatternMatcher = new RoutePatternMatcher(routePattern, Defaults);
}
public RouteValueDictionary Defaults { get; }
@ -70,387 +72,7 @@ namespace Microsoft.AspNetCore.Routing.Template
throw new ArgumentNullException(nameof(values));
}
var i = 0;
var pathTokenizer = new PathTokenizer(path);
// Perf: We do a traversal of the request-segments + route-segments twice.
//
// For most segment-types, we only really need to any work on one of the two passes.
//
// On the first pass, we're just looking to see if there's anything that would disqualify us from matching.
// The most common case would be a literal segment that doesn't match.
//
// On the second pass, we're almost certainly going to match the URL, so go ahead and allocate the 'values'
// and start capturing strings.
foreach (var pathSegment in pathTokenizer)
{
if (pathSegment.Length == 0)
{
return false;
}
var routeSegment = Template.GetSegment(i++);
if (routeSegment == null && pathSegment.Length > 0)
{
// If routeSegment is null, then we're out of route segments. All we can match is the empty
// string.
return false;
}
else if (routeSegment.IsSimple && routeSegment.Parts[0].IsLiteral)
{
// This is a literal segment, so we need to match the text, or the route isn't a match.
var part = routeSegment.Parts[0];
if (!pathSegment.Equals(part.Text, StringComparison.OrdinalIgnoreCase))
{
return false;
}
}
else if (routeSegment.IsSimple && routeSegment.Parts[0].IsCatchAll)
{
// Nothing to validate for a catch-all - it can match any string, including the empty string.
//
// Also, a catch-all has to be the last part, so we're done.
break;
}
else if (routeSegment.IsSimple && routeSegment.Parts[0].IsParameter)
{
// For a parameter, validate that it's a has some length, or we have a default, or it's optional.
var part = routeSegment.Parts[0];
if (pathSegment.Length == 0 &&
!_hasDefaultValue[i] &&
!part.IsOptional)
{
// There's no value for this parameter, the route can't match.
return false;
}
}
else
{
Debug.Assert(!routeSegment.IsSimple);
// Don't attempt to validate a complex segment at this point other than being non-emtpy,
// do it in the second pass.
}
}
for (; i < Template.Segments.Count; i++)
{
// We've matched the request path so far, but still have remaining route segments. These need
// to be all single-part parameter segments with default values or else they won't match.
var routeSegment = Template.GetSegment(i);
Debug.Assert(routeSegment != null);
if (!routeSegment.IsSimple)
{
// If the segment is a complex segment, it MUST contain literals, and we've parsed the full
// path so far, so it can't match.
return false;
}
var part = routeSegment.Parts[0];
if (part.IsLiteral)
{
// If the segment is a simple literal - which need the URL to provide a value, so we don't match.
return false;
}
if (part.IsCatchAll)
{
// Nothing to validate for a catch-all - it can match any string, including the empty string.
//
// Also, a catch-all has to be the last part, so we're done.
break;
}
// If we get here, this is a simple segment with a parameter. We need it to be optional, or for the
// defaults to have a value.
Debug.Assert(routeSegment.IsSimple && part.IsParameter);
if (!_hasDefaultValue[i] && !part.IsOptional)
{
// There's no default for this (non-optional) parameter so it can't match.
return false;
}
}
// At this point we've very likely got a match, so start capturing values for real.
i = 0;
foreach (var requestSegment in pathTokenizer)
{
var routeSegment = Template.GetSegment(i++);
if (routeSegment.IsSimple && routeSegment.Parts[0].IsCatchAll)
{
// A catch-all captures til the end of the string.
var part = routeSegment.Parts[0];
var captured = requestSegment.Buffer.Substring(requestSegment.Offset);
if (captured.Length > 0)
{
values[part.Name] = captured;
}
else
{
// It's ok for a catch-all to produce a null value, so we don't check _hasDefaultValue.
values[part.Name] = _defaultValues[i];
}
// A catch-all has to be the last part, so we're done.
break;
}
else if (routeSegment.IsSimple && routeSegment.Parts[0].IsParameter)
{
// A simple parameter captures the whole segment, or a default value if nothing was
// provided.
var part = routeSegment.Parts[0];
if (requestSegment.Length > 0)
{
values[part.Name] = requestSegment.ToString();
}
else
{
if (_hasDefaultValue[i])
{
values[part.Name] = _defaultValues[i];
}
}
}
else if (!routeSegment.IsSimple)
{
if (!MatchComplexSegment(routeSegment, requestSegment.ToString(), Defaults, values))
{
return false;
}
}
}
for (; i < Template.Segments.Count; i++)
{
// We've matched the request path so far, but still have remaining route segments. We already know these
// are simple parameters that either have a default, or don't need to produce a value.
var routeSegment = Template.GetSegment(i);
Debug.Assert(routeSegment != null);
Debug.Assert(routeSegment.IsSimple);
var part = routeSegment.Parts[0];
Debug.Assert(part.IsParameter);
// It's ok for a catch-all to produce a null value
if (_hasDefaultValue[i] || part.IsCatchAll)
{
// Don't replace an existing value with a null.
var defaultValue = _defaultValues[i];
if (defaultValue != null || !values.ContainsKey(part.Name))
{
values[part.Name] = defaultValue;
}
}
}
// Copy all remaining default values to the route data
foreach (var kvp in Defaults)
{
if (!values.ContainsKey(kvp.Key))
{
values.Add(kvp.Key, kvp.Value);
}
}
return true;
}
private bool MatchComplexSegment(
TemplateSegment routeSegment,
string requestSegment,
IReadOnlyDictionary<string, object> defaults,
RouteValueDictionary values)
{
var indexOfLastSegment = routeSegment.Parts.Count - 1;
// We match the request to the template starting at the rightmost parameter
// If the last segment of template is optional, then request can match the
// template with or without the last parameter. So we start with regular matching,
// but if it doesn't match, we start with next to last parameter. Example:
// Template: {p1}/{p2}.{p3?}. If the request is one/two.three it will match right away
// giving p3 value of three. But if the request is one/two, we start matching from the
// rightmost giving p3 the value of two, then we end up not matching the segment.
// In this case we start again from p2 to match the request and we succeed giving
// the value two to p2
if (routeSegment.Parts[indexOfLastSegment].IsOptional &&
routeSegment.Parts[indexOfLastSegment - 1].IsOptionalSeperator)
{
if (MatchComplexSegmentCore(routeSegment, requestSegment, Defaults, values, indexOfLastSegment))
{
return true;
}
else
{
if (requestSegment.EndsWith(
routeSegment.Parts[indexOfLastSegment - 1].Text,
StringComparison.OrdinalIgnoreCase))
{
return false;
}
return MatchComplexSegmentCore(
routeSegment,
requestSegment,
Defaults,
values,
indexOfLastSegment - 2);
}
}
else
{
return MatchComplexSegmentCore(routeSegment, requestSegment, Defaults, values, indexOfLastSegment);
}
}
private bool MatchComplexSegmentCore(
TemplateSegment routeSegment,
string requestSegment,
IReadOnlyDictionary<string, object> defaults,
RouteValueDictionary values,
int indexOfLastSegmentUsed)
{
Debug.Assert(routeSegment != null);
Debug.Assert(routeSegment.Parts.Count > 1);
// Find last literal segment and get its last index in the string
var lastIndex = requestSegment.Length;
TemplatePart parameterNeedsValue = null; // Keeps track of a parameter segment that is pending a value
TemplatePart lastLiteral = null; // Keeps track of the left-most literal we've encountered
var outValues = new RouteValueDictionary();
while (indexOfLastSegmentUsed >= 0)
{
var newLastIndex = lastIndex;
var part = routeSegment.Parts[indexOfLastSegmentUsed];
if (part.IsParameter)
{
// Hold on to the parameter so that we can fill it in when we locate the next literal
parameterNeedsValue = part;
}
else
{
Debug.Assert(part.IsLiteral);
lastLiteral = part;
var startIndex = lastIndex - 1;
// If we have a pending parameter subsegment, we must leave at least one character for that
if (parameterNeedsValue != null)
{
startIndex--;
}
if (startIndex < 0)
{
return false;
}
var indexOfLiteral = requestSegment.LastIndexOf(
part.Text,
startIndex,
StringComparison.OrdinalIgnoreCase);
if (indexOfLiteral == -1)
{
// If we couldn't find this literal index, this segment cannot match
return false;
}
// If the first subsegment is a literal, it must match at the right-most extent of the request URI.
// Without this check if your route had "/Foo/" we'd match the request URI "/somethingFoo/".
// This check is related to the check we do at the very end of this function.
if (indexOfLastSegmentUsed == (routeSegment.Parts.Count - 1))
{
if ((indexOfLiteral + part.Text.Length) != requestSegment.Length)
{
return false;
}
}
newLastIndex = indexOfLiteral;
}
if ((parameterNeedsValue != null) &&
(((lastLiteral != null) && (part.IsLiteral)) || (indexOfLastSegmentUsed == 0)))
{
// If we have a pending parameter that needs a value, grab that value
int parameterStartIndex;
int parameterTextLength;
if (lastLiteral == null)
{
if (indexOfLastSegmentUsed == 0)
{
parameterStartIndex = 0;
}
else
{
parameterStartIndex = newLastIndex;
Debug.Assert(false, "indexOfLastSegementUsed should always be 0 from the check above");
}
parameterTextLength = lastIndex;
}
else
{
// If we're getting a value for a parameter that is somewhere in the middle of the segment
if ((indexOfLastSegmentUsed == 0) && (part.IsParameter))
{
parameterStartIndex = 0;
parameterTextLength = lastIndex;
}
else
{
parameterStartIndex = newLastIndex + lastLiteral.Text.Length;
parameterTextLength = lastIndex - parameterStartIndex;
}
}
var parameterValueString = requestSegment.Substring(parameterStartIndex, parameterTextLength);
if (string.IsNullOrEmpty(parameterValueString))
{
// If we're here that means we have a segment that contains multiple sub-segments.
// For these segments all parameters must have non-empty values. If the parameter
// has an empty value it's not a match.
return false;
}
else
{
// If there's a value in the segment for this parameter, use the subsegment value
outValues.Add(parameterNeedsValue.Name, parameterValueString);
}
parameterNeedsValue = null;
lastLiteral = null;
}
lastIndex = newLastIndex;
indexOfLastSegmentUsed--;
}
// If the last subsegment is a parameter, it's OK that we didn't parse all the way to the left extent of
// the string since the parameter will have consumed all the remaining text anyway. If the last subsegment
// is a literal then we *must* have consumed the entire text in that literal. Otherwise we end up matching
// the route "Foo" to the request URI "somethingFoo". Thus we have to check that we parsed the *entire*
// request URI in order for it to be a match.
// This check is related to the check we do earlier in this function for LiteralSubsegments.
if (lastIndex == 0 || routeSegment.Parts[0].IsParameter)
{
foreach (var item in outValues)
{
values.Add(item.Key, item.Value);
}
return true;
}
return false;
return _routePatternMatcher.TryMatch(path, values);
}
}
}

View File

@ -1,539 +1,29 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using Microsoft.AspNetCore.Routing.Patterns;
namespace Microsoft.AspNetCore.Routing.Template
{
public static class TemplateParser
{
private const char Separator = '/';
private const char OpenBrace = '{';
private const char CloseBrace = '}';
private const char EqualsSign = '=';
private const char QuestionMark = '?';
private const char Asterisk = '*';
private const string PeriodString = ".";
public static RouteTemplate Parse(string routeTemplate)
{
if (routeTemplate == null)
{
routeTemplate = String.Empty;
throw new ArgumentNullException(routeTemplate);
}
var trimmedRouteTemplate = TrimPrefix(routeTemplate);
var context = new TemplateParserContext(trimmedRouteTemplate);
var segments = new List<TemplateSegment>();
while (context.Next())
try
{
if (context.Current == Separator)
{
// If we get here is means that there's a consecutive '/' character.
// Templates don't start with a '/' and parsing a segment consumes the separator.
throw new ArgumentException(Resources.TemplateRoute_CannotHaveConsecutiveSeparators,
nameof(routeTemplate));
}
else
{
if (!ParseSegment(context, segments))
{
throw new ArgumentException(context.Error, nameof(routeTemplate));
}
}
var inner = RoutePatternFactory.Parse(routeTemplate);
return new RouteTemplate(inner);
}
if (IsAllValid(context, segments))
catch (RoutePatternException ex)
{
return new RouteTemplate(routeTemplate, segments);
}
else
{
throw new ArgumentException(context.Error, nameof(routeTemplate));
}
}
private static string TrimPrefix(string routeTemplate)
{
if (routeTemplate.StartsWith("~/", StringComparison.Ordinal))
{
return routeTemplate.Substring(2);
}
else if (routeTemplate.StartsWith("/", StringComparison.Ordinal))
{
return routeTemplate.Substring(1);
}
else if (routeTemplate.StartsWith("~", StringComparison.Ordinal))
{
throw new ArgumentException(Resources.TemplateRoute_InvalidRouteTemplate, nameof(routeTemplate));
}
return routeTemplate;
}
private static bool ParseSegment(TemplateParserContext context, List<TemplateSegment> segments)
{
Debug.Assert(context != null);
Debug.Assert(segments != null);
var segment = new TemplateSegment();
while (true)
{
if (context.Current == OpenBrace)
{
if (!context.Next())
{
// This is a dangling open-brace, which is not allowed
context.Error = Resources.TemplateRoute_MismatchedParameter;
return false;
}
if (context.Current == OpenBrace)
{
// This is an 'escaped' brace in a literal, like "{{foo"
context.Back();
if (!ParseLiteral(context, segment))
{
return false;
}
}
else
{
// This is the inside of a parameter
if (!ParseParameter(context, segment))
{
return false;
}
}
}
else if (context.Current == Separator)
{
// We've reached the end of the segment
break;
}
else
{
if (!ParseLiteral(context, segment))
{
return false;
}
}
if (!context.Next())
{
// We've reached the end of the string
break;
}
}
if (IsSegmentValid(context, segment))
{
segments.Add(segment);
return true;
}
else
{
return false;
}
}
private static bool ParseParameter(TemplateParserContext context, TemplateSegment segment)
{
context.Mark();
while (true)
{
if (context.Current == OpenBrace)
{
// This is an open brace inside of a parameter, it has to be escaped
if (context.Next())
{
if (context.Current != OpenBrace)
{
// If we see something like "{p1:regex(^\d{3", we will come here.
context.Error = Resources.TemplateRoute_UnescapedBrace;
return false;
}
}
else
{
// This is a dangling open-brace, which is not allowed
// Example: "{p1:regex(^\d{"
context.Error = Resources.TemplateRoute_MismatchedParameter;
return false;
}
}
else if (context.Current == CloseBrace)
{
// When we encounter Closed brace here, it either means end of the parameter or it is a closed
// brace in the parameter, in that case it needs to be escaped.
// Example: {p1:regex(([}}])\w+}. First pair is escaped one and last marks end of the parameter
if (!context.Next())
{
// This is the end of the string -and we have a valid parameter
context.Back();
break;
}
if (context.Current == CloseBrace)
{
// This is an 'escaped' brace in a parameter name
}
else
{
// This is the end of the parameter
context.Back();
break;
}
}
if (!context.Next())
{
// This is a dangling open-brace, which is not allowed
context.Error = Resources.TemplateRoute_MismatchedParameter;
return false;
}
}
var rawParameter = context.Capture();
var decoded = rawParameter.Replace("}}", "}").Replace("{{", "{");
// At this point, we need to parse the raw name for inline constraint,
// default values and optional parameters.
var templatePart = InlineRouteParameterParser.ParseRouteParameter(decoded);
if (templatePart.IsCatchAll && templatePart.IsOptional)
{
context.Error = Resources.TemplateRoute_CatchAllCannotBeOptional;
return false;
}
if (templatePart.IsOptional && templatePart.DefaultValue != null)
{
// Cannot be optional and have a default value.
// The only way to declare an optional parameter is to have a ? at the end,
// hence we cannot have both default value and optional parameter within the template.
// A workaround is to add it as a separate entry in the defaults argument.
context.Error = Resources.TemplateRoute_OptionalCannotHaveDefaultValue;
return false;
}
var parameterName = templatePart.Name;
if (IsValidParameterName(context, parameterName))
{
segment.Parts.Add(templatePart);
return true;
}
else
{
return false;
}
}
private static bool ParseLiteral(TemplateParserContext context, TemplateSegment segment)
{
context.Mark();
string encoded;
while (true)
{
if (context.Current == Separator)
{
encoded = context.Capture();
context.Back();
break;
}
else if (context.Current == OpenBrace)
{
if (!context.Next())
{
// This is a dangling open-brace, which is not allowed
context.Error = Resources.TemplateRoute_MismatchedParameter;
return false;
}
if (context.Current == OpenBrace)
{
// This is an 'escaped' brace in a literal, like "{{foo" - keep going.
}
else
{
// We've just seen the start of a parameter, so back up and return
context.Back();
encoded = context.Capture();
context.Back();
break;
}
}
else if (context.Current == CloseBrace)
{
if (!context.Next())
{
// This is a dangling close-brace, which is not allowed
context.Error = Resources.TemplateRoute_MismatchedParameter;
return false;
}
if (context.Current == CloseBrace)
{
// This is an 'escaped' brace in a literal, like "{{foo" - keep going.
}
else
{
// This is an unbalanced close-brace, which is not allowed
context.Error = Resources.TemplateRoute_MismatchedParameter;
return false;
}
}
if (!context.Next())
{
encoded = context.Capture();
break;
}
}
var decoded = encoded.Replace("}}", "}").Replace("{{", "{");
if (IsValidLiteral(context, decoded))
{
segment.Parts.Add(TemplatePart.CreateLiteral(decoded));
return true;
}
else
{
return false;
}
}
private static bool IsAllValid(TemplateParserContext context, List<TemplateSegment> segments)
{
// A catch-all parameter must be the last part of the last segment
for (var i = 0; i < segments.Count; i++)
{
var segment = segments[i];
for (var j = 0; j < segment.Parts.Count; j++)
{
var part = segment.Parts[j];
if (part.IsParameter &&
part.IsCatchAll &&
(i != segments.Count - 1 || j != segment.Parts.Count - 1))
{
context.Error = Resources.TemplateRoute_CatchAllMustBeLast;
return false;
}
}
}
return true;
}
private static bool IsSegmentValid(TemplateParserContext context, TemplateSegment segment)
{
// If a segment has multiple parts, then it can't contain a catch all.
for (var i = 0; i < segment.Parts.Count; i++)
{
var part = segment.Parts[i];
if (part.IsParameter && part.IsCatchAll && segment.Parts.Count > 1)
{
context.Error = Resources.TemplateRoute_CannotHaveCatchAllInMultiSegment;
return false;
}
}
// if a segment has multiple parts, then only the last one parameter can be optional
// if it is following a optional seperator.
for (var i = 0; i < segment.Parts.Count; i++)
{
var part = segment.Parts[i];
if (part.IsParameter && part.IsOptional && segment.Parts.Count > 1)
{
// This optional parameter is the last part in the segment
if (i == segment.Parts.Count - 1)
{
if (!segment.Parts[i - 1].IsLiteral)
{
// The optional parameter is preceded by something that is not a literal.
// Example of error message:
// "In the segment '{RouteValue}{param?}', the optional parameter 'param' is preceded
// by an invalid segment '{RouteValue}'. Only a period (.) can precede an optional parameter.
context.Error = string.Format(
Resources.TemplateRoute_OptionalParameterCanbBePrecededByPeriod,
segment.DebuggerToString(),
part.Name,
segment.Parts[i - 1].DebuggerToString());
return false;
}
else if (segment.Parts[i - 1].Text != PeriodString)
{
// The optional parameter is preceded by a literal other than period.
// Example of error message:
// "In the segment '{RouteValue}-{param?}', the optional parameter 'param' is preceded
// by an invalid segment '-'. Only a period (.) can precede an optional parameter.
context.Error = string.Format(
Resources.TemplateRoute_OptionalParameterCanbBePrecededByPeriod,
segment.DebuggerToString(),
part.Name,
segment.Parts[i - 1].Text);
return false;
}
segment.Parts[i - 1].IsOptionalSeperator = true;
}
else
{
// This optional parameter is not the last one in the segment
// Example:
// An optional parameter must be at the end of the segment.In the segment '{RouteValue?})',
// optional parameter 'RouteValue' is followed by ')'
var nextPart = segment.Parts[i + 1];
var invalidPartText = nextPart.IsParameter ? nextPart.Name : nextPart.Text;
context.Error = string.Format(
Resources.TemplateRoute_OptionalParameterHasTobeTheLast,
segment.DebuggerToString(),
segment.Parts[i].Name,
invalidPartText);
return false;
}
}
}
// A segment cannot contain two consecutive parameters
var isLastSegmentParameter = false;
for (var i = 0; i < segment.Parts.Count; i++)
{
var part = segment.Parts[i];
if (part.IsParameter && isLastSegmentParameter)
{
context.Error = Resources.TemplateRoute_CannotHaveConsecutiveParameters;
return false;
}
isLastSegmentParameter = part.IsParameter;
}
return true;
}
private static bool IsValidParameterName(TemplateParserContext context, string parameterName)
{
if (parameterName.Length == 0)
{
context.Error = String.Format(CultureInfo.CurrentCulture,
Resources.TemplateRoute_InvalidParameterName, parameterName);
return false;
}
for (var i = 0; i < parameterName.Length; i++)
{
var c = parameterName[i];
if (c == Separator || c == OpenBrace || c == CloseBrace || c == QuestionMark || c == Asterisk)
{
context.Error = String.Format(CultureInfo.CurrentCulture,
Resources.TemplateRoute_InvalidParameterName, parameterName);
return false;
}
}
if (!context.ParameterNames.Add(parameterName))
{
context.Error = String.Format(CultureInfo.CurrentCulture,
Resources.TemplateRoute_RepeatedParameter, parameterName);
return false;
}
return true;
}
private static bool IsValidLiteral(TemplateParserContext context, string literal)
{
Debug.Assert(context != null);
Debug.Assert(literal != null);
if (literal.IndexOf(QuestionMark) != -1)
{
context.Error = String.Format(CultureInfo.CurrentCulture,
Resources.TemplateRoute_InvalidLiteral, literal);
return false;
}
return true;
}
private static bool IsInvalidRouteTemplate(string routeTemplate)
{
return routeTemplate.StartsWith("~", StringComparison.Ordinal) ||
routeTemplate.StartsWith("/", StringComparison.Ordinal);
}
private class TemplateParserContext
{
private readonly string _template;
private int _index;
private int? _mark;
private HashSet<string> _parameterNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
public TemplateParserContext(string template)
{
Debug.Assert(template != null);
_template = template;
_index = -1;
}
public char Current
{
get { return (_index < _template.Length && _index >= 0) ? _template[_index] : (char)0; }
}
public string Error
{
get;
set;
}
public HashSet<string> ParameterNames
{
get { return _parameterNames; }
}
public bool Back()
{
return --_index >= 0;
}
public bool Next()
{
return ++_index < _template.Length;
}
public void Mark()
{
_mark = _index;
}
public string Capture()
{
if (_mark.HasValue)
{
var value = _template.Substring(_mark.Value, _index - _mark.Value);
_mark = null;
return value;
}
else
{
return null;
}
// Preserving the existing behavior of this API even though the logic moved.
throw new ArgumentException(ex.Message, nameof(routeTemplate), ex);
}
}
}

View File

@ -5,12 +5,47 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Microsoft.AspNetCore.Routing.Patterns;
namespace Microsoft.AspNetCore.Routing.Template
{
[DebuggerDisplay("{DebuggerToString()}")]
public class TemplatePart
{
public TemplatePart()
{
}
public TemplatePart(RoutePatternPart other)
{
IsLiteral = other.IsLiteral || other.IsSeparator;
IsParameter = other.IsParameter;
if (other.IsLiteral && other is RoutePatternLiteralPart literal)
{
Text = literal.Content;
}
else if (other.IsParameter && other is RoutePatternParameterPart parameter)
{
// Text is unused by TemplatePart and assumed to be null when the part is a parameter.
Name = parameter.Name;
IsCatchAll = parameter.IsCatchAll;
IsOptional = parameter.IsOptional;
DefaultValue = parameter.Default;
InlineConstraints = parameter.Constraints?.Select(p => new InlineConstraint(p));
}
else if (other.IsSeparator && other is RoutePatternSeparatorPart separator)
{
Text = separator.Content;
IsOptionalSeperator = true;
}
else
{
// Unreachable
throw new NotSupportedException();
}
}
public static TemplatePart CreateLiteral(string text)
{
return new TemplatePart()
@ -20,11 +55,12 @@ namespace Microsoft.AspNetCore.Routing.Template
};
}
public static TemplatePart CreateParameter(string name,
bool isCatchAll,
bool isOptional,
object defaultValue,
IEnumerable<InlineConstraint> inlineConstraints)
public static TemplatePart CreateParameter(
string name,
bool isCatchAll,
bool isOptional,
object defaultValue,
IEnumerable<InlineConstraint> inlineConstraints)
{
if (name == null)
{
@ -63,5 +99,28 @@ namespace Microsoft.AspNetCore.Routing.Template
return Text;
}
}
public RoutePatternPart ToRoutePatternPart()
{
if (IsLiteral && IsOptionalSeperator)
{
return RoutePatternFactory.SeparatorPart(Text);
}
else if (IsLiteral)
{
return RoutePatternFactory.LiteralPart(Text);
}
else
{
var kind = IsCatchAll ?
RoutePatternParameterKind.CatchAll :
IsOptional ?
RoutePatternParameterKind.Optional :
RoutePatternParameterKind.Standard;
var constraints = InlineConstraints.Select(c => new RoutePatternConstraintReference(Name, c.Constraint));
return RoutePatternFactory.ParameterPart(Name, DefaultValue, kind, constraints);
}
}
}
}

View File

@ -4,19 +4,36 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Microsoft.AspNetCore.Routing.Patterns;
namespace Microsoft.AspNetCore.Routing.Template
{
[DebuggerDisplay("{DebuggerToString()}")]
public class TemplateSegment
{
public TemplateSegment()
{
Parts = new List<TemplatePart>();
}
public TemplateSegment(RoutePatternPathSegment other)
{
Parts = new List<TemplatePart>(other.Parts.Select(s => new TemplatePart(s)));
}
public bool IsSimple => Parts.Count == 1;
public List<TemplatePart> Parts { get; } = new List<TemplatePart>();
public List<TemplatePart> Parts { get; }
internal string DebuggerToString()
{
return string.Join(string.Empty, Parts.Select(p => p.DebuggerToString()));
}
public RoutePatternPathSegment ToRoutePatternPathSegment()
{
var parts = Parts.Select(p => p.ToRoutePatternPart());
return RoutePatternFactory.Segment(parts);
}
}
}

View File

@ -0,0 +1,946 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Linq;
using Xunit;
namespace Microsoft.AspNetCore.Routing.Patterns
{
public class InlineRouteParameterParserTest
{
[Theory]
[InlineData("=")]
[InlineData(":")]
public void ParseRouteParameter_WithoutADefaultValue(string parameterName)
{
// Arrange & Act
var templatePart = ParseParameter(parameterName);
// Assert
Assert.Equal(parameterName, templatePart.Name);
Assert.Null(templatePart.Default);
Assert.Empty(templatePart.Constraints);
}
[Fact]
public void ParseRouteParameter_WithEmptyDefaultValue()
{
// Arrange & Act
var templatePart = ParseParameter("param=");
// Assert
Assert.Equal("param", templatePart.Name);
Assert.Equal("", templatePart.Default);
Assert.Empty(templatePart.Constraints);
}
[Fact]
public void ParseRouteParameter_WithoutAConstraintName()
{
// Arrange & Act
var templatePart = ParseParameter("param:");
// Assert
Assert.Equal("param", templatePart.Name);
Assert.Null(templatePart.Default);
Assert.Empty(templatePart.Constraints);
}
[Fact]
public void ParseRouteParameter_WithoutAConstraintNameOrParameterName()
{
// Arrange & Act
var templatePart = ParseParameter("param:=");
// Assert
Assert.Equal("param", templatePart.Name);
Assert.Equal("", templatePart.Default);
Assert.Empty(templatePart.Constraints);
}
[Fact]
public void ParseRouteParameter_WithADefaultValueContainingConstraintSeparator()
{
// Arrange & Act
var templatePart = ParseParameter("param=:");
// Assert
Assert.Equal("param", templatePart.Name);
Assert.Equal(":", templatePart.Default);
Assert.Empty(templatePart.Constraints);
}
[Fact]
public void ParseRouteParameter_ConstraintAndDefault_ParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter("param:int=111111");
// Assert
Assert.Equal("param", templatePart.Name);
Assert.Equal("111111", templatePart.Default);
var constraint = Assert.Single(templatePart.Constraints);
Assert.Equal("int", constraint.Content);
}
[Fact]
public void ParseRouteParameter_ConstraintWithArgumentsAndDefault_ParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@"param:test(\d+)=111111");
// Assert
Assert.Equal("param", templatePart.Name);
Assert.Equal("111111", templatePart.Default);
var constraint = Assert.Single(templatePart.Constraints);
Assert.Equal(@"test(\d+)", constraint.Content);
}
[Fact]
public void ParseRouteParameter_ConstraintAndOptional_ParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@"param:int?");
// Assert
Assert.Equal("param", templatePart.Name);
Assert.True(templatePart.IsOptional);
var constraint = Assert.Single(templatePart.Constraints);
Assert.Equal("int", constraint.Content);
}
[Fact]
public void ParseRouteParameter_ConstraintAndOptional_WithDefaultValue_ParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@"param:int=12?");
// Assert
Assert.Equal("param", templatePart.Name);
Assert.Equal("12", templatePart.Default);
Assert.True(templatePart.IsOptional);
var constraint = Assert.Single(templatePart.Constraints);
Assert.Equal("int", constraint.Content);
}
[Fact]
public void ParseRouteParameter_ConstraintAndOptional_WithDefaultValueWithQuestionMark_ParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@"param:int=12??");
// Assert
Assert.Equal("param", templatePart.Name);
Assert.Equal("12?", templatePart.Default);
Assert.True(templatePart.IsOptional);
var constraint = Assert.Single(templatePart.Constraints);
Assert.Equal("int", constraint.Content);
}
[Fact]
public void ParseRouteParameter_ConstraintWithArgumentsAndOptional_ParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@"param:test(\d+)?");
// Assert
Assert.Equal("param", templatePart.Name);
Assert.True(templatePart.IsOptional);
var constraint = Assert.Single(templatePart.Constraints);
Assert.Equal(@"test(\d+)", constraint.Content);
}
[Fact]
public void ParseRouteParameter_ConstraintWithArgumentsAndOptional_WithDefaultValue_ParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@"param:test(\d+)=abc?");
// Assert
Assert.Equal("param", templatePart.Name);
Assert.True(templatePart.IsOptional);
Assert.Equal("abc", templatePart.Default);
var constraint = Assert.Single(templatePart.Constraints);
Assert.Equal(@"test(\d+)", constraint.Content);
}
[Fact]
public void ParseRouteParameter_ChainedConstraints_ParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@"param:test(d+):test(w+)");
// Assert
Assert.Equal("param", templatePart.Name);
Assert.Collection(templatePart.Constraints,
constraint => Assert.Equal(@"test(d+)", constraint.Content),
constraint => Assert.Equal(@"test(w+)", constraint.Content));
}
[Fact]
public void ParseRouteParameter_ChainedConstraints_DoubleDelimiters_ParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@"param::test(d+)::test(w+)");
// Assert
Assert.Equal("param", templatePart.Name);
Assert.Collection(
templatePart.Constraints,
constraint => Assert.Equal(@"test(d+)", constraint.Content),
constraint => Assert.Equal(@"test(w+)", constraint.Content));
}
[Fact]
public void ParseRouteParameter_ChainedConstraints_ColonInPattern_ParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@"param:test(\d+):test(\w:+)");
// Assert
Assert.Equal("param", templatePart.Name);
Assert.Collection(templatePart.Constraints,
constraint => Assert.Equal(@"test(\d+)", constraint.Content),
constraint => Assert.Equal(@"test(\w:+)", constraint.Content));
}
[Fact]
public void ParseRouteParameter_ChainedConstraints_WithDefaultValue_ParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@"param:test(\d+):test(\w+)=qwer");
// Assert
Assert.Equal("param", templatePart.Name);
Assert.Equal("qwer", templatePart.Default);
Assert.Collection(templatePart.Constraints,
constraint => Assert.Equal(@"test(\d+)", constraint.Content),
constraint => Assert.Equal(@"test(\w+)", constraint.Content));
}
[Fact]
public void ParseRouteParameter_ChainedConstraints_WithDefaultValue_DoubleDelimiters_ParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@"param:test(\d+)::test(\w+)==qwer");
// Assert
Assert.Equal("param", templatePart.Name);
Assert.Equal("=qwer", templatePart.Default);
Assert.Collection(
templatePart.Constraints,
constraint => Assert.Equal(@"test(\d+)", constraint.Content),
constraint => Assert.Equal(@"test(\w+)", constraint.Content));
}
[Theory]
[InlineData("=")]
[InlineData("+=")]
[InlineData(">= || <= || ==")]
public void ParseRouteParameter_WithDefaultValue_ContainingDelimiter(string defaultValue)
{
// Arrange & Act
var templatePart = ParseParameter($"comparison-operator:length(6)={defaultValue}");
// Assert
Assert.Equal("comparison-operator", templatePart.Name);
Assert.Equal(defaultValue, templatePart.Default);
var constraint = Assert.Single(templatePart.Constraints);
Assert.Equal("length(6)", constraint.Content);
}
[Fact]
public void ParseRouteTemplate_ConstraintsDefaultsAndOptionalsInMultipleSections_ParsedCorrectly()
{
// Arrange & Act
var routePattern = RoutePatternFactory.Parse(@"some/url-{p1:int:test(3)=hello}/{p2=abc}/{p3?}");
// Assert
var parameters = routePattern.Parameters.ToArray();
var param1 = parameters[0];
Assert.Equal("p1", param1.Name);
Assert.Equal("hello", param1.Default);
Assert.False(param1.IsOptional);
Assert.Collection(param1.Constraints,
constraint => Assert.Equal("int", constraint.Content),
constraint => Assert.Equal("test(3)", constraint.Content)
);
var param2 = parameters[1];
Assert.Equal("p2", param2.Name);
Assert.Equal("abc", param2.Default);
Assert.False(param2.IsOptional);
var param3 = parameters[2];
Assert.Equal("p3", param3.Name);
Assert.True(param3.IsOptional);
}
[Fact]
public void ParseRouteParameter_NoTokens_ParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter("world");
// Assert
Assert.Equal("world", templatePart.Name);
}
[Fact]
public void ParseRouteParameter_ParamDefault_ParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter("param=world");
// Assert
Assert.Equal("param", templatePart.Name);
Assert.Equal("world", templatePart.Default);
}
[Fact]
public void ParseRouteParameter_ConstraintWithClosingBraceInPattern_ClosingBraceIsParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@"param:test(\})");
// Assert
Assert.Equal("param", templatePart.Name);
var constraint = Assert.Single(templatePart.Constraints);
Assert.Equal(@"test(\})", constraint.Content);
}
[Fact]
public void ParseRouteParameter_ConstraintWithClosingBraceInPattern_WithDefaultValue_ParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@"param:test(\})=wer");
// Assert
Assert.Equal("param", templatePart.Name);
Assert.Equal("wer", templatePart.Default);
var constraint = Assert.Single(templatePart.Constraints);
Assert.Equal(@"test(\})", constraint.Content);
}
[Fact]
public void ParseRouteParameter_ConstraintWithClosingParenInPattern_ClosingParenIsParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@"param:test(\))");
// Assert
Assert.Equal("param", templatePart.Name);
var constraint = Assert.Single(templatePart.Constraints);
Assert.Equal(@"test(\))", constraint.Content);
}
[Fact]
public void ParseRouteParameter_ConstraintWithClosingParenInPattern_WithDefaultValue_ParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@"param:test(\))=fsd");
// Assert
Assert.Equal("param", templatePart.Name);
Assert.Equal("fsd", templatePart.Default);
var constraint = Assert.Single(templatePart.Constraints);
Assert.Equal(@"test(\))", constraint.Content);
}
[Fact]
public void ParseRouteParameter_ConstraintWithColonInPattern_ColonIsParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@"param:test(:)");
// Assert
Assert.Equal("param", templatePart.Name);
var constraint = Assert.Single(templatePart.Constraints);
Assert.Equal(@"test(:)", constraint.Content);
}
[Fact]
public void ParseRouteParameter_ConstraintWithColonInPattern_WithDefaultValue_ParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@"param:test(:)=mnf");
// Assert
Assert.Equal("param", templatePart.Name);
Assert.Equal("mnf", templatePart.Default);
var constraint = Assert.Single(templatePart.Constraints);
Assert.Equal(@"test(:)", constraint.Content);
}
[Fact]
public void ParseRouteParameter_ConstraintWithColonsInPattern_ParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@"param:test(a:b:c)");
// Assert
Assert.Equal("param", templatePart.Name);
var constraint = Assert.Single(templatePart.Constraints);
Assert.Equal(@"test(a:b:c)", constraint.Content);
}
[Fact]
public void ParseRouteParameter_ConstraintWithColonInParamName_ParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@":param:test=12");
// Assert
Assert.Equal(":param", templatePart.Name);
Assert.Equal("12", templatePart.Default);
var constraint = Assert.Single(templatePart.Constraints);
Assert.Equal("test", constraint.Content);
}
[Fact]
public void ParseRouteParameter_ConstraintWithTwoColonInParamName_ParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@":param::test=12");
// Assert
Assert.Equal(":param", templatePart.Name);
Assert.Equal("12", templatePart.Default);
Assert.Collection(
templatePart.Constraints,
constraint => Assert.Equal("test", constraint.Content));
}
[Fact]
public void ParseRouteParameter_EmptyConstraint_ParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@":param:test:");
// Assert
Assert.Equal(":param", templatePart.Name);
Assert.Collection(
templatePart.Constraints,
constraint => Assert.Equal("test", constraint.Content));
}
[Fact]
public void ParseRouteParameter_ConstraintWithCommaInPattern_PatternIsParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@"param:test(\w,\w)");
// Assert
Assert.Equal("param", templatePart.Name);
var constraint = Assert.Single(templatePart.Constraints);
Assert.Equal(@"test(\w,\w)", constraint.Content);
}
[Fact]
public void ParseRouteParameter_ConstraintWithCommaInName_ParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@"par,am:test(\w)");
// Assert
Assert.Equal("par,am", templatePart.Name);
var constraint = Assert.Single(templatePart.Constraints);
Assert.Equal(@"test(\w)", constraint.Content);
}
[Fact]
public void ParseRouteParameter_ConstraintWithCommaInPattern_WithDefaultValue_ParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@"param:test(\w,\w)=jsd");
// Assert
Assert.Equal("param", templatePart.Name);
Assert.Equal("jsd", templatePart.Default);
var constraint = Assert.Single(templatePart.Constraints);
Assert.Equal(@"test(\w,\w)", constraint.Content);
}
[Fact]
public void ParseRouteParameter_ConstraintWithEqualsFollowedByQuestionMark_PatternIsParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@"param:int=?");
// Assert
Assert.Equal("param", templatePart.Name);
Assert.Equal("", templatePart.Default);
Assert.True(templatePart.IsOptional);
var constraint = Assert.Single(templatePart.Constraints);
Assert.Equal("int", constraint.Content);
}
[Fact]
public void ParseRouteParameter_ConstraintWithEqualsSignInPattern_PatternIsParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@"param:test(=)");
// Assert
Assert.Equal("param", templatePart.Name);
Assert.Null(templatePart.Default);
var constraint = Assert.Single(templatePart.Constraints);
Assert.Equal("test(=)", constraint.Content);
}
[Fact]
public void ParseRouteParameter_EqualsSignInDefaultValue_ParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@"param=test=bar");
// Assert
Assert.Equal("param", templatePart.Name);
Assert.Equal("test=bar", templatePart.Default);
}
[Fact]
public void ParseRouteParameter_ConstraintWithEqualEqualSignInPattern_ParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@"param:test(a==b)");
// Assert
Assert.Equal("param", templatePart.Name);
Assert.Null(templatePart.Default);
var constraint = Assert.Single(templatePart.Constraints);
Assert.Equal("test(a==b)", constraint.Content);
}
[Fact]
public void ParseRouteParameter_ConstraintWithEqualEqualSignInPattern_WithDefaultValue_ParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@"param:test(a==b)=dvds");
// Assert
Assert.Equal("param", templatePart.Name);
Assert.Equal("dvds", templatePart.Default);
var constraint = Assert.Single(templatePart.Constraints);
Assert.Equal("test(a==b)", constraint.Content);
}
[Fact]
public void ParseRouteParameter_EqualEqualSignInName_WithDefaultValue_ParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@"par==am:test=dvds");
// Assert
Assert.Equal("par", templatePart.Name);
Assert.Equal("=am:test=dvds", templatePart.Default);
}
[Fact]
public void ParseRouteParameter_EqualEqualSignInDefaultValue_ParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@"param:test==dvds");
// Assert
Assert.Equal("param", templatePart.Name);
Assert.Equal("=dvds", templatePart.Default);
}
[Fact]
public void ParseRouteParameter_DefaultValueWithColonAndParens_ParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@"par=am:test(asd)");
// Assert
Assert.Equal("par", templatePart.Name);
Assert.Equal("am:test(asd)", templatePart.Default);
}
[Fact]
public void ParseRouteParameter_DefaultValueWithEqualsSignIn_ParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@"par=test(am):est=asd");
// Assert
Assert.Equal("par", templatePart.Name);
Assert.Equal("test(am):est=asd", templatePart.Default);
}
[Fact]
public void ParseRouteParameter_ConstraintWithEqualsSignInPattern_WithDefaultValue_ParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@"param:test(=)=sds");
// Assert
Assert.Equal("param", templatePart.Name);
Assert.Equal("sds", templatePart.Default);
var constraint = Assert.Single(templatePart.Constraints);
Assert.Equal("test(=)", constraint.Content);
}
[Fact]
public void ParseRouteParameter_ConstraintWithOpenBraceInPattern_PatternIsParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@"param:test(\{)");
// Assert
Assert.Equal("param", templatePart.Name);
var constraint = Assert.Single(templatePart.Constraints);
Assert.Equal(@"test(\{)", constraint.Content);
}
[Fact]
public void ParseRouteParameter_ConstraintWithOpenBraceInName_ParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@"par{am:test(\sd)");
// Assert
Assert.Equal("par{am", templatePart.Name);
var constraint = Assert.Single(templatePart.Constraints);
Assert.Equal(@"test(\sd)", constraint.Content);
}
[Fact]
public void ParseRouteParameter_ConstraintWithOpenBraceInPattern_WithDefaultValue_ParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@"param:test(\{)=xvc");
// Assert
Assert.Equal("param", templatePart.Name);
Assert.Equal("xvc", templatePart.Default);
var constraint = Assert.Single(templatePart.Constraints);
Assert.Equal(@"test(\{)", constraint.Content);
}
[Fact]
public void ParseRouteParameter_ConstraintWithOpenParenInName_ParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@"par(am:test(\()");
// Assert
Assert.Equal("par(am", templatePart.Name);
var constraint = Assert.Single(templatePart.Constraints);
Assert.Equal(@"test(\()", constraint.Content);
}
[Fact]
public void ParseRouteParameter_ConstraintWithOpenParenInPattern_ParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@"param:test(\()");
// Assert
Assert.Equal("param", templatePart.Name);
var constraint = Assert.Single(templatePart.Constraints);
Assert.Equal(@"test(\()", constraint.Content);
}
[Fact]
public void ParseRouteParameter_ConstraintWithOpenParenNoCloseParen_ParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@"param:test(#$%");
// Assert
Assert.Equal("param", templatePart.Name);
var constraint = Assert.Single(templatePart.Constraints);
Assert.Equal("test(#$%", constraint.Content);
}
[Fact]
public void ParseRouteParameter_ConstraintWithOpenParenAndColon_ParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@"param:test(#:test1");
// Assert
Assert.Equal("param", templatePart.Name);
Assert.Collection(templatePart.Constraints,
constraint => Assert.Equal(@"test(#", constraint.Content),
constraint => Assert.Equal(@"test1", constraint.Content));
}
[Fact]
public void ParseRouteParameter_ConstraintWithOpenParenAndColonWithDefaultValue_ParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@"param:test(abc:somevalue):name(test1:differentname=default-value");
// Assert
Assert.Equal("param", templatePart.Name);
Assert.Equal("default-value", templatePart.Default);
Assert.Collection(templatePart.Constraints,
constraint => Assert.Equal(@"test(abc:somevalue)", constraint.Content),
constraint => Assert.Equal(@"name(test1", constraint.Content),
constraint => Assert.Equal(@"differentname", constraint.Content));
}
[Fact]
public void ParseRouteParameter_ConstraintWithOpenParenAndDefaultValue_ParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@"param:test(constraintvalue=test1");
// Assert
Assert.Equal("param", templatePart.Name);
Assert.Equal("test1", templatePart.Default);
var constraint = Assert.Single(templatePart.Constraints);
Assert.Equal(@"test(constraintvalue", constraint.Content);
}
[Fact]
public void ParseRouteParameter_ConstraintWithOpenParenInPattern_WithDefaultValue_ParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@"param:test(\()=djk");
// Assert
Assert.Equal("param", templatePart.Name);
Assert.Equal("djk", templatePart.Default);
var constraint = Assert.Single(templatePart.Constraints);
Assert.Equal(@"test(\()", constraint.Content);
}
[Fact]
public void ParseRouteParameter_ConstraintWithQuestionMarkInPattern_PatternIsParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@"param:test(\?)");
// Assert
Assert.Equal("param", templatePart.Name);
Assert.Null(templatePart.Default);
Assert.False(templatePart.IsOptional);
var constraint = Assert.Single(templatePart.Constraints);
Assert.Equal(@"test(\?)", constraint.Content);
}
[Fact]
public void ParseRouteParameter_ConstraintWithQuestionMarkInPattern_Optional_PatternIsParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@"param:test(\?)?");
// Assert
Assert.Equal("param", templatePart.Name);
Assert.Null(templatePart.Default);
Assert.True(templatePart.IsOptional);
var constraint = Assert.Single(templatePart.Constraints);
Assert.Equal(@"test(\?)", constraint.Content);
}
[Fact]
public void ParseRouteParameter_ConstraintWithQuestionMarkInPattern_WithDefaultValue_ParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@"param:test(\?)=sdf");
// Assert
Assert.Equal("param", templatePart.Name);
Assert.Equal("sdf", templatePart.Default);
Assert.False(templatePart.IsOptional);
var constraint = Assert.Single(templatePart.Constraints);
Assert.Equal(@"test(\?)", constraint.Content);
}
[Fact]
public void ParseRouteParameter_ConstraintWithQuestionMarkInPattern_Optional_WithDefaultValue_ParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@"param:test(\?)=sdf?");
// Assert
Assert.Equal("param", templatePart.Name);
Assert.Equal("sdf", templatePart.Default);
Assert.True(templatePart.IsOptional);
var constraint = Assert.Single(templatePart.Constraints);
Assert.Equal(@"test(\?)", constraint.Content);
}
[Fact]
public void ParseRouteParameter_ConstraintWithQuestionMarkInName_ParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@"par?am:test(\?)");
// Assert
Assert.Equal("par?am", templatePart.Name);
Assert.Null(templatePart.Default);
Assert.False(templatePart.IsOptional);
var constraint = Assert.Single(templatePart.Constraints);
Assert.Equal(@"test(\?)", constraint.Content);
}
[Fact]
public void ParseRouteParameter_ConstraintWithClosedParenAndColonInPattern_ParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@"param:test(#):$)");
// Assert
Assert.Equal("param", templatePart.Name);
Assert.Null(templatePart.Default);
Assert.False(templatePart.IsOptional);
Assert.Collection(templatePart.Constraints,
constraint => Assert.Equal(@"test(#)", constraint.Content),
constraint => Assert.Equal(@"$)", constraint.Content));
}
[Fact]
public void ParseRouteParameter_ConstraintWithColonAndClosedParenInPattern_PatternIsParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@"param:test(#:)$)");
// Assert
Assert.Equal("param", templatePart.Name);
Assert.Null(templatePart.Default);
Assert.False(templatePart.IsOptional);
var constraint = Assert.Single(templatePart.Constraints);
Assert.Equal(@"test(#:)$)", constraint.Content);
}
[Fact]
public void ParseRouteParameter_ContainingMultipleUnclosedParenthesisInConstraint()
{
// Arrange & Act
var templatePart = ParseParameter(@"foo:regex(\\(\\(\\(\\()");
// Assert
Assert.Equal("foo", templatePart.Name);
Assert.Null(templatePart.Default);
Assert.False(templatePart.IsOptional);
var constraint = Assert.Single(templatePart.Constraints);
Assert.Equal(@"regex(\\(\\(\\(\\()", constraint.Content);
}
[Fact]
public void ParseRouteParameter_ConstraintWithBraces_PatternIsParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@"p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)"); // ssn
// Assert
Assert.Equal("p1", templatePart.Name);
Assert.Null(templatePart.Default);
Assert.False(templatePart.IsOptional);
var constraint = Assert.Single(templatePart.Constraints);
Assert.Equal(@"regex(^\d{{3}}-\d{{3}}-\d{{4}}$)", constraint.Content);
}
[Fact]
public void ParseRouteParameter_ConstraintWithBraces_WithDefaultValue()
{
// Arrange & Act
var templatePart = ParseParameter(@"p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)=123-456-7890"); // ssn
// Assert
Assert.Equal("p1", templatePart.Name);
Assert.Equal("123-456-7890", templatePart.Default);
Assert.False(templatePart.IsOptional);
var constraint = Assert.Single(templatePart.Constraints);
Assert.Equal(@"regex(^\d{{3}}-\d{{3}}-\d{{4}}$)", constraint.Content);
}
[Theory]
[InlineData("", "")]
[InlineData("?", "")]
[InlineData("*", "")]
[InlineData(" ", " ")]
[InlineData("\t", "\t")]
[InlineData("#!@#$%Q@#@%", "#!@#$%Q@#@%")]
[InlineData(",,,", ",,,")]
public void ParseRouteParameter_ParameterWithoutInlineConstraint_ReturnsTemplatePartWithEmptyInlineValues(
string parameter,
string expectedParameterName)
{
// Arrange & Act
var templatePart = ParseParameter(parameter);
// Assert
Assert.Equal(expectedParameterName, templatePart.Name);
Assert.Empty(templatePart.Constraints);
Assert.Null(templatePart.Default);
}
private RoutePatternParameterPart ParseParameter(string routeParameter)
{
// See: #475 - these tests don't pass the 'whole' text.
var templatePart = RouteParameterParser.ParseRouteParameter(routeParameter);
return templatePart;
}
}
}

View File

@ -0,0 +1,247 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Linq;
using Microsoft.AspNetCore.Routing.Constraints;
using Xunit;
namespace Microsoft.AspNetCore.Routing.Patterns
{
public class RoutePatternFactoryTest
{
[Fact]
public void Pattern_MergesDefaultValues()
{
// Arrange
var template = "{a}/{b}/{c=19}";
var defaults = new { a = "15", b = 17 };
var constraints = new { };
var original = RoutePatternFactory.Parse(template);
// Act
var actual = RoutePatternFactory.Pattern(
original.RawText,
defaults,
constraints,
original.PathSegments);
// Assert
Assert.Equal("15", actual.GetParameter("a").Default);
Assert.Equal(17, actual.GetParameter("b").Default);
Assert.Equal("19", actual.GetParameter("c").Default);
Assert.Collection(
actual.Defaults.OrderBy(kvp => kvp.Key),
kvp => { Assert.Equal("a", kvp.Key); Assert.Equal("15", kvp.Value); },
kvp => { Assert.Equal("b", kvp.Key); Assert.Equal(17, kvp.Value); },
kvp => { Assert.Equal("c", kvp.Key); Assert.Equal("19", kvp.Value); });
}
[Fact]
public void Pattern_ExtraDefaultValues()
{
// Arrange
var template = "{a}/{b}/{c}";
var defaults = new { d = "15", e = 17 };
var constraints = new { };
var original = RoutePatternFactory.Parse(template);
// Act
var actual = RoutePatternFactory.Pattern(
original.RawText,
defaults,
constraints,
original.PathSegments);
// Assert
Assert.Collection(
actual.Defaults.OrderBy(kvp => kvp.Key),
kvp => { Assert.Equal("d", kvp.Key); Assert.Equal("15", kvp.Value); },
kvp => { Assert.Equal("e", kvp.Key); Assert.Equal(17, kvp.Value); });
}
[Fact]
public void Pattern_DuplicateDefaultValue_Throws()
{
// Arrange
var template = "{a=13}/{b}/{c}";
var defaults = new { a = "15", };
var constraints = new { };
var original = RoutePatternFactory.Parse(template);
// Act
var ex = Assert.Throws<InvalidOperationException>(() => RoutePatternFactory.Pattern(
original.RawText,
defaults,
constraints,
original.PathSegments));
// Assert
Assert.Equal(
"The route parameter 'a' has both an inline default value and an explicit default " +
"value specified. A route parameter cannot contain an inline default value when a " +
"default value is specified explicitly. Consider removing one of them.",
ex.Message);
}
[Fact]
public void Pattern_OptionalParameterDefaultValue_Throws()
{
// Arrange
var template = "{a}/{b}/{c?}";
var defaults = new { c = "15", };
var constraints = new { };
var original = RoutePatternFactory.Parse(template);
// Act
var ex = Assert.Throws<InvalidOperationException>(() => RoutePatternFactory.Pattern(
original.RawText,
defaults,
constraints,
original.PathSegments));
// Assert
Assert.Equal(
"An optional parameter cannot have default value.",
ex.Message);
}
[Fact]
public void Pattern_MergesConstraints()
{
// Arrange
var template = "{a:int}/{b}/{c}";
var defaults = new { };
var constraints = new { a = new RegexRouteConstraint("foo"), b = new RegexRouteConstraint("bar") };
var original = RoutePatternFactory.Parse(template);
// Act
var actual = RoutePatternFactory.Pattern(
original.RawText,
defaults,
constraints,
original.PathSegments);
// Assert
Assert.Collection(
actual.GetParameter("a").Constraints,
c => Assert.IsType<RegexRouteConstraint>(c.Constraint),
c => Assert.Equal("int", c.Content));
Assert.Collection(
actual.GetParameter("b").Constraints,
c => Assert.IsType<RegexRouteConstraint>(c.Constraint));
Assert.Collection(
actual.Constraints.OrderBy(kvp => kvp.Key),
kvp =>
{
Assert.Equal("a", kvp.Key);
Assert.Collection(
kvp.Value,
c => Assert.IsType<RegexRouteConstraint>(c.Constraint),
c => Assert.Equal("int", c.Content));
},
kvp =>
{
Assert.Equal("b", kvp.Key);
Assert.Collection(
kvp.Value,
c => Assert.IsType<RegexRouteConstraint>(c.Constraint));
});
}
[Fact]
public void Pattern_ExtraConstraints()
{
// Arrange
var template = "{a}/{b}/{c}";
var defaults = new { };
var constraints = new { d = new RegexRouteConstraint("foo"), e = new RegexRouteConstraint("bar") };
var original = RoutePatternFactory.Parse(template);
// Act
var actual = RoutePatternFactory.Pattern(
original.RawText,
defaults,
constraints,
original.PathSegments);
// Assert
Assert.Collection(
actual.Constraints.OrderBy(kvp => kvp.Key),
kvp =>
{
Assert.Equal("d", kvp.Key);
Assert.Collection(
kvp.Value,
c => Assert.IsType<RegexRouteConstraint>(c.Constraint));
},
kvp =>
{
Assert.Equal("e", kvp.Key);
Assert.Collection(
kvp.Value,
c => Assert.IsType<RegexRouteConstraint>(c.Constraint));
});
}
[Fact]
public void Pattern_CreatesConstraintFromString()
{
// Arrange
var template = "{a}/{b}/{c}";
var defaults = new { };
var constraints = new { d = "foo", };
var original = RoutePatternFactory.Parse(template);
// Act
var actual = RoutePatternFactory.Pattern(
original.RawText,
defaults,
constraints,
original.PathSegments);
// Assert
Assert.Collection(
actual.Constraints.OrderBy(kvp => kvp.Key),
kvp =>
{
Assert.Equal("d", kvp.Key);
var regex = Assert.IsType<RegexRouteConstraint>(Assert.Single(kvp.Value).Constraint);
Assert.Equal("^(foo)$", regex.Constraint.ToString());
});
}
[Fact]
public void Pattern_InvalidConstraintTypeThrows()
{
// Arrange
var template = "{a}/{b}/{c}";
var defaults = new { };
var constraints = new { d = 17, };
var original = RoutePatternFactory.Parse(template);
// Act
var ex = Assert.Throws<InvalidOperationException>(() => RoutePatternFactory.Pattern(
original.RawText,
defaults,
constraints,
original.PathSegments));
// Assert
Assert.Equal(
"The constraint entry 'd' - '17' must have a string value or be of a type " +
"which implements 'Microsoft.AspNetCore.Routing.IRouteConstraint'.",
ex.Message);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,765 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Microsoft.AspNetCore.Testing;
using Xunit;
using static Microsoft.AspNetCore.Routing.Patterns.RoutePatternFactory;
namespace Microsoft.AspNetCore.Routing.Patterns
{
public class RoutePatternParameterParserTest
{
[Fact]
public void Parse_SingleLiteral()
{
// Arrange
var template = "cool";
var expected = Pattern(
template,
Segment(LiteralPart("cool")));
// Act
var actual = RoutePatternParser.Parse(template);
// Assert
Assert.Equal<RoutePattern>(expected, actual, new RoutePatternEqualityComparer());
}
[Fact]
public void Parse_SingleParameter()
{
// Arrange
var template = "{p}";
var expected = Pattern(template, Segment(ParameterPart("p")));
// Act
var actual = RoutePatternParser.Parse(template);
// Assert
Assert.Equal<RoutePattern>(expected, actual, new RoutePatternEqualityComparer());
}
[Fact]
public void Parse_OptionalParameter()
{
// Arrange
var template = "{p?}";
var expected = Pattern(template, Segment(ParameterPart("p", null, RoutePatternParameterKind.Optional)));
// Act
var actual = RoutePatternParser.Parse(template);
// Assert
Assert.Equal<RoutePattern>(expected, actual, new RoutePatternEqualityComparer());
}
[Fact]
public void Parse_MultipleLiterals()
{
// Arrange
var template = "cool/awesome/super";
var expected = Pattern(
template,
Segment(LiteralPart("cool")),
Segment(LiteralPart("awesome")),
Segment(LiteralPart("super")));
// Act
var actual = RoutePatternParser.Parse(template);
// Assert
Assert.Equal<RoutePattern>(expected, actual, new RoutePatternEqualityComparer());
}
[Fact]
public void Parse_MultipleParameters()
{
// Arrange
var template = "{p1}/{p2}/{*p3}";
var expected = Pattern(
template,
Segment(ParameterPart("p1")),
Segment(ParameterPart("p2")),
Segment(ParameterPart("p3", null, RoutePatternParameterKind.CatchAll)));
// Act
var actual = RoutePatternParser.Parse(template);
// Assert
Assert.Equal<RoutePattern>(expected, actual, new RoutePatternEqualityComparer());
}
[Fact]
public void Parse_ComplexSegment_LP()
{
// Arrange
var template = "cool-{p1}";
var expected = Pattern(
template,
Segment(
LiteralPart("cool-"),
ParameterPart("p1")));
// Act
var actual = RoutePatternParser.Parse(template);
// Assert
Assert.Equal<RoutePattern>(expected, actual, new RoutePatternEqualityComparer());
}
[Fact]
public void Parse_ComplexSegment_PL()
{
// Arrange
var template = "{p1}-cool";
var expected = Pattern(
template,
Segment(
ParameterPart("p1"),
LiteralPart("-cool")));
// Act
var actual = RoutePatternParser.Parse(template);
// Assert
Assert.Equal<RoutePattern>(expected, actual, new RoutePatternEqualityComparer());
}
[Fact]
public void Parse_ComplexSegment_PLP()
{
// Arrange
var template = "{p1}-cool-{p2}";
var expected = Pattern(
template,
Segment(
ParameterPart("p1"),
LiteralPart("-cool-"),
ParameterPart("p2")));
// Act
var actual = RoutePatternParser.Parse(template);
// Assert
Assert.Equal<RoutePattern>(expected, actual, new RoutePatternEqualityComparer());
}
[Fact]
public void Parse_ComplexSegment_LPL()
{
// Arrange
var template = "cool-{p1}-awesome";
var expected = Pattern(
template,
Segment(
LiteralPart("cool-"),
ParameterPart("p1"),
LiteralPart("-awesome")));
// Act
var actual = RoutePatternParser.Parse(template);
// Assert
Assert.Equal<RoutePattern>(expected, actual, new RoutePatternEqualityComparer());
}
[Fact]
public void Parse_ComplexSegment_OptionalParameterFollowingPeriod()
{
// Arrange
var template = "{p1}.{p2?}";
var expected = Pattern(
template,
Segment(
ParameterPart("p1"),
SeparatorPart("."),
ParameterPart("p2", null, RoutePatternParameterKind.Optional)));
// Act
var actual = RoutePatternParser.Parse(template);
// Assert
Assert.Equal<RoutePattern>(expected, actual, new RoutePatternEqualityComparer());
}
[Fact]
public void Parse_ComplexSegment_ParametersFollowingPeriod()
{
// Arrange
var template = "{p1}.{p2}";
var expected = Pattern(
template,
Segment(
ParameterPart("p1"),
LiteralPart("."),
ParameterPart("p2")));
// Act
var actual = RoutePatternParser.Parse(template);
// Assert
Assert.Equal<RoutePattern>(expected, actual, new RoutePatternEqualityComparer());
}
[Fact]
public void Parse_ComplexSegment_OptionalParameterFollowingPeriod_ThreeParameters()
{
// Arrange
var template = "{p1}.{p2}.{p3?}";
var expected = Pattern(
template,
Segment(
ParameterPart("p1"),
LiteralPart("."),
ParameterPart("p2"),
SeparatorPart("."),
ParameterPart("p3", null, RoutePatternParameterKind.Optional)));
// Act
var actual = RoutePatternParser.Parse(template);
// Assert
Assert.Equal<RoutePattern>(expected, actual, new RoutePatternEqualityComparer());
}
[Fact]
public void Parse_ComplexSegment_ThreeParametersSeperatedByPeriod()
{
// Arrange
var template = "{p1}.{p2}.{p3}";
var expected = Pattern(
template,
Segment(
ParameterPart("p1"),
LiteralPart("."),
ParameterPart("p2"),
LiteralPart("."),
ParameterPart("p3")));
// Act
var actual = RoutePatternParser.Parse(template);
// Assert
Assert.Equal<RoutePattern>(expected, actual, new RoutePatternEqualityComparer());
}
[Fact]
public void Parse_ComplexSegment_OptionalParameterFollowingPeriod_MiddleSegment()
{
// Arrange
var template = "{p1}.{p2?}/{p3}";
var expected = Pattern(
template,
Segment(
ParameterPart("p1"),
SeparatorPart("."),
ParameterPart("p2", null, RoutePatternParameterKind.Optional)),
Segment(
ParameterPart("p3")));
// Act
var actual = RoutePatternParser.Parse(template);
// Assert
Assert.Equal<RoutePattern>(expected, actual, new RoutePatternEqualityComparer());
}
[Fact]
public void Parse_ComplexSegment_OptionalParameterFollowingPeriod_LastSegment()
{
// Arrange
var template = "{p1}/{p2}.{p3?}";
var expected = Pattern(
template,
Segment(
ParameterPart("p1")),
Segment(
ParameterPart("p2"),
SeparatorPart("."),
ParameterPart("p3", null, RoutePatternParameterKind.Optional)));
// Act
var actual = RoutePatternParser.Parse(template);
// Assert
Assert.Equal<RoutePattern>(expected, actual, new RoutePatternEqualityComparer());
}
[Fact]
public void Parse_ComplexSegment_OptionalParameterFollowingPeriod_PeriodAfterSlash()
{
// Arrange
var template = "{p2}/.{p3?}";
var expected = Pattern(
template,
Segment(ParameterPart("p2")),
Segment(
SeparatorPart("."),
ParameterPart("p3", null, RoutePatternParameterKind.Optional)));
// Act
var actual = RoutePatternParser.Parse(template);
// Assert
Assert.Equal<RoutePattern>(expected, actual, new RoutePatternEqualityComparer());
}
[Theory]
[InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)}", @"regex(^\d{3}-\d{3}-\d{4}$)")] // ssn
[InlineData(@"{p1:regex(^\d{{1,2}}\/\d{{1,2}}\/\d{{4}}$)}", @"regex(^\d{1,2}\/\d{1,2}\/\d{4}$)")] // date
[InlineData(@"{p1:regex(^\w+\@\w+\.\w+)}", @"regex(^\w+\@\w+\.\w+)")] // email
[InlineData(@"{p1:regex(([}}])\w+)}", @"regex(([}])\w+)")] // Not balanced }
[InlineData(@"{p1:regex(([{{(])\w+)}", @"regex(([{(])\w+)")] // Not balanced {
public void Parse_RegularExpressions(string template, string constraint)
{
// Arrange
var expected = Pattern(
template,
Segment(
ParameterPart(
"p1",
null,
RoutePatternParameterKind.Standard,
Constraint("p1", constraint))));
// Act
var actual = RoutePatternParser.Parse(template);
// Assert
Assert.Equal<RoutePattern>(expected, actual, new RoutePatternEqualityComparer());
}
[Theory]
[InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}}$)}")] // extra }
[InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)}}")] // extra } at the end
[InlineData(@"{{p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)}")] // extra { at the begining
[InlineData(@"{p1:regex(([}])\w+}")] // Not escaped }
[InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{{4}$)}")] // Not escaped }
[InlineData(@"{p1:regex(abc)")]
public void Parse_RegularExpressions_Invalid(string template)
{
// Act and Assert
ExceptionAssert.Throws<RoutePatternException>(
() => RoutePatternParser.Parse(template),
"There is an incomplete parameter in the route template. Check that each '{' character has a matching " +
"'}' character.");
}
[Theory]
[InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{{{4}}$)}")] // extra {
[InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{4}}$)}")] // Not escaped {
public void Parse_RegularExpressions_Unescaped(string template)
{
// Act and Assert
ExceptionAssert.Throws<RoutePatternException>(
() => RoutePatternParser.Parse(template),
"In a route parameter, '{' and '}' must be escaped with '{{' and '}}'.");
}
[Theory]
[InlineData("{p1}.{p2?}.{p3}", "p2", ".")]
[InlineData("{p1?}{p2}", "p1", "{p2}")]
[InlineData("{p1?}{p2?}", "p1", "{p2?}")]
[InlineData("{p1}.{p2?})", "p2", ")")]
[InlineData("{foorb?}-bar-{z}", "foorb", "-bar-")]
public void Parse_ComplexSegment_OptionalParameter_NotTheLastPart(
string template,
string parameter,
string invalid)
{
// Act and Assert
ExceptionAssert.Throws<RoutePatternException>(
() => RoutePatternParser.Parse(template),
"An optional parameter must be at the end of the segment. In the segment '" + template +
"', optional parameter '" + parameter + "' is followed by '" + invalid + "'.");
}
[Theory]
[InlineData("{p1}-{p2?}", "-")]
[InlineData("{p1}..{p2?}", "..")]
[InlineData("..{p2?}", "..")]
[InlineData("{p1}.abc.{p2?}", ".abc.")]
[InlineData("{p1}{p2?}", "{p1}")]
public void Parse_ComplexSegment_OptionalParametersSeperatedByPeriod_Invalid(string template, string parameter)
{
// Act and Assert
ExceptionAssert.Throws<RoutePatternException>(
() => RoutePatternParser.Parse(template),
"In the segment '" + template + "', the optional parameter 'p2' is preceded by an invalid " +
"segment '" + parameter + "'. Only a period (.) can precede an optional parameter.");
}
[Fact]
public void InvalidTemplate_WithRepeatedParameter()
{
var ex = ExceptionAssert.Throws<RoutePatternException>(
() => RoutePatternParser.Parse("{Controller}.mvc/{id}/{controller}"),
"The route parameter name 'controller' appears more than one time in the route template.");
}
[Theory]
[InlineData("123{a}abc{")]
[InlineData("123{a}abc}")]
[InlineData("xyz}123{a}abc}")]
[InlineData("{{p1}")]
[InlineData("{p1}}")]
[InlineData("p1}}p2{")]
public void InvalidTemplate_WithMismatchedBraces(string template)
{
ExceptionAssert.Throws<RoutePatternException>(
() => RoutePatternParser.Parse(template),
@"There is an incomplete parameter in the route template. Check that each '{' character has a " +
"matching '}' character.");
}
[Fact]
public void InvalidTemplate_CannotHaveCatchAllInMultiSegment()
{
ExceptionAssert.Throws<RoutePatternException>(
() => RoutePatternParser.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.");
}
[Fact]
public void InvalidTemplate_CannotHaveMoreThanOneCatchAll()
{
ExceptionAssert.Throws<RoutePatternException>(
() => RoutePatternParser.Parse("{*p1}/{*p2}"),
"A catch-all parameter can only appear as the last segment of the route template.");
}
[Fact]
public void InvalidTemplate_CannotHaveMoreThanOneCatchAllInMultiSegment()
{
ExceptionAssert.Throws<RoutePatternException>(
() => RoutePatternParser.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.");
}
[Fact]
public void InvalidTemplate_CannotHaveCatchAllWithNoName()
{
ExceptionAssert.Throws<RoutePatternException>(
() => RoutePatternParser.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," +
" and can occur only at the start of the parameter.");
}
[Theory]
[InlineData("{**}", "*")]
[InlineData("{a*}", "a*")]
[InlineData("{*a*}", "a*")]
[InlineData("{*a*:int}", "a*")]
[InlineData("{*a*=5}", "a*")]
[InlineData("{*a*b=5}", "a*b")]
[InlineData("{p1?}.{p2/}/{p3}", "p2/")]
[InlineData("{p{{}", "p{")]
[InlineData("{p}}}", "p}")]
[InlineData("{p/}", "p/")]
public void ParseRouteParameter_ThrowsIf_ParameterContainsSpecialCharacters(
string template,
string parameterName)
{
// Arrange
var expectedMessage = "The route parameter name '" + parameterName + "' 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, and can occur only at the start of the parameter.";
// Act & Assert
ExceptionAssert.Throws<RoutePatternException>(() => RoutePatternParser.Parse(template), expectedMessage);
}
[Fact]
public void InvalidTemplate_CannotHaveConsecutiveOpenBrace()
{
ExceptionAssert.Throws<RoutePatternException>(
() => RoutePatternParser.Parse("foo/{{p1}"),
"There is an incomplete parameter in the route template. Check that each '{' character has a " +
"matching '}' character.");
}
[Fact]
public void InvalidTemplate_CannotHaveConsecutiveCloseBrace()
{
ExceptionAssert.Throws<RoutePatternException>(
() => RoutePatternParser.Parse("foo/{p1}}"),
"There is an incomplete parameter in the route template. Check that each '{' character has a " +
"matching '}' character.");
}
[Fact]
public void InvalidTemplate_SameParameterTwiceThrows()
{
ExceptionAssert.Throws<RoutePatternException>(
() => RoutePatternParser.Parse("{aaa}/{AAA}"),
"The route parameter name 'AAA' appears more than one time in the route template.");
}
[Fact]
public void InvalidTemplate_SameParameterTwiceAndOneCatchAllThrows()
{
ExceptionAssert.Throws<RoutePatternException>(
() => RoutePatternParser.Parse("{aaa}/{*AAA}"),
"The route parameter name 'AAA' appears more than one time in the route template.");
}
[Fact]
public void InvalidTemplate_InvalidParameterNameWithCloseBracketThrows()
{
ExceptionAssert.Throws<RoutePatternException>(
() => RoutePatternParser.Parse("{a}/{aa}a}/{z}"),
"There is an incomplete parameter in the route template. Check that each '{' character has a " +
"matching '}' character.");
}
[Fact]
public void InvalidTemplate_InvalidParameterNameWithOpenBracketThrows()
{
ExceptionAssert.Throws<RoutePatternException>(
() => RoutePatternParser.Parse("{a}/{a{aa}/{z}"),
"In a route parameter, '{' and '}' must be escaped with '{{' and '}}'.");
}
[Fact]
public void InvalidTemplate_InvalidParameterNameWithEmptyNameThrows()
{
ExceptionAssert.Throws<RoutePatternException>(
() => RoutePatternParser.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," +
" and can occur only at the start of the parameter.");
}
[Fact]
public void InvalidTemplate_InvalidParameterNameWithQuestionThrows()
{
ExceptionAssert.Throws<RoutePatternException>(
() => RoutePatternParser.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," +
" and can occur only at the start of the parameter.");
}
[Fact]
public void InvalidTemplate_ConsecutiveSeparatorsSlashSlashThrows()
{
ExceptionAssert.Throws<RoutePatternException>(
() => RoutePatternParser.Parse("{a}//{z}"),
"The route template separator character '/' cannot appear consecutively. It must be separated by " +
"either a parameter or a literal value.");
}
[Fact]
public void InvalidTemplate_WithCatchAllNotAtTheEndThrows()
{
ExceptionAssert.Throws<RoutePatternException>(
() => RoutePatternParser.Parse("foo/{p1}/{*p2}/{p3}"),
"A catch-all parameter can only appear as the last segment of the route template.");
}
[Fact]
public void InvalidTemplate_RepeatedParametersThrows()
{
ExceptionAssert.Throws<RoutePatternException>(
() => RoutePatternParser.Parse("foo/aa{p1}{p2}"),
"A path segment cannot contain two consecutive parameters. They must be separated by a '/' or by " +
"a literal string.");
}
[Theory]
[InlineData("/foo")]
[InlineData("~/foo")]
public void ValidTemplate_CanStartWithSlashOrTildeSlash(string routePattern)
{
// Arrange & Act
var pattern = RoutePatternParser.Parse(routePattern);
// Assert
Assert.Equal(routePattern, pattern.RawText);
}
[Fact]
public void InvalidTemplate_CannotStartWithTilde()
{
ExceptionAssert.Throws<RoutePatternException>(
() => RoutePatternParser.Parse("~foo"),
"The route template cannot start with a '~' character unless followed by a '/'.");
}
[Fact]
public void InvalidTemplate_CannotContainQuestionMark()
{
ExceptionAssert.Throws<RoutePatternException>(
() => RoutePatternParser.Parse("foor?bar"),
"The literal section 'foor?bar' is invalid. Literal sections cannot contain the '?' character.");
}
[Fact]
public void InvalidTemplate_ParameterCannotContainQuestionMark_UnlessAtEnd()
{
ExceptionAssert.Throws<RoutePatternException>(
() => RoutePatternParser.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," +
" and can occur only at the start of the parameter.");
}
[Fact]
public void InvalidTemplate_CatchAllMarkedOptional()
{
ExceptionAssert.Throws<RoutePatternException>(
() => RoutePatternParser.Parse("{a}/{*b?}"),
"A catch-all parameter cannot be marked optional.");
}
private class RoutePatternEqualityComparer :
IEqualityComparer<RoutePattern>,
IEqualityComparer<RoutePatternConstraintReference>
{
public bool Equals(RoutePattern x, RoutePattern y)
{
if (x == null && y == null)
{
return true;
}
else if (x == null || y == null)
{
return false;
}
else
{
if (!string.Equals(x.RawText, y.RawText, StringComparison.Ordinal))
{
return false;
}
if (x.PathSegments.Count != y.PathSegments.Count)
{
return false;
}
for (int i = 0; i < x.PathSegments.Count; i++)
{
if (x.PathSegments[i].Parts.Count != y.PathSegments[i].Parts.Count)
{
return false;
}
for (int j = 0; j < x.PathSegments[i].Parts.Count; j++)
{
if (!Equals(x.PathSegments[i].Parts[j], y.PathSegments[i].Parts[j]))
{
return false;
}
}
}
if (x.Parameters.Count != y.Parameters.Count)
{
return false;
}
for (int i = 0; i < x.Parameters.Count; i++)
{
if (!Equals(x.Parameters[i], y.Parameters[i]))
{
return false;
}
}
return true;
}
}
private bool Equals(RoutePatternPart x, RoutePatternPart y)
{
if (x.GetType() != y.GetType())
{
return false;
}
if (x.IsLiteral && y.IsLiteral)
{
return Equals((RoutePatternLiteralPart)x, (RoutePatternLiteralPart)y);
}
else if (x.IsParameter && y.IsParameter)
{
return Equals((RoutePatternParameterPart)x, (RoutePatternParameterPart)y);
}
else if (x.IsSeparator && y.IsSeparator)
{
return Equals((RoutePatternSeparatorPart)x, (RoutePatternSeparatorPart)y);
}
Debug.Fail("This should not be reachable. Do you need to update the comparison logic?");
return false;
}
private bool Equals(RoutePatternLiteralPart x, RoutePatternLiteralPart y)
{
return x.Content == y.Content;
}
private bool Equals(RoutePatternParameterPart x, RoutePatternParameterPart y)
{
return
x.Name == y.Name &&
x.Default == y.Default &&
x.ParameterKind == y.ParameterKind &&
Enumerable.SequenceEqual(x.Constraints, y.Constraints, this);
}
public bool Equals(RoutePatternConstraintReference x, RoutePatternConstraintReference y)
{
return
x.ParameterName == y.ParameterName &&
x.Content == y.Content &&
x.Constraint == y.Constraint;
}
private bool Equals(RoutePatternSeparatorPart x, RoutePatternSeparatorPart y)
{
return x.Content == y.Content;
}
public int GetHashCode(RoutePattern obj)
{
throw new NotImplementedException();
}
public int GetHashCode(RoutePatternConstraintReference obj)
{
throw new NotImplementedException();
}
}
}
}

View File

@ -5,9 +5,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Routing.Template.Tests
@ -528,8 +525,8 @@ namespace Microsoft.AspNetCore.Routing.Template.Tests
[Theory]
[InlineData("{p1}.{p2?}.{p3}", "p2", ".")]
[InlineData("{p1?}{p2}", "p1", "p2")]
[InlineData("{p1?}{p2?}", "p1", "p2")]
[InlineData("{p1?}{p2}", "p1", "{p2}")]
[InlineData("{p1?}{p2?}", "p1", "{p2?}")]
[InlineData("{p1}.{p2?})", "p2", ")")]
[InlineData("{foorb?}-bar-{z}", "foorb", "-bar-")]
public void Parse_ComplexSegment_OptionalParameter_NotTheLastPart(
@ -655,18 +652,6 @@ namespace Microsoft.AspNetCore.Routing.Template.Tests
"Parameter name: routeTemplate");
}
[Theory]
[InlineData("/foo")]
[InlineData("~/foo")]
public void ValidTemplate_CanStartWithSlashOrTildeSlash(string routeTemplate)
{
// Arrange & Act
var template = TemplateParser.Parse(routeTemplate);
// Assert
Assert.Equal(routeTemplate, template.TemplateText);
}
[Fact]
public void InvalidTemplate_CannotHaveConsecutiveOpenBrace()
{
@ -780,6 +765,18 @@ namespace Microsoft.AspNetCore.Routing.Template.Tests
"Parameter name: routeTemplate");
}
[Theory]
[InlineData("/foo")]
[InlineData("~/foo")]
public void ValidTemplate_CanStartWithSlashOrTildeSlash(string routeTemplate)
{
// Arrange & Act
var pattern = TemplateParser.Parse(routeTemplate);
// Assert
Assert.Equal(routeTemplate, pattern.TemplateText);
}
[Fact]
public void InvalidTemplate_CannotStartWithTilde()
{