Merge pull request #609 from aspnet/release/2.2

Introduce RoutePattern (#585)
This commit is contained in:
Ryan Nowak 2018-07-13 18:10:19 -07:00 committed by GitHub
commit 650178b09d
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()
{