Port TemplateParser to Dispatcher project (#473)

Addresses #466
This commit is contained in:
Jass Bagga 2017-10-17 11:52:26 -07:00 committed by GitHub
parent 927f8ed3d2
commit 3a5cd6dd25
23 changed files with 3183 additions and 1018 deletions

View File

@ -9,7 +9,6 @@ using BenchmarkDotNet.Attributes;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Internal;
using Microsoft.AspNetCore.Routing.Template;
using Microsoft.AspNetCore.Routing.Tree;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.ObjectPool;
@ -35,11 +34,11 @@ namespace Microsoft.AspNetCore.Dispatcher.Performance
new DefaultObjectPool<UriBuildingContext>(new UriBuilderContextPooledObjectPolicy(UrlEncoder.Default)),
new DefaultInlineConstraintResolver(new OptionsManager<RouteOptions>(new OptionsFactory<RouteOptions>(Enumerable.Empty<IConfigureOptions<RouteOptions>>(), Enumerable.Empty<IPostConfigureOptions<RouteOptions>>()))));
treeBuilder.MapInbound(handler, TemplateParser.Parse("api/Widgets"), "default", 0);
treeBuilder.MapInbound(handler, TemplateParser.Parse("api/Widgets/{id}"), "default", 0);
treeBuilder.MapInbound(handler, TemplateParser.Parse("api/Widgets/search/{term}"), "default", 0);
treeBuilder.MapInbound(handler, TemplateParser.Parse("admin/users/{id}"), "default", 0);
treeBuilder.MapInbound(handler, TemplateParser.Parse("admin/users/{id}/manage"), "default", 0);
treeBuilder.MapInbound(handler, Routing.Template.TemplateParser.Parse("api/Widgets"), "default", 0);
treeBuilder.MapInbound(handler, Routing.Template.TemplateParser.Parse("api/Widgets/{id}"), "default", 0);
treeBuilder.MapInbound(handler, Routing.Template.TemplateParser.Parse("api/Widgets/search/{term}"), "default", 0);
treeBuilder.MapInbound(handler, Routing.Template.TemplateParser.Parse("admin/users/{id}"), "default", 0);
treeBuilder.MapInbound(handler, Routing.Template.TemplateParser.Parse("admin/users/{id}/manage"), "default", 0);
_treeRouter = treeBuilder.Build();

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;
namespace Microsoft.AspNetCore.Dispatcher
{
/// <summary>
/// The parsed representation of an inline constraint in a route parameter.
/// </summary>
public class InlineConstraint
{
/// <summary>
/// Creates a new <see cref="InlineConstraint"/>.
/// </summary>
/// <param name="constraint">The constraint text.</param>
public InlineConstraint(string constraint)
{
if (constraint == null)
{
throw new ArgumentNullException(nameof(constraint));
}
Constraint = constraint;
}
/// <summary>
/// Gets the constraint text.
/// </summary>
public string Constraint { get; }
}
}

View File

@ -0,0 +1,242 @@
// 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;
namespace Microsoft.AspNetCore.Dispatcher
{
public static class InlineRouteParameterParser
{
public static TemplatePart ParseRouteParameter(string routeParameter)
{
if (routeParameter == null)
{
throw new ArgumentNullException(nameof(routeParameter));
}
if (routeParameter.Length == 0)
{
return TemplatePart.CreateParameter(
name: string.Empty,
isCatchAll: false,
isOptional: false,
defaultValue: null,
inlineConstraints: null);
}
var startIndex = 0;
var endIndex = routeParameter.Length - 1;
var isCatchAll = false;
var isOptional = false;
if (routeParameter[0] == '*')
{
isCatchAll = true;
startIndex++;
}
if (routeParameter[endIndex] == '?')
{
isOptional = true;
endIndex--;
}
var currentIndex = startIndex;
// Parse parameter name
var parameterName = string.Empty;
while (currentIndex <= endIndex)
{
var currentChar = routeParameter[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 = routeParameter.Substring(startIndex, currentIndex - startIndex);
// Roll the index back and move to the constraint parsing stage.
currentIndex--;
break;
}
else if (currentIndex == endIndex)
{
parameterName = routeParameter.Substring(startIndex, currentIndex - startIndex + 1);
}
currentIndex++;
}
var parseResults = ParseConstraints(routeParameter, currentIndex, endIndex);
currentIndex = parseResults.CurrentIndex;
string defaultValue = null;
if (currentIndex <= endIndex &&
routeParameter[currentIndex] == '=')
{
defaultValue = routeParameter.Substring(currentIndex + 1, endIndex - currentIndex);
}
return TemplatePart.CreateParameter(parameterName,
isCatchAll,
isOptional,
defaultValue,
parseResults.Constraints);
}
private static ConstraintParseResults ParseConstraints(
string routeParameter,
int currentIndex,
int endIndex)
{
var inlineConstraints = new List<InlineConstraint>();
var state = ParseState.Start;
var startIndex = currentIndex;
do
{
var currentChar = currentIndex > endIndex ? null : (char?)routeParameter[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 = routeParameter.Substring(startIndex, currentIndex - startIndex);
inlineConstraints.Add(new InlineConstraint(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?)routeParameter[currentIndex + 1];
switch (nextChar)
{
case null:
state = ParseState.End;
constraintText = routeParameter.Substring(startIndex, currentIndex - startIndex + 1);
inlineConstraints.Add(new InlineConstraint(constraintText));
break;
case ':':
state = ParseState.Start;
constraintText = routeParameter.Substring(startIndex, currentIndex - startIndex + 1);
inlineConstraints.Add(new InlineConstraint(constraintText));
startIndex = currentIndex + 1;
break;
case '=':
state = ParseState.End;
constraintText = routeParameter.Substring(startIndex, currentIndex - startIndex + 1);
inlineConstraints.Add(new InlineConstraint(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 = routeParameter.IndexOf(')', currentIndex + 1);
if (indexOfClosingParantheses == -1)
{
constraintText = routeParameter.Substring(startIndex, currentIndex - startIndex);
inlineConstraints.Add(new InlineConstraint(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 = routeParameter.Substring(startIndex, currentIndex - startIndex);
inlineConstraints.Add(new InlineConstraint(constraintText));
break;
case ':':
constraintText = routeParameter.Substring(startIndex, currentIndex - startIndex);
inlineConstraints.Add(new InlineConstraint(constraintText));
startIndex = currentIndex + 1;
break;
case '(':
state = ParseState.InsideParenthesis;
break;
case '=':
state = ParseState.End;
constraintText = routeParameter.Substring(startIndex, currentIndex - startIndex);
inlineConstraints.Add(new InlineConstraint(constraintText));
currentIndex--;
break;
}
break;
}
currentIndex++;
} while (state != ParseState.End);
return new ConstraintParseResults
{
CurrentIndex = currentIndex,
Constraints = inlineConstraints
};
}
private enum ParseState
{
Start,
ParsingName,
InsideParenthesis,
End
}
private struct ConstraintParseResults
{
public int CurrentIndex;
public IEnumerable<InlineConstraint> Constraints;
}
}
}

View File

@ -24,6 +24,202 @@ namespace Microsoft.AspNetCore.Dispatcher
internal static string FormatAmbiguousEndpoints(object p0, object p1)
=> string.Format(CultureInfo.CurrentCulture, GetString("AmbiguousEndpoints"), p0, p1);
/// <summary>
/// A path segment that contains more than one section, such as a literal section or a parameter, cannot contain a catch-all parameter.
/// </summary>
internal static string TemplateRoute_CannotHaveCatchAllInMultiSegment
{
get => GetString("TemplateRoute_CannotHaveCatchAllInMultiSegment");
}
/// <summary>
/// A path segment that contains more than one section, such as a literal section or a parameter, cannot contain a catch-all parameter.
/// </summary>
internal static string FormatTemplateRoute_CannotHaveCatchAllInMultiSegment()
=> GetString("TemplateRoute_CannotHaveCatchAllInMultiSegment");
/// <summary>
/// A path segment cannot contain two consecutive parameters. They must be separated by a '/' or by a literal string.
/// </summary>
internal static string TemplateRoute_CannotHaveConsecutiveParameters
{
get => GetString("TemplateRoute_CannotHaveConsecutiveParameters");
}
/// <summary>
/// A path segment cannot contain two consecutive parameters. They must be separated by a '/' or by a literal string.
/// </summary>
internal static string FormatTemplateRoute_CannotHaveConsecutiveParameters()
=> GetString("TemplateRoute_CannotHaveConsecutiveParameters");
/// <summary>
/// The route template separator character '/' cannot appear consecutively. It must be separated by either a parameter or a literal value.
/// </summary>
internal static string TemplateRoute_CannotHaveConsecutiveSeparators
{
get => GetString("TemplateRoute_CannotHaveConsecutiveSeparators");
}
/// <summary>
/// The route template separator character '/' cannot appear consecutively. It must be separated by either a parameter or a literal value.
/// </summary>
internal static string FormatTemplateRoute_CannotHaveConsecutiveSeparators()
=> GetString("TemplateRoute_CannotHaveConsecutiveSeparators");
/// <summary>
/// A catch-all parameter cannot be marked optional.
/// </summary>
internal static string TemplateRoute_CatchAllCannotBeOptional
{
get => GetString("TemplateRoute_CatchAllCannotBeOptional");
}
/// <summary>
/// A catch-all parameter cannot be marked optional.
/// </summary>
internal static string FormatTemplateRoute_CatchAllCannotBeOptional()
=> GetString("TemplateRoute_CatchAllCannotBeOptional");
/// <summary>
/// A catch-all parameter can only appear as the last segment of the route template.
/// </summary>
internal static string TemplateRoute_CatchAllMustBeLast
{
get => GetString("TemplateRoute_CatchAllMustBeLast");
}
/// <summary>
/// A catch-all parameter can only appear as the last segment of the route template.
/// </summary>
internal static string FormatTemplateRoute_CatchAllMustBeLast()
=> GetString("TemplateRoute_CatchAllMustBeLast");
/// <summary>
/// The literal section '{0}' is invalid. Literal sections cannot contain the '?' character.
/// </summary>
internal static string TemplateRoute_InvalidLiteral
{
get => GetString("TemplateRoute_InvalidLiteral");
}
/// <summary>
/// The literal section '{0}' is invalid. Literal sections cannot contain the '?' character.
/// </summary>
internal static string FormatTemplateRoute_InvalidLiteral(object p0)
=> string.Format(CultureInfo.CurrentCulture, GetString("TemplateRoute_InvalidLiteral"), p0);
/// <summary>
/// The route parameter name '{0}' 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.
/// </summary>
internal static string TemplateRoute_InvalidParameterName
{
get => GetString("TemplateRoute_InvalidParameterName");
}
/// <summary>
/// The route parameter name '{0}' 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.
/// </summary>
internal static string FormatTemplateRoute_InvalidParameterName(object p0)
=> string.Format(CultureInfo.CurrentCulture, GetString("TemplateRoute_InvalidParameterName"), p0);
/// <summary>
/// The route template cannot start with a '/' or '~' character.
/// </summary>
internal static string TemplateRoute_InvalidRouteTemplate
{
get => GetString("TemplateRoute_InvalidRouteTemplate");
}
/// <summary>
/// The route template cannot start with a '/' or '~' character.
/// </summary>
internal static string FormatTemplateRoute_InvalidRouteTemplate()
=> GetString("TemplateRoute_InvalidRouteTemplate");
/// <summary>
/// There is an incomplete parameter in the route template. Check that each '{' character has a matching '}' character.
/// </summary>
internal static string TemplateRoute_MismatchedParameter
{
get => GetString("TemplateRoute_MismatchedParameter");
}
/// <summary>
/// There is an incomplete parameter in the route template. Check that each '{' character has a matching '}' character.
/// </summary>
internal static string FormatTemplateRoute_MismatchedParameter()
=> GetString("TemplateRoute_MismatchedParameter");
/// <summary>
/// An optional parameter cannot have default value.
/// </summary>
internal static string TemplateRoute_OptionalCannotHaveDefaultValue
{
get => GetString("TemplateRoute_OptionalCannotHaveDefaultValue");
}
/// <summary>
/// An optional parameter cannot have default value.
/// </summary>
internal static string FormatTemplateRoute_OptionalCannotHaveDefaultValue()
=> GetString("TemplateRoute_OptionalCannotHaveDefaultValue");
/// <summary>
/// In the segment '{0}', the optional parameter '{1}' is preceded by an invalid segment '{2}'. Only a period (.) can precede an optional parameter.
/// </summary>
internal static string TemplateRoute_OptionalParameterCanbBePrecededByPeriod
{
get => GetString("TemplateRoute_OptionalParameterCanbBePrecededByPeriod");
}
/// <summary>
/// In the segment '{0}', the optional parameter '{1}' is preceded by an invalid segment '{2}'. Only a period (.) can precede an optional parameter.
/// </summary>
internal static string FormatTemplateRoute_OptionalParameterCanbBePrecededByPeriod(object p0, object p1, object p2)
=> string.Format(CultureInfo.CurrentCulture, GetString("TemplateRoute_OptionalParameterCanbBePrecededByPeriod"), p0, p1, p2);
/// <summary>
/// An optional parameter must be at the end of the segment. In the segment '{0}', optional parameter '{1}' is followed by '{2}'.
/// </summary>
internal static string TemplateRoute_OptionalParameterHasTobeTheLast
{
get => GetString("TemplateRoute_OptionalParameterHasTobeTheLast");
}
/// <summary>
/// An optional parameter must be at the end of the segment. In the segment '{0}', optional parameter '{1}' is followed by '{2}'.
/// </summary>
internal static string FormatTemplateRoute_OptionalParameterHasTobeTheLast(object p0, object p1, object p2)
=> string.Format(CultureInfo.CurrentCulture, GetString("TemplateRoute_OptionalParameterHasTobeTheLast"), p0, p1, p2);
/// <summary>
/// The route parameter name '{0}' appears more than one time in the route template.
/// </summary>
internal static string TemplateRoute_RepeatedParameter
{
get => GetString("TemplateRoute_RepeatedParameter");
}
/// <summary>
/// The route parameter name '{0}' appears more than one time in the route template.
/// </summary>
internal static string FormatTemplateRoute_RepeatedParameter(object p0)
=> string.Format(CultureInfo.CurrentCulture, GetString("TemplateRoute_RepeatedParameter"), p0);
/// <summary>
/// In a route parameter, '{' and '}' must be escaped with '{{' and '}}'.
/// </summary>
internal static string TemplateRoute_UnescapedBrace
{
get => GetString("TemplateRoute_UnescapedBrace");
}
/// <summary>
/// In a route parameter, '{' and '}' must be escaped with '{{' and '}}'.
/// </summary>
internal static string FormatTemplateRoute_UnescapedBrace()
=> GetString("TemplateRoute_UnescapedBrace");
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);

View File

@ -121,4 +121,46 @@
<value>Multiple endpoints matched. The following endpoints matched the request:{0}{0}{1}</value>
<comment>0 is the newline - 1 is a newline separate list of action display names</comment>
</data>
<data name="TemplateRoute_CannotHaveCatchAllInMultiSegment" xml:space="preserve">
<value>A path segment that contains more than one section, such as a literal section or a parameter, cannot contain a catch-all parameter.</value>
</data>
<data name="TemplateRoute_CannotHaveConsecutiveParameters" xml:space="preserve">
<value>A path segment cannot contain two consecutive parameters. They must be separated by a '/' or by a literal string.</value>
</data>
<data name="TemplateRoute_CannotHaveConsecutiveSeparators" xml:space="preserve">
<value>The route template separator character '/' cannot appear consecutively. It must be separated by either a parameter or a literal value.</value>
</data>
<data name="TemplateRoute_CatchAllCannotBeOptional" xml:space="preserve">
<value>A catch-all parameter cannot be marked optional.</value>
</data>
<data name="TemplateRoute_CatchAllMustBeLast" xml:space="preserve">
<value>A catch-all parameter can only appear as the last segment of the route template.</value>
</data>
<data name="TemplateRoute_InvalidLiteral" xml:space="preserve">
<value>The literal section '{0}' is invalid. Literal sections cannot contain the '?' character.</value>
</data>
<data name="TemplateRoute_InvalidParameterName" xml:space="preserve">
<value>The route parameter name '{0}' 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.</value>
</data>
<data name="TemplateRoute_InvalidRouteTemplate" xml:space="preserve">
<value>The route template cannot start with a '/' or '~' character.</value>
</data>
<data name="TemplateRoute_MismatchedParameter" xml:space="preserve">
<value>There is an incomplete parameter in the route template. Check that each '{' character has a matching '}' character.</value>
</data>
<data name="TemplateRoute_OptionalCannotHaveDefaultValue" xml:space="preserve">
<value>An optional parameter cannot have default value.</value>
</data>
<data name="TemplateRoute_OptionalParameterCanbBePrecededByPeriod" xml:space="preserve">
<value>In the segment '{0}', the optional parameter '{1}' is preceded by an invalid segment '{2}'. Only a period (.) can precede an optional parameter.</value>
</data>
<data name="TemplateRoute_OptionalParameterHasTobeTheLast" xml:space="preserve">
<value>An optional parameter must be at the end of the segment. In the segment '{0}', optional parameter '{1}' is followed by '{2}'.</value>
</data>
<data name="TemplateRoute_RepeatedParameter" xml:space="preserve">
<value>The route parameter name '{0}' appears more than one time in the route template.</value>
</data>
<data name="TemplateRoute_UnescapedBrace" xml:space="preserve">
<value>In a route parameter, '{' and '}' must be escaped with '{{' and '}}'.</value>
</data>
</root>

View File

@ -0,0 +1,82 @@
// 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.Dispatcher
{
[DebuggerDisplay("{DebuggerToString()}")]
public class RouteTemplate
{
private const string SeparatorString = "/";
public RouteTemplate(string template, List<TemplateSegment> segments)
{
if (segments == null)
{
throw new ArgumentNullException(nameof(segments));
}
TemplateText = template;
Segments = segments;
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 string TemplateText { get; }
public IList<TemplatePart> Parameters { get; }
public IList<TemplateSegment> Segments { get; }
public TemplateSegment GetSegment(int index)
{
if (index < 0)
{
throw new IndexOutOfRangeException();
}
return index >= Segments.Count ? null : Segments[index];
}
private string DebuggerToString()
{
return string.Join(SeparatorString, Segments.Select(s => s.DebuggerToString()));
}
/// <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 TemplatePart GetParameter(string 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;
}
}
}

View File

@ -0,0 +1,526 @@
// 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;
namespace Microsoft.AspNetCore.Dispatcher
{
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;
}
if (IsInvalidRouteTemplate(routeTemplate))
{
throw new ArgumentException(Resources.TemplateRoute_InvalidRouteTemplate, nameof(routeTemplate));
}
var context = new TemplateParserContext(routeTemplate);
var segments = new List<TemplateSegment>();
while (context.Next())
{
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));
}
}
}
if (IsAllValid(context, segments))
{
return new RouteTemplate(routeTemplate, segments);
}
else
{
throw new ArgumentException(context.Error, nameof(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;
}
}
}
}
}

View File

@ -0,0 +1,67 @@
// 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.Dispatcher
{
[DebuggerDisplay("{DebuggerToString()}")]
public class TemplatePart
{
public static TemplatePart CreateLiteral(string text)
{
return new TemplatePart()
{
IsLiteral = true,
Text = text,
};
}
public static TemplatePart CreateParameter(string name,
bool isCatchAll,
bool isOptional,
object defaultValue,
IEnumerable<InlineConstraint> inlineConstraints)
{
if (name == null)
{
throw new ArgumentNullException(nameof(name));
}
return new TemplatePart()
{
IsParameter = true,
Name = name,
IsCatchAll = isCatchAll,
IsOptional = isOptional,
DefaultValue = defaultValue,
InlineConstraints = inlineConstraints ?? Enumerable.Empty<InlineConstraint>(),
};
}
public bool IsCatchAll { get; private set; }
public bool IsLiteral { get; private set; }
public bool IsParameter { get; private set; }
public bool IsOptional { get; private set; }
public bool IsOptionalSeperator { get; set; }
public string Name { get; private set; }
public string Text { get; private set; }
public object DefaultValue { get; private set; }
public IEnumerable<InlineConstraint> InlineConstraints { get; private set; }
internal string DebuggerToString()
{
if (IsParameter)
{
return "{" + (IsCatchAll ? "*" : string.Empty) + Name + (IsOptional ? "?" : string.Empty) + "}";
}
else
{
return Text;
}
}
}
}

View File

@ -0,0 +1,22 @@
// 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.Dispatcher
{
[DebuggerDisplay("{DebuggerToString()}")]
public class TemplateSegment
{
public bool IsSimple => Parts.Count == 1;
public List<TemplatePart> Parts { get; } = new List<TemplatePart>();
internal string DebuggerToString()
{
return string.Join(string.Empty, Parts.Select(p => p.DebuggerToString()));
}
}
}

View File

@ -43,7 +43,7 @@ namespace Microsoft.AspNetCore.Routing.Dispatcher
throw new InvalidOperationException("Can't find address");
}
var binder = new TemplateBinder(_urlEncoder, _pool, TemplateParser.Parse(address.Template), new RouteValueDictionary());
var binder = new TemplateBinder(_urlEncoder, _pool, Template.TemplateParser.Parse(address.Template), new RouteValueDictionary());
var feature = httpContext.Features.Get<IDispatcherFeature>();
var result = binder.GetValues(feature.Values.AsRouteValueDictionary(), new RouteValueDictionary(values));

View File

@ -146,7 +146,7 @@ namespace Microsoft.AspNetCore.Routing.Dispatcher
var entries = new List<InboundRouteEntry>();
foreach (var group in groups)
{
var template = TemplateParser.Parse(group.Key.RouteTemplate);
var template = Template.TemplateParser.Parse(group.Key.RouteTemplate);
var defaults = new RouteValueDictionary();
for (var i = 0; i < template.Parameters.Count; i++)

View File

@ -1,8 +1,7 @@
// 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 Microsoft.AspNetCore.Routing.Template;
namespace Microsoft.AspNetCore.Routing
@ -16,228 +15,8 @@ namespace Microsoft.AspNetCore.Routing
throw new ArgumentNullException(nameof(routeParameter));
}
if (routeParameter.Length == 0)
{
return TemplatePart.CreateParameter(
name: string.Empty,
isCatchAll: false,
isOptional: false,
defaultValue: null,
inlineConstraints: null);
}
var startIndex = 0;
var endIndex = routeParameter.Length - 1;
var isCatchAll = false;
var isOptional = false;
if (routeParameter[0] == '*')
{
isCatchAll = true;
startIndex++;
}
if (routeParameter[endIndex] == '?')
{
isOptional = true;
endIndex--;
}
var currentIndex = startIndex;
// Parse parameter name
var parameterName = string.Empty;
while (currentIndex <= endIndex)
{
var currentChar = routeParameter[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 = routeParameter.Substring(startIndex, currentIndex - startIndex);
// Roll the index back and move to the constraint parsing stage.
currentIndex--;
break;
}
else if (currentIndex == endIndex)
{
parameterName = routeParameter.Substring(startIndex, currentIndex - startIndex + 1);
}
currentIndex++;
}
var parseResults = ParseConstraints(routeParameter, currentIndex, endIndex);
currentIndex = parseResults.CurrentIndex;
string defaultValue = null;
if (currentIndex <= endIndex &&
routeParameter[currentIndex] == '=')
{
defaultValue = routeParameter.Substring(currentIndex + 1, endIndex - currentIndex);
}
return TemplatePart.CreateParameter(parameterName,
isCatchAll,
isOptional,
defaultValue,
parseResults.Constraints);
}
private static ConstraintParseResults ParseConstraints(
string routeParameter,
int currentIndex,
int endIndex)
{
var inlineConstraints = new List<InlineConstraint>();
var state = ParseState.Start;
var startIndex = currentIndex;
do
{
var currentChar = currentIndex > endIndex ? null : (char?)routeParameter[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 = routeParameter.Substring(startIndex, currentIndex - startIndex);
inlineConstraints.Add(new InlineConstraint(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?)routeParameter[currentIndex + 1];
switch (nextChar)
{
case null:
state = ParseState.End;
constraintText = routeParameter.Substring(startIndex, currentIndex - startIndex + 1);
inlineConstraints.Add(new InlineConstraint(constraintText));
break;
case ':':
state = ParseState.Start;
constraintText = routeParameter.Substring(startIndex, currentIndex - startIndex + 1);
inlineConstraints.Add(new InlineConstraint(constraintText));
startIndex = currentIndex + 1;
break;
case '=':
state = ParseState.End;
constraintText = routeParameter.Substring(startIndex, currentIndex - startIndex + 1);
inlineConstraints.Add(new InlineConstraint(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 = routeParameter.IndexOf(')', currentIndex + 1);
if (indexOfClosingParantheses == -1)
{
constraintText = routeParameter.Substring(startIndex, currentIndex - startIndex);
inlineConstraints.Add(new InlineConstraint(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 = routeParameter.Substring(startIndex, currentIndex - startIndex);
inlineConstraints.Add(new InlineConstraint(constraintText));
break;
case ':':
constraintText = routeParameter.Substring(startIndex, currentIndex - startIndex);
inlineConstraints.Add(new InlineConstraint(constraintText));
startIndex = currentIndex + 1;
break;
case '(':
state = ParseState.InsideParenthesis;
break;
case '=':
state = ParseState.End;
constraintText = routeParameter.Substring(startIndex, currentIndex - startIndex);
inlineConstraints.Add(new InlineConstraint(constraintText));
currentIndex--;
break;
}
break;
}
currentIndex++;
} while (state != ParseState.End);
return new ConstraintParseResults
{
CurrentIndex = currentIndex,
Constraints = inlineConstraints
};
}
private enum ParseState
{
Start,
ParsingName,
InsideParenthesis,
End
}
private struct ConstraintParseResults
{
public int CurrentIndex;
public IEnumerable<InlineConstraint> Constraints;
var inner = AspNetCore.Dispatcher.InlineRouteParameterParser.ParseRouteParameter(routeParameter);
return new TemplatePart(inner);
}
}
}
}

View File

@ -122,174 +122,6 @@ namespace Microsoft.AspNetCore.Routing
internal static string FormatDefaultInlineConstraintResolver_TypeNotConstraint(object p0, object p1, object p2)
=> string.Format(CultureInfo.CurrentCulture, GetString("DefaultInlineConstraintResolver_TypeNotConstraint"), p0, p1, p2);
/// <summary>
/// A path segment that contains more than one section, such as a literal section or a parameter, cannot contain a catch-all parameter.
/// </summary>
internal static string TemplateRoute_CannotHaveCatchAllInMultiSegment
{
get => GetString("TemplateRoute_CannotHaveCatchAllInMultiSegment");
}
/// <summary>
/// A path segment that contains more than one section, such as a literal section or a parameter, cannot contain a catch-all parameter.
/// </summary>
internal static string FormatTemplateRoute_CannotHaveCatchAllInMultiSegment()
=> GetString("TemplateRoute_CannotHaveCatchAllInMultiSegment");
/// <summary>
/// The route parameter '{0}' 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.
/// </summary>
internal static string TemplateRoute_CannotHaveDefaultValueSpecifiedInlineAndExplicitly
{
get => GetString("TemplateRoute_CannotHaveDefaultValueSpecifiedInlineAndExplicitly");
}
/// <summary>
/// The route parameter '{0}' 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.
/// </summary>
internal static string FormatTemplateRoute_CannotHaveDefaultValueSpecifiedInlineAndExplicitly(object p0)
=> string.Format(CultureInfo.CurrentCulture, GetString("TemplateRoute_CannotHaveDefaultValueSpecifiedInlineAndExplicitly"), p0);
/// <summary>
/// A path segment cannot contain two consecutive parameters. They must be separated by a '/' or by a literal string.
/// </summary>
internal static string TemplateRoute_CannotHaveConsecutiveParameters
{
get => GetString("TemplateRoute_CannotHaveConsecutiveParameters");
}
/// <summary>
/// A path segment cannot contain two consecutive parameters. They must be separated by a '/' or by a literal string.
/// </summary>
internal static string FormatTemplateRoute_CannotHaveConsecutiveParameters()
=> GetString("TemplateRoute_CannotHaveConsecutiveParameters");
/// <summary>
/// The route template separator character '/' cannot appear consecutively. It must be separated by either a parameter or a literal value.
/// </summary>
internal static string TemplateRoute_CannotHaveConsecutiveSeparators
{
get => GetString("TemplateRoute_CannotHaveConsecutiveSeparators");
}
/// <summary>
/// The route template separator character '/' cannot appear consecutively. It must be separated by either a parameter or a literal value.
/// </summary>
internal static string FormatTemplateRoute_CannotHaveConsecutiveSeparators()
=> GetString("TemplateRoute_CannotHaveConsecutiveSeparators");
/// <summary>
/// A catch-all parameter cannot be marked optional.
/// </summary>
internal static string TemplateRoute_CatchAllCannotBeOptional
{
get => GetString("TemplateRoute_CatchAllCannotBeOptional");
}
/// <summary>
/// A catch-all parameter cannot be marked optional.
/// </summary>
internal static string FormatTemplateRoute_CatchAllCannotBeOptional()
=> GetString("TemplateRoute_CatchAllCannotBeOptional");
/// <summary>
/// An optional parameter cannot have default value.
/// </summary>
internal static string TemplateRoute_OptionalCannotHaveDefaultValue
{
get => GetString("TemplateRoute_OptionalCannotHaveDefaultValue");
}
/// <summary>
/// An optional parameter cannot have default value.
/// </summary>
internal static string FormatTemplateRoute_OptionalCannotHaveDefaultValue()
=> GetString("TemplateRoute_OptionalCannotHaveDefaultValue");
/// <summary>
/// A catch-all parameter can only appear as the last segment of the route template.
/// </summary>
internal static string TemplateRoute_CatchAllMustBeLast
{
get => GetString("TemplateRoute_CatchAllMustBeLast");
}
/// <summary>
/// A catch-all parameter can only appear as the last segment of the route template.
/// </summary>
internal static string FormatTemplateRoute_CatchAllMustBeLast()
=> GetString("TemplateRoute_CatchAllMustBeLast");
/// <summary>
/// The literal section '{0}' is invalid. Literal sections cannot contain the '?' character.
/// </summary>
internal static string TemplateRoute_InvalidLiteral
{
get => GetString("TemplateRoute_InvalidLiteral");
}
/// <summary>
/// The literal section '{0}' is invalid. Literal sections cannot contain the '?' character.
/// </summary>
internal static string FormatTemplateRoute_InvalidLiteral(object p0)
=> string.Format(CultureInfo.CurrentCulture, GetString("TemplateRoute_InvalidLiteral"), p0);
/// <summary>
/// The route parameter name '{0}' 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.
/// </summary>
internal static string TemplateRoute_InvalidParameterName
{
get => GetString("TemplateRoute_InvalidParameterName");
}
/// <summary>
/// The route parameter name '{0}' 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.
/// </summary>
internal static string FormatTemplateRoute_InvalidParameterName(object p0)
=> string.Format(CultureInfo.CurrentCulture, GetString("TemplateRoute_InvalidParameterName"), p0);
/// <summary>
/// The route template cannot start with a '/' or '~' character.
/// </summary>
internal static string TemplateRoute_InvalidRouteTemplate
{
get => GetString("TemplateRoute_InvalidRouteTemplate");
}
/// <summary>
/// The route template cannot start with a '/' or '~' character.
/// </summary>
internal static string FormatTemplateRoute_InvalidRouteTemplate()
=> GetString("TemplateRoute_InvalidRouteTemplate");
/// <summary>
/// There is an incomplete parameter in the route template. Check that each '{' character has a matching '}' character.
/// </summary>
internal static string TemplateRoute_MismatchedParameter
{
get => GetString("TemplateRoute_MismatchedParameter");
}
/// <summary>
/// There is an incomplete parameter in the route template. Check that each '{' character has a matching '}' character.
/// </summary>
internal static string FormatTemplateRoute_MismatchedParameter()
=> GetString("TemplateRoute_MismatchedParameter");
/// <summary>
/// The route parameter name '{0}' appears more than one time in the route template.
/// </summary>
internal static string TemplateRoute_RepeatedParameter
{
get => GetString("TemplateRoute_RepeatedParameter");
}
/// <summary>
/// The route parameter name '{0}' appears more than one time in the route template.
/// </summary>
internal static string FormatTemplateRoute_RepeatedParameter(object p0)
=> string.Format(CultureInfo.CurrentCulture, GetString("TemplateRoute_RepeatedParameter"), p0);
/// <summary>
/// The constraint entry '{0}' - '{1}' on the route '{2}' must have a string value or be of a type which implements '{3}'.
/// </summary>
@ -318,48 +150,6 @@ namespace Microsoft.AspNetCore.Routing
internal static string FormatRouteConstraintBuilder_CouldNotResolveConstraint(object p0, object p1, object p2, object p3)
=> string.Format(CultureInfo.CurrentCulture, GetString("RouteConstraintBuilder_CouldNotResolveConstraint"), p0, p1, p2, p3);
/// <summary>
/// In a route parameter, '{' and '}' must be escaped with '{{' and '}}'.
/// </summary>
internal static string TemplateRoute_UnescapedBrace
{
get => GetString("TemplateRoute_UnescapedBrace");
}
/// <summary>
/// In a route parameter, '{' and '}' must be escaped with '{{' and '}}'.
/// </summary>
internal static string FormatTemplateRoute_UnescapedBrace()
=> GetString("TemplateRoute_UnescapedBrace");
/// <summary>
/// In the segment '{0}', the optional parameter '{1}' is preceded by an invalid segment '{2}'. Only a period (.) can precede an optional parameter.
/// </summary>
internal static string TemplateRoute_OptionalParameterCanbBePrecededByPeriod
{
get => GetString("TemplateRoute_OptionalParameterCanbBePrecededByPeriod");
}
/// <summary>
/// In the segment '{0}', the optional parameter '{1}' is preceded by an invalid segment '{2}'. Only a period (.) can precede an optional parameter.
/// </summary>
internal static string FormatTemplateRoute_OptionalParameterCanbBePrecededByPeriod(object p0, object p1, object p2)
=> string.Format(CultureInfo.CurrentCulture, GetString("TemplateRoute_OptionalParameterCanbBePrecededByPeriod"), p0, p1, p2);
/// <summary>
/// An optional parameter must be at the end of the segment. In the segment '{0}', optional parameter '{1}' is followed by '{2}'.
/// </summary>
internal static string TemplateRoute_OptionalParameterHasTobeTheLast
{
get => GetString("TemplateRoute_OptionalParameterHasTobeTheLast");
}
/// <summary>
/// An optional parameter must be at the end of the segment. In the segment '{0}', optional parameter '{1}' is followed by '{2}'.
/// </summary>
internal static string FormatTemplateRoute_OptionalParameterHasTobeTheLast(object p0, object p1, object p2)
=> string.Format(CultureInfo.CurrentCulture, GetString("TemplateRoute_OptionalParameterHasTobeTheLast"), p0, p1, p2);
/// <summary>
/// Two or more routes named '{0}' have different templates.
/// </summary>
@ -388,20 +178,6 @@ namespace Microsoft.AspNetCore.Routing
internal static string FormatUnableToFindServices(object p0, object p1, object p2)
=> string.Format(CultureInfo.CurrentCulture, GetString("UnableToFindServices"), p0, p1, p2);
/// <summary>
/// An error occurred while creating the route with name '{0}' and template '{1}'.
/// </summary>
internal static string TemplateRoute_Exception
{
get => GetString("TemplateRoute_Exception");
}
/// <summary>
/// An error occurred while creating the route with name '{0}' and template '{1}'.
/// </summary>
internal static string FormatTemplateRoute_Exception(object p0, object p1)
=> string.Format(CultureInfo.CurrentCulture, GetString("TemplateRoute_Exception"), p0, p1);
/// <summary>
/// The '{0}' has no '{1}'. '{2}' requires a dispatcher.
/// </summary>
@ -416,6 +192,34 @@ namespace Microsoft.AspNetCore.Routing
internal static string FormatDispatcherFeatureIsRequired(object p0, object p1, object p2)
=> string.Format(CultureInfo.CurrentCulture, GetString("DispatcherFeatureIsRequired"), p0, p1, p2);
/// <summary>
/// The route parameter '{0}' 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.
/// </summary>
internal static string TemplateRoute_CannotHaveDefaultValueSpecifiedInlineAndExplicitly
{
get => GetString("TemplateRoute_CannotHaveDefaultValueSpecifiedInlineAndExplicitly");
}
/// <summary>
/// The route parameter '{0}' 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.
/// </summary>
internal static string FormatTemplateRoute_CannotHaveDefaultValueSpecifiedInlineAndExplicitly(object p0)
=> string.Format(CultureInfo.CurrentCulture, GetString("TemplateRoute_CannotHaveDefaultValueSpecifiedInlineAndExplicitly"), p0);
/// <summary>
/// An error occurred while creating the route with name '{0}' and template '{1}'.
/// </summary>
internal static string TemplateRoute_Exception
{
get => GetString("TemplateRoute_Exception");
}
/// <summary>
/// An error occurred while creating the route with name '{0}' and template '{1}'.
/// </summary>
internal static string FormatTemplateRoute_Exception(object p0, object p1)
=> string.Format(CultureInfo.CurrentCulture, GetString("TemplateRoute_Exception"), p0, p1);
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);

View File

@ -141,67 +141,25 @@
<data name="DefaultInlineConstraintResolver_TypeNotConstraint" xml:space="preserve">
<value>The constraint type '{0}' which is mapped to constraint key '{1}' must implement the '{2}' interface.</value>
</data>
<data name="TemplateRoute_CannotHaveCatchAllInMultiSegment" xml:space="preserve">
<value>A path segment that contains more than one section, such as a literal section or a parameter, cannot contain a catch-all parameter.</value>
</data>
<data name="TemplateRoute_CannotHaveDefaultValueSpecifiedInlineAndExplicitly" xml:space="preserve">
<value>The route parameter '{0}' 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.</value>
</data>
<data name="TemplateRoute_CannotHaveConsecutiveParameters" xml:space="preserve">
<value>A path segment cannot contain two consecutive parameters. They must be separated by a '/' or by a literal string.</value>
</data>
<data name="TemplateRoute_CannotHaveConsecutiveSeparators" xml:space="preserve">
<value>The route template separator character '/' cannot appear consecutively. It must be separated by either a parameter or a literal value.</value>
</data>
<data name="TemplateRoute_CatchAllCannotBeOptional" xml:space="preserve">
<value>A catch-all parameter cannot be marked optional.</value>
</data>
<data name="TemplateRoute_OptionalCannotHaveDefaultValue" xml:space="preserve">
<value>An optional parameter cannot have default value.</value>
</data>
<data name="TemplateRoute_CatchAllMustBeLast" xml:space="preserve">
<value>A catch-all parameter can only appear as the last segment of the route template.</value>
</data>
<data name="TemplateRoute_InvalidLiteral" xml:space="preserve">
<value>The literal section '{0}' is invalid. Literal sections cannot contain the '?' character.</value>
</data>
<data name="TemplateRoute_InvalidParameterName" xml:space="preserve">
<value>The route parameter name '{0}' 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.</value>
</data>
<data name="TemplateRoute_InvalidRouteTemplate" xml:space="preserve">
<value>The route template cannot start with a '/' or '~' character.</value>
</data>
<data name="TemplateRoute_MismatchedParameter" xml:space="preserve">
<value>There is an incomplete parameter in the route template. Check that each '{' character has a matching '}' character.</value>
</data>
<data name="TemplateRoute_RepeatedParameter" xml:space="preserve">
<value>The route parameter name '{0}' appears more than one time in the route template.</value>
</data>
<data name="RouteConstraintBuilder_ValidationMustBeStringOrCustomConstraint" xml:space="preserve">
<value>The constraint entry '{0}' - '{1}' on the route '{2}' must have a string value or be of a type which implements '{3}'.</value>
</data>
<data name="RouteConstraintBuilder_CouldNotResolveConstraint" xml:space="preserve">
<value>The constraint entry '{0}' - '{1}' on the route '{2}' could not be resolved by the constraint resolver of type '{3}'.</value>
</data>
<data name="TemplateRoute_UnescapedBrace" xml:space="preserve">
<value>In a route parameter, '{' and '}' must be escaped with '{{' and '}}'.</value>
</data>
<data name="TemplateRoute_OptionalParameterCanbBePrecededByPeriod" xml:space="preserve">
<value>In the segment '{0}', the optional parameter '{1}' is preceded by an invalid segment '{2}'. Only a period (.) can precede an optional parameter.</value>
</data>
<data name="TemplateRoute_OptionalParameterHasTobeTheLast" xml:space="preserve">
<value>An optional parameter must be at the end of the segment. In the segment '{0}', optional parameter '{1}' is followed by '{2}'.</value>
</data>
<data name="AttributeRoute_DifferentLinkGenerationEntries_SameName" xml:space="preserve">
<value>Two or more routes named '{0}' have different templates.</value>
</data>
<data name="UnableToFindServices" xml:space="preserve">
<value>Unable to find the required services. Please add all the required services by calling '{0}.{1}' inside the call to '{2}' in the application startup code.</value>
</data>
<data name="TemplateRoute_Exception" xml:space="preserve">
<value>An error occurred while creating the route with name '{0}' and template '{1}'.</value>
</data>
<data name="DispatcherFeatureIsRequired" xml:space="preserve">
<value>The '{0}' has no '{1}'. '{2}' requires a dispatcher.</value>
</data>
<data name="TemplateRoute_CannotHaveDefaultValueSpecifiedInlineAndExplicitly" xml:space="preserve">
<value>The route parameter '{0}' 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.</value>
</data>
<data name="TemplateRoute_Exception" xml:space="preserve">
<value>An error occurred while creating the route with name '{0}' and template '{1}'.</value>
</data>
</root>

View File

@ -24,6 +24,16 @@ namespace Microsoft.AspNetCore.Routing.Template
Constraint = constraint;
}
public InlineConstraint(AspNetCore.Dispatcher.InlineConstraint constraint)
{
if (constraint == null)
{
throw new ArgumentNullException(nameof(constraint));
}
Constraint = constraint.Constraint;
}
/// <summary>
/// Gets the constraint text.
/// </summary>

View File

@ -13,6 +13,25 @@ namespace Microsoft.AspNetCore.Routing.Template
{
private const string SeparatorString = "/";
public RouteTemplate(AspNetCore.Dispatcher.RouteTemplate routeTemplate)
{
TemplateText = routeTemplate.TemplateText;
Segments = new List<TemplateSegment>(routeTemplate.Segments.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)

View File

@ -1,526 +1,21 @@
// 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;
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);
}
if (IsInvalidRouteTemplate(routeTemplate))
{
throw new ArgumentException(Resources.TemplateRoute_InvalidRouteTemplate, nameof(routeTemplate));
}
var context = new TemplateParserContext(routeTemplate);
var segments = new List<TemplateSegment>();
while (context.Next())
{
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));
}
}
}
if (IsAllValid(context, segments))
{
return new RouteTemplate(routeTemplate, segments);
}
else
{
throw new ArgumentException(context.Error, nameof(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;
}
}
var inner = AspNetCore.Dispatcher.TemplateParser.Parse(routeTemplate);
return new RouteTemplate(inner);
}
}
}

View File

@ -11,6 +11,23 @@ namespace Microsoft.AspNetCore.Routing.Template
[DebuggerDisplay("{DebuggerToString()}")]
public class TemplatePart
{
public TemplatePart()
{
}
public TemplatePart(AspNetCore.Dispatcher.TemplatePart templatePart)
{
IsCatchAll = templatePart.IsCatchAll;
IsLiteral = templatePart.IsLiteral;
IsOptional = templatePart.IsOptional;
IsOptionalSeperator = templatePart.IsOptionalSeperator;
IsParameter = templatePart.IsParameter;
Name = templatePart.Name;
Text = templatePart.Text;
DefaultValue = templatePart.DefaultValue;
InlineConstraints = templatePart.InlineConstraints?.Select(p => new InlineConstraint(p));
}
public static TemplatePart CreateLiteral(string text)
{
return new TemplatePart()

View File

@ -10,6 +10,15 @@ namespace Microsoft.AspNetCore.Routing.Template
[DebuggerDisplay("{DebuggerToString()}")]
public class TemplateSegment
{
public TemplateSegment()
{
}
public TemplateSegment(AspNetCore.Dispatcher.TemplateSegment templateSegment)
{
Parts = new List<TemplatePart>(templateSegment.Parts.Select(s => new TemplatePart(s)));
}
public bool IsSimple => Parts.Count == 1;
public List<TemplatePart> Parts { get; } = new List<TemplatePart>();

View File

@ -0,0 +1,954 @@
// 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.Dispatcher
{
public class InlineRouteParameterParserTests
{
[Theory]
[InlineData("=")]
[InlineData(":")]
public void ParseRouteParameter_WithoutADefaultValue(string parameterName)
{
// Arrange & Act
var templatePart = ParseParameter(parameterName);
// Assert
Assert.Equal(parameterName, templatePart.Name);
Assert.Null(templatePart.DefaultValue);
Assert.Empty(templatePart.InlineConstraints);
}
[Fact]
public void ParseRouteParameter_WithEmptyDefaultValue()
{
// Arrange & Act
var templatePart = ParseParameter("param=");
// Assert
Assert.Equal("param", templatePart.Name);
Assert.Equal("", templatePart.DefaultValue);
Assert.Empty(templatePart.InlineConstraints);
}
[Fact]
public void ParseRouteParameter_WithoutAConstraintName()
{
// Arrange & Act
var templatePart = ParseParameter("param:");
// Assert
Assert.Equal("param", templatePart.Name);
Assert.Null(templatePart.DefaultValue);
var constraint = Assert.Single(templatePart.InlineConstraints);
Assert.Empty(constraint.Constraint);
}
[Fact]
public void ParseRouteParameter_WithoutAConstraintNameOrParameterName()
{
// Arrange & Act
var templatePart = ParseParameter("param:=");
// Assert
Assert.Equal("param", templatePart.Name);
Assert.Equal("", templatePart.DefaultValue);
var constraint = Assert.Single(templatePart.InlineConstraints);
Assert.Empty(constraint.Constraint);
}
[Fact]
public void ParseRouteParameter_WithADefaultValueContainingConstraintSeparator()
{
// Arrange & Act
var templatePart = ParseParameter("param=:");
// Assert
Assert.Equal("param", templatePart.Name);
Assert.Equal(":", templatePart.DefaultValue);
Assert.Empty(templatePart.InlineConstraints);
}
[Fact]
public void ParseRouteParameter_ConstraintAndDefault_ParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter("param:int=111111");
// Assert
Assert.Equal("param", templatePart.Name);
Assert.Equal("111111", templatePart.DefaultValue);
var constraint = Assert.Single(templatePart.InlineConstraints);
Assert.Equal("int", constraint.Constraint);
}
[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.DefaultValue);
var constraint = Assert.Single(templatePart.InlineConstraints);
Assert.Equal(@"test(\d+)", constraint.Constraint);
}
[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.InlineConstraints);
Assert.Equal("int", constraint.Constraint);
}
[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.DefaultValue);
Assert.True(templatePart.IsOptional);
var constraint = Assert.Single(templatePart.InlineConstraints);
Assert.Equal("int", constraint.Constraint);
}
[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.DefaultValue);
Assert.True(templatePart.IsOptional);
var constraint = Assert.Single(templatePart.InlineConstraints);
Assert.Equal("int", constraint.Constraint);
}
[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.InlineConstraints);
Assert.Equal(@"test(\d+)", constraint.Constraint);
}
[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.DefaultValue);
var constraint = Assert.Single(templatePart.InlineConstraints);
Assert.Equal(@"test(\d+)", constraint.Constraint);
}
[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.InlineConstraints,
constraint => Assert.Equal(@"test(d+)", constraint.Constraint),
constraint => Assert.Equal(@"test(w+)", constraint.Constraint));
}
[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.InlineConstraints,
constraint => Assert.Empty(constraint.Constraint),
constraint => Assert.Equal(@"test(d+)", constraint.Constraint),
constraint => Assert.Empty(constraint.Constraint),
constraint => Assert.Equal(@"test(w+)", constraint.Constraint));
}
[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.InlineConstraints,
constraint => Assert.Equal(@"test(\d+)", constraint.Constraint),
constraint => Assert.Equal(@"test(\w:+)", constraint.Constraint));
}
[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.DefaultValue);
Assert.Collection(templatePart.InlineConstraints,
constraint => Assert.Equal(@"test(\d+)", constraint.Constraint),
constraint => Assert.Equal(@"test(\w+)", constraint.Constraint));
}
[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.DefaultValue);
Assert.Collection(templatePart.InlineConstraints,
constraint => Assert.Equal(@"test(\d+)", constraint.Constraint),
constraint => Assert.Empty(constraint.Constraint),
constraint => Assert.Equal(@"test(\w+)", constraint.Constraint));
}
[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.DefaultValue);
var constraint = Assert.Single(templatePart.InlineConstraints);
Assert.Equal("length(6)", constraint.Constraint);
}
[Fact]
public void ParseRouteTemplate_ConstraintsDefaultsAndOptionalsInMultipleSections_ParsedCorrectly()
{
// Arrange & Act
var template = ParseRouteTemplate(@"some/url-{p1:int:test(3)=hello}/{p2=abc}/{p3?}");
// Assert
var parameters = template.Parameters.ToArray();
var param1 = parameters[0];
Assert.Equal("p1", param1.Name);
Assert.Equal("hello", param1.DefaultValue);
Assert.False(param1.IsOptional);
Assert.Collection(param1.InlineConstraints,
constraint => Assert.Equal("int", constraint.Constraint),
constraint => Assert.Equal("test(3)", constraint.Constraint)
);
var param2 = parameters[1];
Assert.Equal("p2", param2.Name);
Assert.Equal("abc", param2.DefaultValue);
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.DefaultValue);
}
[Fact]
public void ParseRouteParameter_ConstraintWithClosingBraceInPattern_ClosingBraceIsParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@"param:test(\})");
// Assert
Assert.Equal("param", templatePart.Name);
var constraint = Assert.Single(templatePart.InlineConstraints);
Assert.Equal(@"test(\})", constraint.Constraint);
}
[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.DefaultValue);
var constraint = Assert.Single(templatePart.InlineConstraints);
Assert.Equal(@"test(\})", constraint.Constraint);
}
[Fact]
public void ParseRouteParameter_ConstraintWithClosingParenInPattern_ClosingParenIsParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@"param:test(\))");
// Assert
Assert.Equal("param", templatePart.Name);
var constraint = Assert.Single(templatePart.InlineConstraints);
Assert.Equal(@"test(\))", constraint.Constraint);
}
[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.DefaultValue);
var constraint = Assert.Single(templatePart.InlineConstraints);
Assert.Equal(@"test(\))", constraint.Constraint);
}
[Fact]
public void ParseRouteParameter_ConstraintWithColonInPattern_ColonIsParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@"param:test(:)");
// Assert
Assert.Equal("param", templatePart.Name);
var constraint = Assert.Single(templatePart.InlineConstraints);
Assert.Equal(@"test(:)", constraint.Constraint);
}
[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.DefaultValue);
var constraint = Assert.Single(templatePart.InlineConstraints);
Assert.Equal(@"test(:)", constraint.Constraint);
}
[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.InlineConstraints);
Assert.Equal(@"test(a:b:c)", constraint.Constraint);
}
[Fact]
public void ParseRouteParameter_ConstraintWithColonInParamName_ParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@":param:test=12");
// Assert
Assert.Equal(":param", templatePart.Name);
Assert.Equal("12", templatePart.DefaultValue);
var constraint = Assert.Single(templatePart.InlineConstraints);
Assert.Equal("test", constraint.Constraint);
}
[Fact]
public void ParseRouteParameter_ConstraintWithTwoColonInParamName_ParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@":param::test=12");
// Assert
Assert.Equal(":param", templatePart.Name);
Assert.Equal("12", templatePart.DefaultValue);
Assert.Collection(templatePart.InlineConstraints,
constraint => Assert.Empty(constraint.Constraint),
constraint => Assert.Equal("test", constraint.Constraint));
}
[Fact]
public void ParseRouteParameter_EmptyConstraint_ParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@":param:test:");
// Assert
Assert.Equal(":param", templatePart.Name);
Assert.Collection(templatePart.InlineConstraints,
constraint => Assert.Equal("test", constraint.Constraint),
constraint => Assert.Empty(constraint.Constraint));
}
[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.InlineConstraints);
Assert.Equal(@"test(\w,\w)", constraint.Constraint);
}
[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.InlineConstraints);
Assert.Equal(@"test(\w)", constraint.Constraint);
}
[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.DefaultValue);
var constraint = Assert.Single(templatePart.InlineConstraints);
Assert.Equal(@"test(\w,\w)", constraint.Constraint);
}
[Fact]
public void ParseRouteParameter_ConstraintWithEqualsFollowedByQuestionMark_PatternIsParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@"param:int=?");
// Assert
Assert.Equal("param", templatePart.Name);
Assert.Equal("", templatePart.DefaultValue);
Assert.True(templatePart.IsOptional);
var constraint = Assert.Single(templatePart.InlineConstraints);
Assert.Equal("int", constraint.Constraint);
}
[Fact]
public void ParseRouteParameter_ConstraintWithEqualsSignInPattern_PatternIsParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@"param:test(=)");
// Assert
Assert.Equal("param", templatePart.Name);
Assert.Null(templatePart.DefaultValue);
var constraint = Assert.Single(templatePart.InlineConstraints);
Assert.Equal("test(=)", constraint.Constraint);
}
[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.DefaultValue);
}
[Fact]
public void ParseRouteParameter_ConstraintWithEqualEqualSignInPattern_ParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@"param:test(a==b)");
// Assert
Assert.Equal("param", templatePart.Name);
Assert.Null(templatePart.DefaultValue);
var constraint = Assert.Single(templatePart.InlineConstraints);
Assert.Equal("test(a==b)", constraint.Constraint);
}
[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.DefaultValue);
var constraint = Assert.Single(templatePart.InlineConstraints);
Assert.Equal("test(a==b)", constraint.Constraint);
}
[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.DefaultValue);
}
[Fact]
public void ParseRouteParameter_EqualEqualSignInDefaultValue_ParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@"param:test==dvds");
// Assert
Assert.Equal("param", templatePart.Name);
Assert.Equal("=dvds", templatePart.DefaultValue);
}
[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.DefaultValue);
}
[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.DefaultValue);
}
[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.DefaultValue);
var constraint = Assert.Single(templatePart.InlineConstraints);
Assert.Equal("test(=)", constraint.Constraint);
}
[Fact]
public void ParseRouteParameter_ConstraintWithOpenBraceInPattern_PatternIsParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@"param:test(\{)");
// Assert
Assert.Equal("param", templatePart.Name);
var constraint = Assert.Single(templatePart.InlineConstraints);
Assert.Equal(@"test(\{)", constraint.Constraint);
}
[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.InlineConstraints);
Assert.Equal(@"test(\sd)", constraint.Constraint);
}
[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.DefaultValue);
var constraint = Assert.Single(templatePart.InlineConstraints);
Assert.Equal(@"test(\{)", constraint.Constraint);
}
[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.InlineConstraints);
Assert.Equal(@"test(\()", constraint.Constraint);
}
[Fact]
public void ParseRouteParameter_ConstraintWithOpenParenInPattern_ParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@"param:test(\()");
// Assert
Assert.Equal("param", templatePart.Name);
var constraint = Assert.Single(templatePart.InlineConstraints);
Assert.Equal(@"test(\()", constraint.Constraint);
}
[Fact]
public void ParseRouteParameter_ConstraintWithOpenParenNoCloseParen_ParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@"param:test(#$%");
// Assert
Assert.Equal("param", templatePart.Name);
var constraint = Assert.Single(templatePart.InlineConstraints);
Assert.Equal("test(#$%", constraint.Constraint);
}
[Fact]
public void ParseRouteParameter_ConstraintWithOpenParenAndColon_ParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@"param:test(#:test1");
// Assert
Assert.Equal("param", templatePart.Name);
Assert.Collection(templatePart.InlineConstraints,
constraint => Assert.Equal(@"test(#", constraint.Constraint),
constraint => Assert.Equal(@"test1", constraint.Constraint));
}
[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.DefaultValue);
Assert.Collection(templatePart.InlineConstraints,
constraint => Assert.Equal(@"test(abc:somevalue)", constraint.Constraint),
constraint => Assert.Equal(@"name(test1", constraint.Constraint),
constraint => Assert.Equal(@"differentname", constraint.Constraint));
}
[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.DefaultValue);
var constraint = Assert.Single(templatePart.InlineConstraints);
Assert.Equal(@"test(constraintvalue", constraint.Constraint);
}
[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.DefaultValue);
var constraint = Assert.Single(templatePart.InlineConstraints);
Assert.Equal(@"test(\()", constraint.Constraint);
}
[Fact]
public void ParseRouteParameter_ConstraintWithQuestionMarkInPattern_PatternIsParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@"param:test(\?)");
// Assert
Assert.Equal("param", templatePart.Name);
Assert.Null(templatePart.DefaultValue);
Assert.False(templatePart.IsOptional);
var constraint = Assert.Single(templatePart.InlineConstraints);
Assert.Equal(@"test(\?)", constraint.Constraint);
}
[Fact]
public void ParseRouteParameter_ConstraintWithQuestionMarkInPattern_Optional_PatternIsParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@"param:test(\?)?");
// Assert
Assert.Equal("param", templatePart.Name);
Assert.Null(templatePart.DefaultValue);
Assert.True(templatePart.IsOptional);
var constraint = Assert.Single(templatePart.InlineConstraints);
Assert.Equal(@"test(\?)", constraint.Constraint);
}
[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.DefaultValue);
Assert.False(templatePart.IsOptional);
var constraint = Assert.Single(templatePart.InlineConstraints);
Assert.Equal(@"test(\?)", constraint.Constraint);
}
[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.DefaultValue);
Assert.True(templatePart.IsOptional);
var constraint = Assert.Single(templatePart.InlineConstraints);
Assert.Equal(@"test(\?)", constraint.Constraint);
}
[Fact]
public void ParseRouteParameter_ConstraintWithQuestionMarkInName_ParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@"par?am:test(\?)");
// Assert
Assert.Equal("par?am", templatePart.Name);
Assert.Null(templatePart.DefaultValue);
Assert.False(templatePart.IsOptional);
var constraint = Assert.Single(templatePart.InlineConstraints);
Assert.Equal(@"test(\?)", constraint.Constraint);
}
[Fact]
public void ParseRouteParameter_ConstraintWithClosedParenAndColonInPattern_ParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@"param:test(#):$)");
// Assert
Assert.Equal("param", templatePart.Name);
Assert.Null(templatePart.DefaultValue);
Assert.False(templatePart.IsOptional);
Assert.Collection(templatePart.InlineConstraints,
constraint => Assert.Equal(@"test(#)", constraint.Constraint),
constraint => Assert.Equal(@"$)", constraint.Constraint));
}
[Fact]
public void ParseRouteParameter_ConstraintWithColonAndClosedParenInPattern_PatternIsParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@"param:test(#:)$)");
// Assert
Assert.Equal("param", templatePart.Name);
Assert.Null(templatePart.DefaultValue);
Assert.False(templatePart.IsOptional);
var constraint = Assert.Single(templatePart.InlineConstraints);
Assert.Equal(@"test(#:)$)", constraint.Constraint);
}
[Fact]
public void ParseRouteParameter_ContainingMultipleUnclosedParenthesisInConstraint()
{
// Arrange & Act
var templatePart = ParseParameter(@"foo:regex(\\(\\(\\(\\()");
// Assert
Assert.Equal("foo", templatePart.Name);
Assert.Null(templatePart.DefaultValue);
Assert.False(templatePart.IsOptional);
var constraint = Assert.Single(templatePart.InlineConstraints);
Assert.Equal(@"regex(\\(\\(\\(\\()", constraint.Constraint);
}
[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.DefaultValue);
Assert.False(templatePart.IsOptional);
var constraint = Assert.Single(templatePart.InlineConstraints);
Assert.Equal(@"regex(^\d{{3}}-\d{{3}}-\d{{4}}$)", constraint.Constraint);
}
[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.DefaultValue);
Assert.False(templatePart.IsOptional);
var constraint = Assert.Single(templatePart.InlineConstraints);
Assert.Equal(@"regex(^\d{{3}}-\d{{3}}-\d{{4}}$)", constraint.Constraint);
}
[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.InlineConstraints);
Assert.Null(templatePart.DefaultValue);
}
private TemplatePart ParseParameter(string routeParameter)
{
var templatePart = InlineRouteParameterParser.ParseRouteParameter(routeParameter);
return templatePart;
}
private static RouteTemplate ParseRouteTemplate(string template)
{
return TemplateParser.Parse(template);
}
}
}

View File

@ -0,0 +1,916 @@
// 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.Testing;
using Xunit;
namespace Microsoft.AspNetCore.Dispatcher
{
public class TemplateRouteParserTests
{
[Fact]
public void Parse_SingleLiteral()
{
// Arrange
var template = "cool";
var expected = new RouteTemplate(template, new List<TemplateSegment>());
expected.Segments.Add(new TemplateSegment());
expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool"));
// Act
var actual = TemplateParser.Parse(template);
// Assert
Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer());
}
[Fact]
public void Parse_SingleParameter()
{
// Arrange
var template = "{p}";
var expected = new RouteTemplate(template, new List<TemplateSegment>());
expected.Segments.Add(new TemplateSegment());
expected.Segments[0].Parts.Add(
TemplatePart.CreateParameter("p", false, false, defaultValue: null, inlineConstraints: null));
expected.Parameters.Add(expected.Segments[0].Parts[0]);
// Act
var actual = TemplateParser.Parse(template);
// Assert
Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer());
}
[Fact]
public void Parse_OptionalParameter()
{
// Arrange
var template = "{p?}";
var expected = new RouteTemplate(template, new List<TemplateSegment>());
expected.Segments.Add(new TemplateSegment());
expected.Segments[0].Parts.Add(
TemplatePart.CreateParameter("p", false, true, defaultValue: null, inlineConstraints: null));
expected.Parameters.Add(expected.Segments[0].Parts[0]);
// Act
var actual = TemplateParser.Parse(template);
// Assert
Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer());
}
[Fact]
public void Parse_MultipleLiterals()
{
// Arrange
var template = "cool/awesome/super";
var expected = new RouteTemplate(template, new List<TemplateSegment>());
expected.Segments.Add(new TemplateSegment());
expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool"));
expected.Segments.Add(new TemplateSegment());
expected.Segments[1].Parts.Add(TemplatePart.CreateLiteral("awesome"));
expected.Segments.Add(new TemplateSegment());
expected.Segments[2].Parts.Add(TemplatePart.CreateLiteral("super"));
// Act
var actual = TemplateParser.Parse(template);
// Assert
Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer());
}
[Fact]
public void Parse_MultipleParameters()
{
// Arrange
var template = "{p1}/{p2}/{*p3}";
var expected = new RouteTemplate(template, new List<TemplateSegment>());
expected.Segments.Add(new TemplateSegment());
expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1",
false,
false,
defaultValue: null,
inlineConstraints: null));
expected.Parameters.Add(expected.Segments[0].Parts[0]);
expected.Segments.Add(new TemplateSegment());
expected.Segments[1].Parts.Add(TemplatePart.CreateParameter("p2",
false,
false,
defaultValue: null,
inlineConstraints: null));
expected.Parameters.Add(expected.Segments[1].Parts[0]);
expected.Segments.Add(new TemplateSegment());
expected.Segments[2].Parts.Add(TemplatePart.CreateParameter("p3",
true,
false,
defaultValue: null,
inlineConstraints: null));
expected.Parameters.Add(expected.Segments[2].Parts[0]);
// Act
var actual = TemplateParser.Parse(template);
// Assert
Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer());
}
[Fact]
public void Parse_ComplexSegment_LP()
{
// Arrange
var template = "cool-{p1}";
var expected = new RouteTemplate(template, new List<TemplateSegment>());
expected.Segments.Add(new TemplateSegment());
expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool-"));
expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1",
false,
false,
defaultValue: null,
inlineConstraints: null));
expected.Parameters.Add(expected.Segments[0].Parts[1]);
// Act
var actual = TemplateParser.Parse(template);
// Assert
Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer());
}
[Fact]
public void Parse_ComplexSegment_PL()
{
// Arrange
var template = "{p1}-cool";
var expected = new RouteTemplate(template, new List<TemplateSegment>());
expected.Segments.Add(new TemplateSegment());
expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1",
false,
false,
defaultValue: null,
inlineConstraints: null));
expected.Parameters.Add(expected.Segments[0].Parts[0]);
expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool-"));
// Act
var actual = TemplateParser.Parse(template);
// Assert
Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer());
}
[Fact]
public void Parse_ComplexSegment_PLP()
{
// Arrange
var template = "{p1}-cool-{p2}";
var expected = new RouteTemplate(template, new List<TemplateSegment>());
expected.Segments.Add(new TemplateSegment());
expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1",
false,
false,
defaultValue: null,
inlineConstraints: null));
expected.Parameters.Add(expected.Segments[0].Parts[0]);
expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool-"));
expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p2",
false,
false,
defaultValue: null,
inlineConstraints: null));
expected.Parameters.Add(expected.Segments[0].Parts[2]);
// Act
var actual = TemplateParser.Parse(template);
// Assert
Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer());
}
[Fact]
public void Parse_ComplexSegment_LPL()
{
// Arrange
var template = "cool-{p1}-awesome";
var expected = new RouteTemplate(template, new List<TemplateSegment>());
expected.Segments.Add(new TemplateSegment());
expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool-"));
expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1",
false,
false,
defaultValue: null,
inlineConstraints: null));
expected.Parameters.Add(expected.Segments[0].Parts[1]);
expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("-awesome"));
// Act
var actual = TemplateParser.Parse(template);
// Assert
Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer());
}
[Fact]
public void Parse_ComplexSegment_OptionalParameterFollowingPeriod()
{
// Arrange
var template = "{p1}.{p2?}";
var expected = new RouteTemplate(template, new List<TemplateSegment>());
expected.Segments.Add(new TemplateSegment());
expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1",
false,
false,
defaultValue: null,
inlineConstraints: null));
expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("."));
expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p2",
false,
true,
defaultValue: null,
inlineConstraints: null));
expected.Parameters.Add(expected.Segments[0].Parts[0]);
expected.Parameters.Add(expected.Segments[0].Parts[2]);
// Act
var actual = TemplateParser.Parse(template);
// Assert
Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer());
}
[Fact]
public void Parse_ComplexSegment_ParametersFollowingPeriod()
{
// Arrange
var template = "{p1}.{p2}";
var expected = new RouteTemplate(template, new List<TemplateSegment>());
expected.Segments.Add(new TemplateSegment());
expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1",
false,
false,
defaultValue: null,
inlineConstraints: null));
expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("."));
expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p2",
false,
false,
defaultValue: null,
inlineConstraints: null));
expected.Parameters.Add(expected.Segments[0].Parts[0]);
expected.Parameters.Add(expected.Segments[0].Parts[2]);
// Act
var actual = TemplateParser.Parse(template);
// Assert
Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer());
}
[Fact]
public void Parse_ComplexSegment_OptionalParameterFollowingPeriod_ThreeParameters()
{
// Arrange
var template = "{p1}.{p2}.{p3?}";
var expected = new RouteTemplate(template, new List<TemplateSegment>());
expected.Segments.Add(new TemplateSegment());
expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1",
false,
false,
defaultValue: null,
inlineConstraints: null));
expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("."));
expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p2",
false,
false,
defaultValue: null,
inlineConstraints: null));
expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("."));
expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p3",
false,
true,
defaultValue: null,
inlineConstraints: null));
expected.Parameters.Add(expected.Segments[0].Parts[0]);
expected.Parameters.Add(expected.Segments[0].Parts[2]);
expected.Parameters.Add(expected.Segments[0].Parts[4]);
// Act
var actual = TemplateParser.Parse(template);
// Assert
Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer());
}
[Fact]
public void Parse_ComplexSegment_ThreeParametersSeperatedByPeriod()
{
// Arrange
var template = "{p1}.{p2}.{p3}";
var expected = new RouteTemplate(template, new List<TemplateSegment>());
expected.Segments.Add(new TemplateSegment());
expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1",
false,
false,
defaultValue: null,
inlineConstraints: null));
expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("."));
expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p2",
false,
false,
defaultValue: null,
inlineConstraints: null));
expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("."));
expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p3",
false,
false,
defaultValue: null,
inlineConstraints: null));
expected.Parameters.Add(expected.Segments[0].Parts[0]);
expected.Parameters.Add(expected.Segments[0].Parts[2]);
expected.Parameters.Add(expected.Segments[0].Parts[4]);
// Act
var actual = TemplateParser.Parse(template);
// Assert
Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer());
}
[Fact]
public void Parse_ComplexSegment_OptionalParameterFollowingPeriod_MiddleSegment()
{
// Arrange
var template = "{p1}.{p2?}/{p3}";
var expected = new RouteTemplate(template, new List<TemplateSegment>());
expected.Segments.Add(new TemplateSegment());
expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1",
false,
false,
defaultValue: null,
inlineConstraints: null));
expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("."));
expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p2",
false,
true,
defaultValue: null,
inlineConstraints: null));
expected.Parameters.Add(expected.Segments[0].Parts[0]);
expected.Parameters.Add(expected.Segments[0].Parts[2]);
expected.Segments.Add(new TemplateSegment());
expected.Segments[1].Parts.Add(TemplatePart.CreateParameter("p3",
false,
false,
null,
null));
expected.Parameters.Add(expected.Segments[1].Parts[0]);
// Act
var actual = TemplateParser.Parse(template);
// Assert
Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer());
}
[Fact]
public void Parse_ComplexSegment_OptionalParameterFollowingPeriod_LastSegment()
{
// Arrange
var template = "{p1}/{p2}.{p3?}";
var expected = new RouteTemplate(template, new List<TemplateSegment>());
expected.Segments.Add(new TemplateSegment());
expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1",
false,
false,
defaultValue: null,
inlineConstraints: null));
expected.Segments.Add(new TemplateSegment());
expected.Segments[1].Parts.Add(TemplatePart.CreateParameter("p2",
false,
false,
defaultValue: null,
inlineConstraints: null));
expected.Segments[1].Parts.Add(TemplatePart.CreateLiteral("."));
expected.Segments[1].Parts.Add(TemplatePart.CreateParameter("p3",
false,
true,
null,
null));
expected.Parameters.Add(expected.Segments[0].Parts[0]);
expected.Parameters.Add(expected.Segments[1].Parts[0]);
expected.Parameters.Add(expected.Segments[1].Parts[2]);
// Act
var actual = TemplateParser.Parse(template);
// Assert
Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer());
}
[Fact]
public void Parse_ComplexSegment_OptionalParameterFollowingPeriod_PeriodAfterSlash()
{
// Arrange
var template = "{p2}/.{p3?}";
var expected = new RouteTemplate(template, new List<TemplateSegment>());
expected.Segments.Add(new TemplateSegment());
expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p2",
false,
false,
defaultValue: null,
inlineConstraints: null));
expected.Segments.Add(new TemplateSegment());
expected.Segments[1].Parts.Add(TemplatePart.CreateLiteral("."));
expected.Segments[1].Parts.Add(TemplatePart.CreateParameter("p3",
false,
true,
null,
null));
expected.Parameters.Add(expected.Segments[0].Parts[0]);
expected.Parameters.Add(expected.Segments[1].Parts[1]);
// Act
var actual = TemplateParser.Parse(template);
// Assert
Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer());
}
[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 = new RouteTemplate(template, new List<TemplateSegment>());
expected.Segments.Add(new TemplateSegment());
var c = new InlineConstraint(constraint);
expected.Segments[0].Parts.Add(
TemplatePart.CreateParameter("p1",
false,
false,
defaultValue: null,
inlineConstraints: new List<InlineConstraint> { c }));
expected.Parameters.Add(expected.Segments[0].Parts[0]);
// Act
var actual = TemplateParser.Parse(template);
// Assert
Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer());
}
[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<ArgumentException>(
() => TemplateParser.Parse(template),
"There is an incomplete parameter in the route template. Check that each '{' character has a matching " +
"'}' character." + Environment.NewLine + "Parameter name: routeTemplate");
}
[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<ArgumentException>(
() => TemplateParser.Parse(template),
"In a route parameter, '{' and '}' must be escaped with '{{' and '}}'." + Environment.NewLine +
"Parameter name: routeTemplate");
}
[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<ArgumentException>(
() => TemplateParser.Parse(template),
"An optional parameter must be at the end of the segment. In the segment '" + template +
"', optional parameter '" + parameter + "' is followed by '" + invalid + "'."
+ Environment.NewLine + "Parameter name: routeTemplate");
}
[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<ArgumentException>(
() => TemplateParser.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." +
Environment.NewLine + "Parameter name: routeTemplate");
}
[Fact]
public void InvalidTemplate_WithRepeatedParameter()
{
var ex = ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("{Controller}.mvc/{id}/{controller}"),
"The route parameter name 'controller' appears more than one time in the route template." +
Environment.NewLine + "Parameter name: routeTemplate");
}
[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<ArgumentException>(
() => TemplateParser.Parse(template),
@"There is an incomplete parameter in the route template. Check that each '{' character has a " +
"matching '}' character." + Environment.NewLine +
"Parameter name: routeTemplate");
}
[Fact]
public void InvalidTemplate_CannotHaveCatchAllInMultiSegment()
{
ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("123{a}abc{*moo}"),
"A path segment that contains more than one section, such as a literal section or a parameter, " +
"cannot contain a catch-all parameter." + Environment.NewLine +
"Parameter name: routeTemplate");
}
[Fact]
public void InvalidTemplate_CannotHaveMoreThanOneCatchAll()
{
ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("{*p1}/{*p2}"),
"A catch-all parameter can only appear as the last segment of the route template." +
Environment.NewLine +
"Parameter name: routeTemplate");
}
[Fact]
public void InvalidTemplate_CannotHaveMoreThanOneCatchAllInMultiSegment()
{
ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("{*p1}abc{*p2}"),
"A path segment that contains more than one section, such as a literal section or a parameter, " +
"cannot contain a catch-all parameter." + Environment.NewLine +
"Parameter name: routeTemplate");
}
[Fact]
public void InvalidTemplate_CannotHaveCatchAllWithNoName()
{
ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("foo/{*}"),
"The route parameter name '' is invalid. Route parameter names must be non-empty and cannot" +
" contain these characters: '{', '}', '/'. The '?' character marks a parameter as optional," +
" and can occur only at the end of the parameter. The '*' character marks a parameter as catch-all," +
" and can occur only at the start of the parameter." + Environment.NewLine +
"Parameter name: routeTemplate");
}
[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<ArgumentException>(
() => TemplateParser.Parse(template), expectedMessage + Environment.NewLine +
"Parameter name: routeTemplate");
}
[Fact]
public void InvalidTemplate_CannotHaveConsecutiveOpenBrace()
{
ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("foo/{{p1}"),
"There is an incomplete parameter in the route template. Check that each '{' character has a " +
"matching '}' character." + Environment.NewLine +
"Parameter name: routeTemplate");
}
[Fact]
public void InvalidTemplate_CannotHaveConsecutiveCloseBrace()
{
ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("foo/{p1}}"),
"There is an incomplete parameter in the route template. Check that each '{' character has a " +
"matching '}' character." + Environment.NewLine +
"Parameter name: routeTemplate");
}
[Fact]
public void InvalidTemplate_SameParameterTwiceThrows()
{
ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("{aaa}/{AAA}"),
"The route parameter name 'AAA' appears more than one time in the route template." +
Environment.NewLine +
"Parameter name: routeTemplate");
}
[Fact]
public void InvalidTemplate_SameParameterTwiceAndOneCatchAllThrows()
{
ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("{aaa}/{*AAA}"),
"The route parameter name 'AAA' appears more than one time in the route template." +
Environment.NewLine +
"Parameter name: routeTemplate");
}
[Fact]
public void InvalidTemplate_InvalidParameterNameWithCloseBracketThrows()
{
ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("{a}/{aa}a}/{z}"),
"There is an incomplete parameter in the route template. Check that each '{' character has a " +
"matching '}' character." + Environment.NewLine +
"Parameter name: routeTemplate");
}
[Fact]
public void InvalidTemplate_InvalidParameterNameWithOpenBracketThrows()
{
ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("{a}/{a{aa}/{z}"),
"In a route parameter, '{' and '}' must be escaped with '{{' and '}}'." + Environment.NewLine +
"Parameter name: routeTemplate");
}
[Fact]
public void InvalidTemplate_InvalidParameterNameWithEmptyNameThrows()
{
ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("{a}/{}/{z}"),
"The route parameter name '' is invalid. Route parameter names must be non-empty and cannot" +
" contain these characters: '{', '}', '/'. The '?' character marks a parameter as optional, and" +
" can occur only at the end of the parameter. The '*' character marks a parameter as catch-all," +
" and can occur only at the start of the parameter." + Environment.NewLine +
"Parameter name: routeTemplate");
}
[Fact]
public void InvalidTemplate_InvalidParameterNameWithQuestionThrows()
{
ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("{Controller}.mvc/{?}"),
"The route parameter name '' is invalid. Route parameter names must be non-empty and cannot" +
" contain these characters: '{', '}', '/'. The '?' character marks a parameter as optional, and" +
" can occur only at the end of the parameter. The '*' character marks a parameter as catch-all," +
" and can occur only at the start of the parameter." + Environment.NewLine +
"Parameter name: routeTemplate");
}
[Fact]
public void InvalidTemplate_ConsecutiveSeparatorsSlashSlashThrows()
{
ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("{a}//{z}"),
"The route template separator character '/' cannot appear consecutively. It must be separated by " +
"either a parameter or a literal value." + Environment.NewLine +
"Parameter name: routeTemplate");
}
[Fact]
public void InvalidTemplate_WithCatchAllNotAtTheEndThrows()
{
ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("foo/{p1}/{*p2}/{p3}"),
"A catch-all parameter can only appear as the last segment of the route template." +
Environment.NewLine +
"Parameter name: routeTemplate");
}
[Fact]
public void InvalidTemplate_RepeatedParametersThrows()
{
ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("foo/aa{p1}{p2}"),
"A path segment cannot contain two consecutive parameters. They must be separated by a '/' or by " +
"a literal string." + Environment.NewLine +
"Parameter name: routeTemplate");
}
[Fact]
public void InvalidTemplate_CannotStartWithSlash()
{
ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("/foo"),
"The route template cannot start with a '/' or '~' character." + Environment.NewLine +
"Parameter name: routeTemplate");
}
[Fact]
public void InvalidTemplate_CannotStartWithTilde()
{
ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("~foo"),
"The route template cannot start with a '/' or '~' character." + Environment.NewLine +
"Parameter name: routeTemplate");
}
[Fact]
public void InvalidTemplate_CannotContainQuestionMark()
{
ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("foor?bar"),
"The literal section 'foor?bar' is invalid. Literal sections cannot contain the '?' character." +
Environment.NewLine +
"Parameter name: routeTemplate");
}
[Fact]
public void InvalidTemplate_ParameterCannotContainQuestionMark_UnlessAtEnd()
{
ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("{foor?b}"),
"The route parameter name 'foor?b' is invalid. Route parameter names must be non-empty and cannot" +
" contain these characters: '{', '}', '/'. The '?' character marks a parameter as optional, and" +
" can occur only at the end of the parameter. The '*' character marks a parameter as catch-all," +
" and can occur only at the start of the parameter." + Environment.NewLine +
"Parameter name: routeTemplate");
}
[Fact]
public void InvalidTemplate_CatchAllMarkedOptional()
{
ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("{a}/{*b?}"),
"A catch-all parameter cannot be marked optional." + Environment.NewLine +
"Parameter name: routeTemplate");
}
private class TemplateEqualityComparer : IEqualityComparer<RouteTemplate>
{
public bool Equals(RouteTemplate x, RouteTemplate y)
{
if (x == null && y == null)
{
return true;
}
else if (x == null || y == null)
{
return false;
}
else
{
if (!string.Equals(x.TemplateText, y.TemplateText, StringComparison.Ordinal))
{
return false;
}
if (x.Segments.Count != y.Segments.Count)
{
return false;
}
for (int i = 0; i < x.Segments.Count; i++)
{
if (x.Segments[i].Parts.Count != y.Segments[i].Parts.Count)
{
return false;
}
for (int j = 0; j < x.Segments[i].Parts.Count; j++)
{
if (!Equals(x.Segments[i].Parts[j], y.Segments[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(TemplatePart x, TemplatePart y)
{
if (x.IsLiteral != y.IsLiteral ||
x.IsParameter != y.IsParameter ||
x.IsCatchAll != y.IsCatchAll ||
x.IsOptional != y.IsOptional ||
!String.Equals(x.Name, y.Name, StringComparison.Ordinal) ||
!String.Equals(x.Name, y.Name, StringComparison.Ordinal) ||
(x.InlineConstraints == null && y.InlineConstraints != null) ||
(x.InlineConstraints != null && y.InlineConstraints == null))
{
return false;
}
if (x.InlineConstraints == null && y.InlineConstraints == null)
{
return true;
}
if (x.InlineConstraints.Count() != y.InlineConstraints.Count())
{
return false;
}
foreach (var xconstraint in x.InlineConstraints)
{
if (!y.InlineConstraints.Any<InlineConstraint>(
c => string.Equals(c.Constraint, xconstraint.Constraint)))
{
return false;
}
}
return true;
}
public int GetHashCode(RouteTemplate obj)
{
throw new NotImplementedException();
}
}
}
}

View File

@ -2,7 +2,6 @@
// 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.Http;
using Microsoft.AspNetCore.Routing.Template;

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