RoutePattern everywhere

This commit is contained in:
Ryan Nowak 2018-07-19 13:34:18 -07:00
parent 90395c933d
commit f1c060bf3d
34 changed files with 859 additions and 675 deletions

View File

@ -5,6 +5,7 @@ using System.Text;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Matchers;
using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.Extensions.DependencyInjection;
namespace Benchmarks
@ -31,10 +32,8 @@ namespace Benchmarks
response.ContentLength = payloadLength;
return response.Body.WriteAsync(_helloWorldPayload, 0, payloadLength);
},
template: "/plaintext",
defaults: new RouteValueDictionary(),
routePattern: RoutePatternFactory.Parse("/plaintext"),
requiredValues: new RouteValueDictionary(),
nonInlineMatchProcessorReferences: null,
order: 0,
metadata: EndpointMetadataCollection.Empty,
displayName: "Plaintext"),

View File

@ -8,6 +8,7 @@ using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing.EndpointConstraints;
using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.AspNetCore.Routing.Matchers
@ -42,11 +43,9 @@ namespace Microsoft.AspNetCore.Routing.Matchers
}
return new MatcherEndpoint(
(next) => (context) => Task.CompletedTask,
template,
MatcherEndpoint.EmptyInvoker,
RoutePatternFactory.Parse(template),
new RouteValueDictionary(),
new RouteValueDictionary(),
new List<MatchProcessorReference>(),
0,
new EndpointMetadataCollection(metadata),
template);

View File

@ -39,7 +39,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
}
var path = httpContext.Request.Path.Value;
if (string.Equals(_endpoint.Template, path, StringComparison.OrdinalIgnoreCase))
if (string.Equals(_endpoint.RoutePattern.RawText, path, StringComparison.OrdinalIgnoreCase))
{
feature.Endpoint = _endpoint;
feature.Values = new RouteValueDictionary();
@ -51,7 +51,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
// This is here so this can be tested alongside DFA matcher.
internal CandidateSet SelectCandidates(string path, ReadOnlySpan<PathSegment> segments)
{
if (string.Equals(_endpoint.Template, path, StringComparison.OrdinalIgnoreCase))
if (string.Equals(_endpoint.RoutePattern.RawText, path, StringComparison.OrdinalIgnoreCase))
{
return _candidates;
}

View File

@ -8,6 +8,7 @@ using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Matchers;
using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.Extensions.DependencyInjection;
namespace DispatcherSample.Web
@ -39,10 +40,8 @@ namespace DispatcherSample.Web
response.ContentLength = payloadLength;
return response.Body.WriteAsync(_homePayload, 0, payloadLength);
},
"/",
RoutePatternFactory.Parse("/"),
new RouteValueDictionary(),
new RouteValueDictionary(),
new List<MatchProcessorReference>(),
0,
EndpointMetadataCollection.Empty,
"Home"),
@ -55,10 +54,8 @@ namespace DispatcherSample.Web
response.ContentLength = payloadLength;
return response.Body.WriteAsync(_helloWorldPayload, 0, payloadLength);
},
"/plaintext",
new RouteValueDictionary(),
new RouteValueDictionary(),
new List<MatchProcessorReference>(),
RoutePatternFactory.Parse("/plaintext"),
new RouteValueDictionary(),
0,
EndpointMetadataCollection.Empty,
"Plaintext"),
@ -69,10 +66,8 @@ namespace DispatcherSample.Web
response.ContentType = "text/plain";
return response.WriteAsync("WithConstraints");
},
"/withconstraints/{id:endsWith(_001)}",
RoutePatternFactory.Parse("/withconstraints/{id:endsWith(_001)}"),
new RouteValueDictionary(),
new RouteValueDictionary(),
new List<MatchProcessorReference>(),
0,
EndpointMetadataCollection.Empty,
"withconstraints"),
@ -83,10 +78,8 @@ namespace DispatcherSample.Web
response.ContentType = "text/plain";
return response.WriteAsync("withoptionalconstraints");
},
"/withoptionalconstraints/{id:endsWith(_001)?}",
RoutePatternFactory.Parse("/withoptionalconstraints/{id:endsWith(_001)?}"),
new RouteValueDictionary(),
new RouteValueDictionary(),
new List<MatchProcessorReference>(),
0,
EndpointMetadataCollection.Empty,
"withoptionalconstraints"),

View File

@ -86,8 +86,8 @@ namespace Microsoft.AspNetCore.Routing
var templateBinder = new TemplateBinder(
UrlEncoder.Default,
_uriBuildingContextPool,
endpoint.ParsedTemplate,
endpoint.Defaults);
new RouteTemplate(endpoint.RoutePattern),
new RouteValueDictionary(endpoint.RoutePattern.Defaults));
var templateValuesResult = templateBinder.GetValues(ambientValues, explicitValues);
if (templateValuesResult == null)
@ -110,20 +110,19 @@ namespace Microsoft.AspNetCore.Routing
{
throw new ArgumentNullException(nameof(routeValues));
}
for (var i = 0; i < endpoint.MatchProcessorReferences.Count; i++)
foreach (var kvp in endpoint.RoutePattern.Constraints)
{
var matchProcessorReference = endpoint.MatchProcessorReferences[i];
var parameter = endpoint.ParsedTemplate.GetParameter(matchProcessorReference.ParameterName);
if (parameter != null && parameter.IsOptional && !routeValues.ContainsKey(parameter.Name))
var parameter = endpoint.RoutePattern.GetParameter(kvp.Key); // may be null, that's ok
var constraintReferences = kvp.Value;
for (var i = 0; i < constraintReferences.Count; i++)
{
continue;
}
var matchProcessor = _matchProcessorFactory.Create(matchProcessorReference);
if (!matchProcessor.ProcessOutbound(httpContext, routeValues))
{
return false;
var constraintReference = constraintReferences[i];
var matchProcessor = _matchProcessorFactory.Create(parameter, constraintReference);
if (!matchProcessor.ProcessOutbound(httpContext, routeValues))
{
return false;
}
}
}

View File

@ -20,145 +20,113 @@ namespace Microsoft.AspNetCore.Routing.Matchers
_serviceProvider = serviceProvider;
}
public override MatchProcessor Create(MatchProcessorReference matchProcessorReference)
public override MatchProcessor Create(string parameterName, IRouteConstraint value, bool optional)
{
if (matchProcessorReference == null)
if (value == null)
{
throw new ArgumentNullException(nameof(matchProcessorReference));
throw new ArgumentNullException(nameof(value));
}
if (matchProcessorReference.MatchProcessor != null)
return InitializeMatchProcessor(parameterName, optional, value, argument: null);
}
public override MatchProcessor Create(string parameterName, MatchProcessor value, bool optional)
{
if (value == null)
{
return matchProcessorReference.MatchProcessor;
throw new ArgumentNullException(nameof(value));
}
return InitializeMatchProcessor(parameterName, optional, value, argument: null);
}
public override MatchProcessor Create(string parameterName, string value, bool optional)
{
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}
// Example:
// {productId:regex(\d+)}
//
// ParameterName: productId
// ConstraintText: regex(\d+)
// ConstraintName: regex
// ConstraintArgument: \d+
// value: regex(\d+)
// name: regex
// argument: \d+
(var name, var argument) = Parse(value);
(var constraintName, var constraintArgument) = Parse(matchProcessorReference.ConstraintText);
if (!_options.ConstraintMap.TryGetValue(constraintName, out var constraintType))
if (!_options.ConstraintMap.TryGetValue(name, out var type))
{
throw new InvalidOperationException(
$"No constraint has been registered with name '{constraintName}'.");
throw new InvalidOperationException(Resources.FormatRoutePattern_ConstraintReferenceNotFound(
name,
typeof(RouteOptions),
nameof(RouteOptions.ConstraintMap)));
}
var processor = ResolveMatchProcessor(
matchProcessorReference.ParameterName,
matchProcessorReference.Optional,
constraintType,
constraintArgument);
if (processor != null)
if (typeof(MatchProcessor).IsAssignableFrom(type))
{
return processor;
var matchProcessor = (MatchProcessor)_serviceProvider.GetRequiredService(type);
return InitializeMatchProcessor(parameterName, optional, matchProcessor, argument);
}
if (!typeof(IRouteConstraint).IsAssignableFrom(constraintType))
if (typeof(IRouteConstraint).IsAssignableFrom(type))
{
throw new InvalidOperationException(
Resources.FormatDefaultInlineConstraintResolver_TypeNotConstraint(
constraintType,
constraintName,
typeof(IRouteConstraint).Name));
var constraint = DefaultInlineConstraintResolver.CreateConstraint(type, argument);
return InitializeMatchProcessor(parameterName, optional, constraint, argument);
}
try
{
return CreateMatchProcessorFromRouteConstraint(
matchProcessorReference.ParameterName,
matchProcessorReference.Optional,
constraintType,
constraintArgument);
}
catch (RouteCreationException)
{
throw;
}
catch (Exception exception)
{
throw new InvalidOperationException(
$"An error occurred while trying to create an instance of route constraint '{constraintType.FullName}'.",
exception);
}
var message = Resources.FormatRoutePattern_InvalidStringConstraintReference(
type,
name,
typeof(IRouteConstraint),
typeof(MatchProcessor));
throw new InvalidOperationException(message);
}
private MatchProcessor CreateMatchProcessorFromRouteConstraint(
private MatchProcessor InitializeMatchProcessor(
string parameterName,
bool optional,
Type constraintType,
string constraintArgument)
IRouteConstraint constraint,
string argument)
{
var matchProcessor = (MatchProcessor)new RouteConstraintMatchProcessor(parameterName, constraint);
return InitializeMatchProcessor(parameterName, optional, matchProcessor, argument);
}
private MatchProcessor InitializeMatchProcessor(
string parameterName,
bool optional,
MatchProcessor matchProcessor,
string argument)
{
var routeConstraint = DefaultInlineConstraintResolver.CreateConstraint(constraintType, constraintArgument);
var matchProcessor = new MatchProcessorReference(parameterName, routeConstraint).MatchProcessor;
if (optional)
{
matchProcessor = new OptionalMatchProcessor(matchProcessor);
}
matchProcessor.Initialize(parameterName, constraintArgument);
matchProcessor.Initialize(parameterName, argument);
return matchProcessor;
}
private MatchProcessor ResolveMatchProcessor(
string parameterName,
bool optional,
Type constraintType,
string constraintArgument)
private (string name, string argument) Parse(string text)
{
if (constraintType == null)
string name;
string argument;
var indexOfFirstOpenParens = text.IndexOf('(');
if (indexOfFirstOpenParens >= 0 && text.EndsWith(")", StringComparison.Ordinal))
{
throw new ArgumentNullException(nameof(constraintType));
}
if (!typeof(MatchProcessor).IsAssignableFrom(constraintType))
{
// Since a constraint type could be of type IRouteConstraint, do not throw
return null;
}
var registeredProcessor = _serviceProvider.GetRequiredService(constraintType);
if (registeredProcessor is MatchProcessor matchProcessor)
{
if (optional)
{
matchProcessor = new OptionalMatchProcessor(matchProcessor);
}
matchProcessor.Initialize(parameterName, constraintArgument);
return matchProcessor;
}
else
{
throw new InvalidOperationException(
$"Registered constraint type '{constraintType}' is not of type '{typeof(MatchProcessor)}'.");
}
}
private (string constraintName, string constraintArgument) Parse(string constraintText)
{
string constraintName;
string constraintArgument;
var indexOfFirstOpenParens = constraintText.IndexOf('(');
if (indexOfFirstOpenParens >= 0 && constraintText.EndsWith(")", StringComparison.Ordinal))
{
constraintName = constraintText.Substring(0, indexOfFirstOpenParens);
constraintArgument = constraintText.Substring(
name = text.Substring(0, indexOfFirstOpenParens);
argument = text.Substring(
indexOfFirstOpenParens + 1,
constraintText.Length - indexOfFirstOpenParens - 2);
text.Length - indexOfFirstOpenParens - 2);
}
else
{
constraintName = constraintText;
constraintArgument = null;
name = text;
argument = null;
}
return (constraintName, constraintArgument);
return (name, argument);
}
}
}

View File

@ -53,7 +53,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
for (var i = 0; i < _entries.Count; i++)
{
var entry = _entries[i];
maxDepth = Math.Max(maxDepth, entry.Pattern.Segments.Count);
maxDepth = Math.Max(maxDepth, entry.RoutePattern.PathSegments.Count);
work.Add((entry, new List<DfaNode>() { root, }));
}
@ -88,9 +88,10 @@ namespace Microsoft.AspNetCore.Routing.Matchers
for (var j = 0; j < parents.Count; j++)
{
var parent = parents[j];
if (segment.IsSimple && segment.Parts[0].IsLiteral)
var part = segment.Parts[0];
if (segment.IsSimple && part is RoutePatternLiteralPart literalPart)
{
var literal = segment.Parts[0].Text;
var literal = literalPart.Content;
if (!parent.Literals.TryGetValue(literal, out var next))
{
next = new DfaNode()
@ -103,7 +104,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
nextParents.Add(next);
}
else if (segment.IsSimple && segment.Parts[0].IsCatchAll)
else if (segment.IsSimple && part is RoutePatternParameterPart parameterPart && parameterPart.IsCatchAll)
{
// A catch all should traverse all literal nodes as well as parameter nodes
// we don't need to create the parameter node here because of ordering
@ -134,7 +135,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
parent.CatchAll.Matches.Add(entry);
}
else if (segment.IsSimple && segment.Parts[0].IsParameter)
else if (segment.IsSimple && part.IsParameter)
{
if (parent.Parameters == null)
{
@ -182,20 +183,20 @@ namespace Microsoft.AspNetCore.Routing.Matchers
return root;
}
private TemplateSegment GetCurrentSegment(MatcherBuilderEntry entry, int depth)
private RoutePatternPathSegment GetCurrentSegment(MatcherBuilderEntry entry, int depth)
{
if (depth < entry.Pattern.Segments.Count)
if (depth < entry.RoutePattern.PathSegments.Count)
{
return entry.Pattern.Segments[depth];
return entry.RoutePattern.PathSegments[depth];
}
if (entry.Pattern.Segments.Count == 0)
if (entry.RoutePattern.PathSegments.Count == 0)
{
return null;
}
var lastSegment = entry.Pattern.Segments[entry.Pattern.Segments.Count - 1];
if (lastSegment.IsSimple && lastSegment.Parts[0].IsCatchAll)
var lastSegment = entry.RoutePattern.PathSegments[entry.RoutePattern.PathSegments.Count - 1];
if (lastSegment.IsSimple && lastSegment.Parts[0] is RoutePatternParameterPart parameterPart && parameterPart.IsCatchAll)
{
return lastSegment;
}
@ -305,63 +306,68 @@ namespace Microsoft.AspNetCore.Routing.Matchers
var captures = new List<(string parameterName, int segmentIndex, int slotIndex)>();
(string parameterName, int segmentIndex, int slotIndex) catchAll = default;
foreach (var kvp in entry.Endpoint.Defaults)
foreach (var kvp in entry.Endpoint.RoutePattern.Defaults)
{
assignments.Add(kvp.Key, assignments.Count);
slots.Add(kvp);
}
for (var i = 0; i < entry.Pattern.Segments.Count; i++)
for (var i = 0; i < entry.Endpoint.RoutePattern.PathSegments.Count; i++)
{
var segment = entry.Pattern.Segments[i];
var segment = entry.Endpoint.RoutePattern.PathSegments[i];
if (!segment.IsSimple)
{
continue;
}
var part = segment.Parts[0];
if (!part.IsParameter)
var parameterPart = segment.Parts[0] as RoutePatternParameterPart;
if (parameterPart == null)
{
continue;
}
if (!assignments.TryGetValue(part.Name, out var slotIndex))
if (!assignments.TryGetValue(parameterPart.Name, out var slotIndex))
{
slotIndex = assignments.Count;
assignments.Add(part.Name, slotIndex);
assignments.Add(parameterPart.Name, slotIndex);
var hasDefaultValue = part.DefaultValue != null || part.IsCatchAll;
slots.Add(hasDefaultValue ? new KeyValuePair<string, object>(part.Name, part.DefaultValue) : default);
var hasDefaultValue = parameterPart.Default != null || parameterPart.IsCatchAll;
slots.Add(hasDefaultValue ? new KeyValuePair<string, object>(parameterPart.Name, parameterPart.Default) : default);
}
if (part.IsCatchAll)
if (parameterPart.IsCatchAll)
{
catchAll = (part.Name, i, slotIndex);
catchAll = (parameterPart.Name, i, slotIndex);
}
else
{
captures.Add((part.Name, i, slotIndex));
captures.Add((parameterPart.Name, i, slotIndex));
}
}
var complexSegments = new List<(RoutePatternPathSegment pathSegment, int segmentIndex)>();
for (var i = 0; i < entry.Pattern.Segments.Count; i++)
for (var i = 0; i < entry.RoutePattern.PathSegments.Count; i++)
{
var segment = entry.Pattern.Segments[i];
var segment = entry.RoutePattern.PathSegments[i];
if (segment.IsSimple)
{
continue;
}
complexSegments.Add((segment.ToRoutePatternPathSegment(), i));
complexSegments.Add((segment, i));
}
var matchProcessors = new List<MatchProcessor>();
for (var i = 0; i < entry.Endpoint.MatchProcessorReferences.Count; i++)
foreach (var kvp in entry.Endpoint.RoutePattern.Constraints)
{
var reference = entry.Endpoint.MatchProcessorReferences[i];
var processor = _matchProcessorFactory.Create(reference);
matchProcessors.Add(processor);
var parameter = entry.Endpoint.RoutePattern.GetParameter(kvp.Key); // may be null, that's ok
var constraintReferences = kvp.Value;
for (var i = 0; i < constraintReferences.Count; i++)
{
var constraintReference = constraintReferences[i];
var matchProcessor = _matchProcessorFactory.Create(parameter, constraintReference);
matchProcessors.Add(matchProcessor);
}
}
return new Candidate(
@ -405,25 +411,25 @@ namespace Microsoft.AspNetCore.Routing.Matchers
private static bool HasAdditionalRequiredSegments(MatcherBuilderEntry entry, int depth)
{
for (var i = depth; i < entry.Pattern.Segments.Count; i++)
for (var i = depth; i < entry.RoutePattern.PathSegments.Count; i++)
{
var segment = entry.Pattern.Segments[i];
var segment = entry.RoutePattern.PathSegments[i];
if (!segment.IsSimple)
{
// Complex segments always require more processing
return true;
}
var part = segment.Parts[0];
if (part.IsLiteral)
var parameterPart = segment.Parts[0] as RoutePatternParameterPart;
if (parameterPart == null)
{
// It's a literal
return true;
}
if (!part.IsOptional &&
!part.IsCatchAll &&
part.DefaultValue == null &&
!entry.Endpoint.Defaults.ContainsKey(part.Name))
if (!parameterPart.IsOptional &&
!parameterPart.IsCatchAll &&
parameterPart.Default == null)
{
return true;
}

View File

@ -1,10 +1,46 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Diagnostics;
using Microsoft.AspNetCore.Routing.Patterns;
namespace Microsoft.AspNetCore.Routing.Matchers
{
internal abstract class MatchProcessorFactory
{
public abstract MatchProcessor Create(MatchProcessorReference matchProcessorReference);
public abstract MatchProcessor Create(string parameterName, string value, bool optional);
public abstract MatchProcessor Create(string parameterName, IRouteConstraint value, bool optional);
public abstract MatchProcessor Create(string parameterName, MatchProcessor value, bool optional);
public MatchProcessor Create(RoutePatternParameterPart parameter, RoutePatternConstraintReference reference)
{
if (reference == null)
{
throw new ArgumentNullException(nameof(reference));
}
Debug.Assert(reference.MatchProcessor != null || reference.Constraint != null || reference.Content != null);
if (reference.MatchProcessor != null)
{
return Create(parameter?.Name, reference.MatchProcessor, parameter?.IsOptional ?? false);
}
if (reference.Constraint != null)
{
return Create(parameter?.Name, reference.Constraint, parameter?.IsOptional ?? false);
}
if (reference.Content != null)
{
return Create(parameter?.Name, reference.Content, parameter?.IsOptional ?? false);
}
// Unreachable
throw new NotSupportedException();
}
}
}

View File

@ -1,86 +0,0 @@
// 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 Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Routing.Matchers
{
public sealed class MatchProcessorReference
{
// Example:
// api/products/{productId:regex(\d+)}
//
// ParameterName = productId
// ConstraintText = regex(\d+)
// ConstraintArgument = \d+
public MatchProcessorReference(string parameterName, string constraintText)
{
ParameterName = parameterName;
ConstraintText = constraintText;
}
public MatchProcessorReference(string parameterName, bool optional, string constraintText)
{
ParameterName = parameterName;
Optional = optional;
ConstraintText = constraintText;
}
public MatchProcessorReference(string parameterName, MatchProcessor matchProcessor)
{
ParameterName = parameterName;
MatchProcessor = matchProcessor;
}
internal MatchProcessor MatchProcessor { get; private set; }
internal string ConstraintText { get; private set; }
internal string ParameterName { get; private set; }
internal bool Optional { get; private set; }
public MatchProcessorReference(string parameterName, IRouteConstraint routeConstraint)
: this(parameterName, new RouteConstraintMatchProcessorAdapter(parameterName, routeConstraint))
{
}
private class RouteConstraintMatchProcessorAdapter : MatchProcessor
{
public string ParameterName { get; private set; }
public IRouteConstraint RouteConstraint { get; }
public RouteConstraintMatchProcessorAdapter(string parameterName, IRouteConstraint routeConstraint)
{
ParameterName = parameterName;
RouteConstraint = routeConstraint;
}
public override void Initialize(string parameterName, string constraintArgument)
{
}
public override bool ProcessInbound(HttpContext httpContext, RouteValueDictionary routeValues)
{
return RouteConstraint.Match(
httpContext,
NullRouter.Instance,
ParameterName,
routeValues,
RouteDirection.IncomingRequest);
}
public override bool ProcessOutbound(HttpContext httpContext, RouteValueDictionary values)
{
return RouteConstraint.Match(
httpContext,
NullRouter.Instance,
ParameterName,
values,
RouteDirection.UrlGeneration);
}
}
}
}

View File

@ -2,8 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Linq;
using Microsoft.AspNetCore.Routing.EndpointConstraints;
using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.AspNetCore.Routing.Template;
namespace Microsoft.AspNetCore.Routing.Matchers
@ -14,14 +13,14 @@ namespace Microsoft.AspNetCore.Routing.Matchers
{
Endpoint = endpoint;
Precedence = RoutePrecedence.ComputeInbound(endpoint.ParsedTemplate);
Precedence = RoutePrecedence.ComputeInbound(endpoint.RoutePattern);
}
public MatcherEndpoint Endpoint { get; }
public int Order => Endpoint.Order;
public RouteTemplate Pattern => Endpoint.ParsedTemplate;
public RoutePattern RoutePattern => Endpoint.RoutePattern;
public decimal Precedence { get; }
@ -39,7 +38,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
return comparison;
}
return Pattern.TemplateText.CompareTo(other.Pattern.TemplateText);
return RoutePattern.RawText.CompareTo(other.RoutePattern.RawText);
}
public bool PriorityEquals(MatcherBuilderEntry other)

View File

@ -5,7 +5,7 @@ using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing.Template;
using Microsoft.AspNetCore.Routing.Patterns;
namespace Microsoft.AspNetCore.Routing.Matchers
{
@ -18,25 +18,11 @@ namespace Microsoft.AspNetCore.Routing.Matchers
public MatcherEndpoint(
Func<RequestDelegate, RequestDelegate> invoker,
string template,
RouteValueDictionary defaults,
RoutePattern routePattern,
RouteValueDictionary requiredValues,
int order,
EndpointMetadataCollection metadata,
string displayName)
: this(invoker, template, defaults, requiredValues, new List<MatchProcessorReference>(), order, metadata, displayName)
{
}
public MatcherEndpoint(
Func<RequestDelegate, RequestDelegate> invoker,
string template,
RouteValueDictionary defaults,
RouteValueDictionary requiredValues,
List<MatchProcessorReference> nonInlineMatchProcessorReferences,
int order,
EndpointMetadataCollection metadata,
string displayName)
: base(metadata, displayName)
{
if (invoker == null)
@ -44,89 +30,24 @@ namespace Microsoft.AspNetCore.Routing.Matchers
throw new ArgumentNullException(nameof(invoker));
}
if (template == null)
if (routePattern == null)
{
throw new ArgumentNullException(nameof(template));
throw new ArgumentNullException(nameof(routePattern));
}
Invoker = invoker;
Order = order;
Template = template;
ParsedTemplate = TemplateParser.Parse(template);
RoutePattern = routePattern;
RequiredValues = requiredValues;
var mergedDefaults = GetDefaults(ParsedTemplate, defaults);
Defaults = mergedDefaults;
var mergedReferences = MergeMatchProcessorReferences(ParsedTemplate, nonInlineMatchProcessorReferences);
MatchProcessorReferences = mergedReferences.AsReadOnly();
Order = order;
}
public Func<RequestDelegate, RequestDelegate> Invoker { get; }
public int Order { get; }
public Func<RequestDelegate, RequestDelegate> Invoker { get; }
public string Template { get; }
public RouteValueDictionary Defaults { get; }
// Values required by an endpoint for it to be successfully matched on link generation
public RouteValueDictionary RequiredValues { get; }
public IReadOnlyDictionary<string, object> RequiredValues { get; }
// Todo: needs review
public RouteTemplate ParsedTemplate { get; }
public IReadOnlyList<MatchProcessorReference> MatchProcessorReferences { get; }
// Merge inline and non inline defaults into one
private RouteValueDictionary GetDefaults(RouteTemplate parsedTemplate, RouteValueDictionary nonInlineDefaults)
{
var result = nonInlineDefaults == null ? new RouteValueDictionary() : new RouteValueDictionary(nonInlineDefaults);
foreach (var parameter in parsedTemplate.Parameters)
{
if (parameter.DefaultValue != null)
{
if (result.ContainsKey(parameter.Name))
{
throw new InvalidOperationException(
Resources.FormatTemplateRoute_CannotHaveDefaultValueSpecifiedInlineAndExplicitly(
parameter.Name));
}
else
{
result.Add(parameter.Name, parameter.DefaultValue);
}
}
}
return result;
}
private List<MatchProcessorReference> MergeMatchProcessorReferences(
RouteTemplate parsedTemplate,
List<MatchProcessorReference> nonInlineReferences)
{
var matchProcessorReferences = new List<MatchProcessorReference>();
if (nonInlineReferences != null)
{
matchProcessorReferences.AddRange(nonInlineReferences);
}
foreach (var parameter in parsedTemplate.Parameters)
{
if (parameter.InlineConstraints != null)
{
foreach (var constraint in parameter.InlineConstraints)
{
matchProcessorReferences.Add(
new MatchProcessorReference(
parameter.Name,
optional: parameter.IsOptional,
constraintText: constraint.Constraint));
}
}
}
return matchProcessorReferences;
}
public RoutePattern RoutePattern { get; }
}
}

View File

@ -0,0 +1,61 @@
// 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 Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Routing.Matchers
{
internal class RouteConstraintMatchProcessor : MatchProcessor
{
public RouteConstraintMatchProcessor(string parameterName, IRouteConstraint constraint)
{
ParameterName = parameterName;
Constraint = constraint;
}
public string ParameterName { get; }
public IRouteConstraint Constraint { get; }
public override bool ProcessInbound(HttpContext httpContext, RouteValueDictionary values)
{
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}
if (values == null)
{
throw new ArgumentNullException(nameof(values));
}
return Constraint.Match(
httpContext,
NullRouter.Instance,
ParameterName,
values,
RouteDirection.IncomingRequest);
}
public override bool ProcessOutbound(HttpContext httpContext, RouteValueDictionary values)
{
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}
if (values == null)
{
throw new ArgumentNullException(nameof(values));
}
return Constraint.Match(
httpContext,
NullRouter.Instance,
ParameterName,
values,
RouteDirection.UrlGeneration);
}
}
}

View File

@ -121,7 +121,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns
case null:
state = ParseState.End;
var constraintText = text.Substring(startIndex, currentIndex - startIndex);
constraints.Add(RoutePatternFactory.Constraint(parameterName, constraintText));
constraints.Add(RoutePatternFactory.Constraint(constraintText));
break;
case ')':
// Only consume a ')' token if
@ -135,18 +135,18 @@ namespace Microsoft.AspNetCore.Routing.Patterns
case null:
state = ParseState.End;
constraintText = text.Substring(startIndex, currentIndex - startIndex + 1);
constraints.Add(RoutePatternFactory.Constraint(parameterName, constraintText));
constraints.Add(RoutePatternFactory.Constraint(constraintText));
break;
case ':':
state = ParseState.Start;
constraintText = text.Substring(startIndex, currentIndex - startIndex + 1);
constraints.Add(RoutePatternFactory.Constraint(parameterName, constraintText));
constraints.Add(RoutePatternFactory.Constraint(constraintText));
startIndex = currentIndex + 1;
break;
case '=':
state = ParseState.End;
constraintText = text.Substring(startIndex, currentIndex - startIndex + 1);
constraints.Add(RoutePatternFactory.Constraint(parameterName, constraintText));
constraints.Add(RoutePatternFactory.Constraint(constraintText));
break;
}
break;
@ -161,7 +161,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns
if (indexOfClosingParantheses == -1)
{
constraintText = text.Substring(startIndex, currentIndex - startIndex);
constraints.Add(RoutePatternFactory.Constraint(parameterName, constraintText));
constraints.Add(RoutePatternFactory.Constraint(constraintText));
if (currentChar == ':')
{
@ -190,14 +190,14 @@ namespace Microsoft.AspNetCore.Routing.Patterns
var constraintText = text.Substring(startIndex, currentIndex - startIndex);
if (constraintText.Length > 0)
{
constraints.Add(RoutePatternFactory.Constraint(parameterName, constraintText));
constraints.Add(RoutePatternFactory.Constraint(constraintText));
}
break;
case ':':
constraintText = text.Substring(startIndex, currentIndex - startIndex);
if (constraintText.Length > 0)
{
constraints.Add(RoutePatternFactory.Constraint(parameterName, constraintText));
constraints.Add(RoutePatternFactory.Constraint(constraintText));
}
startIndex = currentIndex + 1;
break;
@ -209,7 +209,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns
constraintText = text.Substring(startIndex, currentIndex - startIndex);
if (constraintText.Length > 0)
{
constraints.Add(RoutePatternFactory.Constraint(parameterName, constraintText));
constraints.Add(RoutePatternFactory.Constraint(constraintText));
}
currentIndex--;
break;

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Diagnostics;
using Microsoft.AspNetCore.Routing.Matchers;
namespace Microsoft.AspNetCore.Routing.Patterns
{
@ -11,18 +12,21 @@ namespace Microsoft.AspNetCore.Routing.Patterns
[DebuggerDisplay("{DebuggerToString()}")]
public sealed class RoutePatternConstraintReference
{
internal RoutePatternConstraintReference(string parameterName, string content)
internal RoutePatternConstraintReference(string content)
{
ParameterName = parameterName;
Content = content;
}
internal RoutePatternConstraintReference(string parameterName, IRouteConstraint constraint)
internal RoutePatternConstraintReference(IRouteConstraint constraint)
{
ParameterName = parameterName;
Constraint = constraint;
}
internal RoutePatternConstraintReference(MatchProcessor matchProcessor)
{
MatchProcessor = matchProcessor;
}
/// <summary>
/// Gets the constraint text.
/// </summary>
@ -34,9 +38,9 @@ namespace Microsoft.AspNetCore.Routing.Patterns
public IRouteConstraint Constraint { get; }
/// <summary>
/// Gets the parameter name associated with the constraint.
/// Gets a pre-existing <see cref="Matchers.MatchProcessor"/> that was used to construct this reference.
/// </summary>
public string ParameterName { get; }
public MatchProcessor MatchProcessor { get; }
private string DebuggerToString()
{

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Routing.Constraints;
using Microsoft.AspNetCore.Routing.Matchers;
namespace Microsoft.AspNetCore.Routing.Patterns
{
@ -158,7 +159,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns
{
updatedConstraints.Add(kvp.Key, new List<RoutePatternConstraintReference>()
{
Constraint(kvp.Key, kvp.Value),
Constraint(kvp.Value),
});
}
}
@ -450,55 +451,73 @@ namespace Microsoft.AspNetCore.Routing.Patterns
return new RoutePatternParameterPart(parameterName, @default, parameterKind, constraints.ToArray());
}
public static RoutePatternConstraintReference Constraint(string parameterName, object constraint)
public static RoutePatternConstraintReference Constraint(object constraint)
{
// Similar to RouteConstraintBuilder
if (constraint is IRouteConstraint routeConstraint)
{
return ConstraintCore(parameterName, routeConstraint);
return ConstraintCore(routeConstraint);
}
else if (constraint is MatchProcessor matchProcessor)
{
return ConstraintCore(matchProcessor);
}
else if (constraint is string content)
{
return ConstraintCore(parameterName, new RegexRouteConstraint("^(" + content + ")$"));
return ConstraintCore(new RegexRouteConstraint("^(" + content + ")$"));
}
else
{
throw new InvalidOperationException(Resources.FormatConstraintMustBeStringOrConstraint(
parameterName,
constraint,
typeof(IRouteConstraint)));
throw new InvalidOperationException(Resources.FormatRoutePattern_InvalidConstraintReference(
constraint ?? "null",
typeof(IRouteConstraint),
typeof(MatchProcessor)));
}
}
public static RoutePatternConstraintReference Constraint(string parameterName, IRouteConstraint constraint)
public static RoutePatternConstraintReference Constraint(IRouteConstraint constraint)
{
if (constraint == null)
{
throw new ArgumentNullException(nameof(constraint));
}
return ConstraintCore(parameterName, constraint);
return ConstraintCore(constraint);
}
public static RoutePatternConstraintReference Constraint(string parameterName, string constraint)
public static RoutePatternConstraintReference Constraint(MatchProcessor matchProcessor)
{
if (matchProcessor == null)
{
throw new ArgumentNullException(nameof(matchProcessor));
}
return ConstraintCore(matchProcessor);
}
public static RoutePatternConstraintReference Constraint(string constraint)
{
if (string.IsNullOrEmpty(constraint))
{
throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(constraint));
}
return ConstraintCore(parameterName, constraint);
return ConstraintCore(constraint);
}
private static RoutePatternConstraintReference ConstraintCore(string parameterName, IRouteConstraint constraint)
private static RoutePatternConstraintReference ConstraintCore(string constraint)
{
return new RoutePatternConstraintReference(parameterName, constraint);
return new RoutePatternConstraintReference(constraint);
}
private static RoutePatternConstraintReference ConstraintCore(string parameterName, string constraint)
private static RoutePatternConstraintReference ConstraintCore(IRouteConstraint constraint)
{
return new RoutePatternConstraintReference(parameterName, constraint);
return new RoutePatternConstraintReference(constraint);
}
private static RoutePatternConstraintReference ConstraintCore(MatchProcessor matchProcessor)
{
return new RoutePatternConstraintReference(matchProcessor);
}
}
}

View File

@ -458,6 +458,62 @@ namespace Microsoft.AspNetCore.Routing
internal static string FormatConstraintMustBeStringOrConstraint(object p0, object p1, object p2)
=> string.Format(CultureInfo.CurrentCulture, GetString("ConstraintMustBeStringOrConstraint"), p0, p1, p2);
/// <summary>
/// Invalid constraint '{0}'. A constraint must be of type 'string', '{1}', or '{2}'.
/// </summary>
internal static string RoutePattern_InvalidConstraintReference
{
get => GetString("RoutePattern_InvalidConstraintReference");
}
/// <summary>
/// Invalid constraint '{0}'. A constraint must be of type 'string', '{1}', or '{2}'.
/// </summary>
internal static string FormatRoutePattern_InvalidConstraintReference(object p0, object p1, object p2)
=> string.Format(CultureInfo.CurrentCulture, GetString("RoutePattern_InvalidConstraintReference"), p0, p1, p2);
/// <summary>
/// Invalid constraint '{0}' for parameter '{1}'. A constraint must be of type 'string', '{2}', or '{3}'.
/// </summary>
internal static string RoutePattern_InvalidParameterConstraintReference
{
get => GetString("RoutePattern_InvalidParameterConstraintReference");
}
/// <summary>
/// Invalid constraint '{0}' for parameter '{1}'. A constraint must be of type 'string', '{2}', or '{3}'.
/// </summary>
internal static string FormatRoutePattern_InvalidParameterConstraintReference(object p0, object p1, object p2, object p3)
=> string.Format(CultureInfo.CurrentCulture, GetString("RoutePattern_InvalidParameterConstraintReference"), p0, p1, p2, p3);
/// <summary>
/// The constraint reference '{0}' could not be resolved to a type. Register the constraint type with '{1}.{2}'.
/// </summary>
internal static string RoutePattern_ConstraintReferenceNotFound
{
get => GetString("RoutePattern_ConstraintReferenceNotFound");
}
/// <summary>
/// The constraint reference '{0}' could not be resolved to a type. Register the constraint type with '{1}.{2}'.
/// </summary>
internal static string FormatRoutePattern_ConstraintReferenceNotFound(object p0, object p1, object p2)
=> string.Format(CultureInfo.CurrentCulture, GetString("RoutePattern_ConstraintReferenceNotFound"), p0, p1, p2);
/// <summary>
/// Invalid constraint type '{0}' registered as '{1}'. A constraint type must either implement '{2}', or inherit from '{3}'.
/// </summary>
internal static string RoutePattern_InvalidStringConstraintReference
{
get => GetString("RoutePattern_InvalidStringConstraintReference");
}
/// <summary>
/// Invalid constraint type '{0}' registered as '{1}'. A constraint type must either implement '{2}', or inherit from '{3}'.
/// </summary>
internal static string FormatRoutePattern_InvalidStringConstraintReference(object p0, object p1, object p2, object p3)
=> string.Format(CultureInfo.CurrentCulture, GetString("RoutePattern_InvalidStringConstraintReference"), p0, p1, p2, p3);
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);

View File

@ -213,4 +213,16 @@
<data name="ConstraintMustBeStringOrConstraint" xml:space="preserve">
<value>The constraint entry '{0}' - '{1}' must have a string value or be of a type which implements '{2}'.</value>
</data>
<data name="RoutePattern_InvalidConstraintReference" xml:space="preserve">
<value>Invalid constraint '{0}'. A constraint must be of type 'string', '{1}', or '{2}'.</value>
</data>
<data name="RoutePattern_InvalidParameterConstraintReference" xml:space="preserve">
<value>Invalid constraint '{0}' for parameter '{1}'. A constraint must be of type 'string', '{2}', or '{3}'.</value>
</data>
<data name="RoutePattern_ConstraintReferenceNotFound" xml:space="preserve">
<value>The constraint reference '{0}' could not be resolved to a type. Register the constraint type with '{1}.{2}'.</value>
</data>
<data name="RoutePattern_InvalidStringConstraintReference" xml:space="preserve">
<value>Invalid constraint type '{0}' registered as '{1}'. A constraint type must either implement '{2}', or inherit from '{3}'.</value>
</data>
</root>

View File

@ -120,13 +120,13 @@ namespace Microsoft.AspNetCore.Routing
{
Handler = NullRouter.Instance,
Order = endpoint.Order,
Precedence = RoutePrecedence.ComputeOutbound(endpoint.ParsedTemplate),
RequiredLinkValues = endpoint.RequiredValues,
RouteTemplate = endpoint.ParsedTemplate,
Precedence = RoutePrecedence.ComputeOutbound(endpoint.RoutePattern),
RequiredLinkValues = new RouteValueDictionary(endpoint.RequiredValues),
RouteTemplate = new RouteTemplate(endpoint.RoutePattern),
Data = endpoint,
RouteName = routeNameMetadata?.Name,
};
entry.Defaults = endpoint.Defaults;
entry.Defaults = new RouteValueDictionary(endpoint.RoutePattern.Defaults);
return entry;
}

View File

@ -4,6 +4,7 @@
using System;
using System.Diagnostics;
using System.Linq;
using Microsoft.AspNetCore.Routing.Patterns;
namespace Microsoft.AspNetCore.Routing.Template
{
@ -36,6 +37,24 @@ namespace Microsoft.AspNetCore.Routing.Template
return precedence;
}
// See description on ComputeInbound(RouteTemplate)
internal static decimal ComputeInbound(RoutePattern routePattern)
{
var precedence = 0m;
for (var i = 0; i < routePattern.PathSegments.Count; i++)
{
var segment = routePattern.PathSegments[i];
var digit = ComputeInboundPrecedenceDigit(segment);
Debug.Assert(digit >= 0 && digit < 10);
precedence += decimal.Divide(digit, (decimal)Math.Pow(10, i));
}
return precedence;
}
// Compute the precedence for generating a url
// e.g.: /api/template == 5.5
// /api/template/{id} == 5.53
@ -60,6 +79,26 @@ namespace Microsoft.AspNetCore.Routing.Template
return precedence;
}
// see description on ComputeOutbound(RouteTemplate)
internal static decimal ComputeOutbound(RoutePattern routePattern)
{
// Each precedence digit corresponds to one decimal place. For example, 3 segments with precedences 2, 1,
// and 4 results in a combined precedence of 2.14 (decimal).
var precedence = 0m;
for (var i = 0; i < routePattern.PathSegments.Count; i++)
{
var segment = routePattern.PathSegments[i];
var digit = ComputeOutboundPrecedenceDigit(segment);
Debug.Assert(digit >= 0 && digit < 10);
precedence += decimal.Divide(digit, (decimal)Math.Pow(10, i));
}
return precedence;
}
// Segments have the following order:
// 5 - Literal segments
// 4 - Multi-part segments && Constrained parameter segments
@ -92,6 +131,38 @@ namespace Microsoft.AspNetCore.Routing.Template
}
}
// See description on ComputeOutboundPrecedenceDigit(TemplateSegment segment)
private static int ComputeOutboundPrecedenceDigit(RoutePatternPathSegment pathSegment)
{
if (pathSegment.Parts.Count > 1)
{
return 4;
}
var part = pathSegment.Parts[0];
if (part.IsLiteral)
{
return 5;
}
else if (part is RoutePatternParameterPart parameterPart)
{
Debug.Assert(parameterPart != null);
var digit = parameterPart.IsCatchAll ? 1 : 3;
if (parameterPart.Constraints.Count > 0)
{
digit++;
}
return digit;
}
else
{
// Unreachable
throw new NotSupportedException();
}
}
// Segments have the following order:
// 1 - Literal segments
// 2 - Constrained parameter segments / Multi-part segments
@ -127,5 +198,40 @@ namespace Microsoft.AspNetCore.Routing.Template
return digit;
}
}
// see description on ComputeInboundPrecedenceDigit(TemplateSegment segment)
private static int ComputeInboundPrecedenceDigit(RoutePatternPathSegment pathSegment)
{
if (pathSegment.Parts.Count > 1)
{
// Multi-part segments should appear after literal segments and along with parameter segments
return 2;
}
var part = pathSegment.Parts[0];
// Literal segments always go first
if (part.IsLiteral)
{
return 1;
}
else if (part is RoutePatternParameterPart parameterPart)
{
var digit = parameterPart.IsCatchAll ? 5 : 3;
// If there is a route constraint for the parameter, reduce order by 1
// Constrained parameters end up with order 2, Constrained catch alls end up with order 4
if (parameterPart.Constraints.Count > 0)
{
digit--;
}
return digit;
}
else
{
// Unreachable
throw new NotSupportedException();
}
}
}
}

View File

@ -118,7 +118,7 @@ namespace Microsoft.AspNetCore.Routing.Template
RoutePatternParameterKind.Optional :
RoutePatternParameterKind.Standard;
var constraints = InlineConstraints.Select(c => new RoutePatternConstraintReference(Name, c.Constraint));
var constraints = InlineConstraints.Select(c => new RoutePatternConstraintReference(c.Constraint));
return RoutePatternFactory.ParameterPart(Name, DefaultValue, kind, constraints);
}
}

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Routing.Matchers;
using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.AspNetCore.Routing.TestObjects;
using Microsoft.Extensions.Primitives;
using Xunit;
@ -147,19 +148,15 @@ namespace Microsoft.AspNetCore.Routing
private MatcherEndpoint CreateEndpoint(
string template,
object defaultValues = null,
object defaults = null,
object requiredValues = null,
int order = 0,
string routeName = null)
{
var defaults = defaultValues == null ? new RouteValueDictionary() : new RouteValueDictionary(defaultValues);
var required = requiredValues == null ? new RouteValueDictionary() : new RouteValueDictionary(requiredValues);
return new MatcherEndpoint(
next => (httpContext) => Task.CompletedTask,
template,
defaults,
required,
MatcherEndpoint.EmptyInvoker,
RoutePatternFactory.Parse(template, defaults, constraints: null),
new RouteValueDictionary(requiredValues),
order,
EndpointMetadataCollection.Empty,
null);

View File

@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Routing.Constraints;
using Microsoft.AspNetCore.Routing.EndpointFinders;
using Microsoft.AspNetCore.Routing.Internal;
using Microsoft.AspNetCore.Routing.Matchers;
using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.AspNetCore.Routing.TestObjects;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.ObjectPool;
@ -254,12 +255,11 @@ namespace Microsoft.AspNetCore.Routing
// Arrange
var context = CreateRouteValuesContext(new { p1 = "abcd" });
var linkGenerator = CreateLinkGenerator();
var matchProcessorReferences = new List<MatchProcessorReference>();
matchProcessorReferences.Add(new MatchProcessorReference("p2", new RegexRouteConstraint("\\d{4}")));
var endpoint = CreateEndpoint(
"{p1}/{p2}",
new { p2 = "catchall" },
matchProcessorReferences: matchProcessorReferences);
constraints: new { p2 = "\\d{4}" });
// Act
var canGenerateLink = linkGenerator.TryGetLink(
@ -279,12 +279,11 @@ namespace Microsoft.AspNetCore.Routing
// Arrange
var context = CreateRouteValuesContext(new { p1 = "hello", p2 = "1234" });
var linkGenerator = CreateLinkGenerator();
var matchProcessorReferences = new List<MatchProcessorReference>();
matchProcessorReferences.Add(new MatchProcessorReference("p2", new RegexRouteConstraint("\\d{4}")));
var endpoint = CreateEndpoint(
"{p1}/{p2}",
new { p2 = "catchall" },
matchProcessorReferences);
new { p2 = new RegexRouteConstraint("\\d{4}"), });
// Act
var canGenerateLink = linkGenerator.TryGetLink(
@ -305,12 +304,11 @@ namespace Microsoft.AspNetCore.Routing
// Arrange
var context = CreateRouteValuesContext(new { p1 = "abcd" });
var linkGenerator = CreateLinkGenerator();
var matchProcessorReferences = new List<MatchProcessorReference>();
matchProcessorReferences.Add(new MatchProcessorReference("p2", new RegexRouteConstraint("\\d{4}")));
var endpoint = CreateEndpoint(
"{p1}/{*p2}",
new { p2 = "catchall" },
matchProcessorReferences: matchProcessorReferences);
new { p2 = new RegexRouteConstraint("\\d{4}") });
// Act
var canGenerateLink = linkGenerator.TryGetLink(
@ -330,12 +328,11 @@ namespace Microsoft.AspNetCore.Routing
// Arrange
var context = CreateRouteValuesContext(new { p1 = "hello", p2 = "1234" });
var linkGenerator = CreateLinkGenerator();
var matchProcessorReferences = new List<MatchProcessorReference>();
matchProcessorReferences.Add(new MatchProcessorReference("p2", new RegexRouteConstraint("\\d{4}")));
var endpoint = CreateEndpoint(
"{p1}/{*p2}",
new { p2 = "catchall" },
matchProcessorReferences);
new { p2 = new RegexRouteConstraint("\\d{4}") });
// Act
var canGenerateLink = linkGenerator.TryGetLink(
@ -370,11 +367,8 @@ namespace Microsoft.AspNetCore.Routing
var endpoint = CreateEndpoint(
"{p1}/{p2}",
defaultValues: new { p2 = "catchall" },
matchProcessorReferences: new List<MatchProcessorReference>
{
new MatchProcessorReference("p2", target.Object)
});
defaults: new { p2 = "catchall" },
constraints: new { p2 = target.Object });
// Act
var canGenerateLink = linkGenerator.TryGetLink(
@ -400,11 +394,8 @@ namespace Microsoft.AspNetCore.Routing
var linkGenerator = CreateLinkGenerator();
var endpoint = CreateEndpoint(
template: "slug/Home/Store",
defaultValues: new { controller = "Home", action = "Store" },
matchProcessorReferences: new List<MatchProcessorReference>()
{
new MatchProcessorReference("c", constraint)
});
defaults: new { controller = "Home", action = "Store" },
constraints: new { c = constraint });
var context = CreateRouteValuesContext(
suppliedValues: new { action = "Store" },
@ -437,11 +428,8 @@ namespace Microsoft.AspNetCore.Routing
var linkGenerator = CreateLinkGenerator();
var endpoint = CreateEndpoint(
template: "slug/Home/Store",
defaultValues: new { controller = "Home", action = "Store", otherthing = "17" },
matchProcessorReferences: new List<MatchProcessorReference>()
{
new MatchProcessorReference("c", constraint)
});
defaults: new { controller = "Home", action = "Store", otherthing = "17" },
constraints: new { c = constraint });
var context = CreateRouteValuesContext(
suppliedValues: new { action = "Store" },
@ -471,11 +459,8 @@ namespace Microsoft.AspNetCore.Routing
var linkGenerator = CreateLinkGenerator();
var endpoint = CreateEndpoint(
template: "slug/{controller}/{action}",
defaultValues: new { action = "Index" },
matchProcessorReferences: new List<MatchProcessorReference>()
{
new MatchProcessorReference("c", constraint)
});
defaults: new { action = "Index" },
constraints: new { c = constraint, });
var context = CreateRouteValuesContext(
suppliedValues: new { controller = "Shopping" },
@ -506,11 +491,8 @@ namespace Microsoft.AspNetCore.Routing
var linkGenerator = CreateLinkGenerator();
var endpoint = CreateEndpoint(
template: "slug/Home/Store",
defaultValues: new { controller = "Home", action = "Store", otherthing = "17", thirdthing = "13" },
matchProcessorReferences: new List<MatchProcessorReference>()
{
new MatchProcessorReference("c", constraint)
});
defaults: new { controller = "Home", action = "Store", otherthing = "17", thirdthing = "13" },
constraints: new { c = constraint, });
var context = CreateRouteValuesContext(
suppliedValues: new { action = "Store", thirdthing = "13" },
@ -537,12 +519,10 @@ namespace Microsoft.AspNetCore.Routing
// Arrange
var linkGenerator = CreateLinkGenerator();
var endpoint = CreateEndpoint(
template: "Home/Index/{id}",
defaultValues: new { controller = "Home", action = "Index" },
matchProcessorReferences: new List<MatchProcessorReference>()
{
new MatchProcessorReference("id", "int")
});
template: "Home/Index/{id:int}",
defaults: new { controller = "Home", action = "Index" },
constraints: new { });
var context = CreateRouteValuesContext(
suppliedValues: new { action = "Index", controller = "Home", id = 4 });
@ -564,11 +544,9 @@ namespace Microsoft.AspNetCore.Routing
var linkGenerator = CreateLinkGenerator();
var endpoint = CreateEndpoint(
template: "Home/Index/{id}",
defaultValues: new { controller = "Home", action = "Index" },
matchProcessorReferences: new List<MatchProcessorReference>()
{
new MatchProcessorReference("id", "int")
});
defaults: new { controller = "Home", action = "Index" },
constraints: new {id = "int"});
var context = CreateRouteValuesContext(
suppliedValues: new { action = "Index", controller = "Home", id = "not-an-integer" });
@ -590,12 +568,9 @@ namespace Microsoft.AspNetCore.Routing
// Arrange
var linkGenerator = CreateLinkGenerator();
var endpoint = CreateEndpoint(
template: "Home/Index/{id}",
defaultValues: new { controller = "Home", action = "Index" },
matchProcessorReferences: new List<MatchProcessorReference>()
{
new MatchProcessorReference("id", optional: true, "int")
});
template: "Home/Index/{id:int?}",
defaults: new { controller = "Home", action = "Index" },
constraints: new { });
var context = CreateRouteValuesContext(
suppliedValues: new { action = "Index", controller = "Home", id = 98 });
@ -617,11 +592,9 @@ namespace Microsoft.AspNetCore.Routing
var linkGenerator = CreateLinkGenerator();
var endpoint = CreateEndpoint(
template: "Home/Index/{id?}",
defaultValues: new { controller = "Home", action = "Index" },
matchProcessorReferences: new List<MatchProcessorReference>()
{
new MatchProcessorReference("id", optional: true, "int")
});
defaults: new { controller = "Home", action = "Index" },
constraints: new { id = "int" });
var context = CreateRouteValuesContext(
suppliedValues: new { action = "Index", controller = "Home" });
@ -642,12 +615,10 @@ namespace Microsoft.AspNetCore.Routing
// Arrange
var linkGenerator = CreateLinkGenerator();
var endpoint = CreateEndpoint(
template: "Home/Index/{id}",
defaultValues: new { controller = "Home", action = "Index" },
matchProcessorReferences: new List<MatchProcessorReference>()
{
new MatchProcessorReference("id", optional: true, "int")
});
template: "Home/Index/{id?}",
defaults: new { controller = "Home", action = "Index" },
constraints: new { id = "int" });
var context = CreateRouteValuesContext(
suppliedValues: new { action = "Index", controller = "Home", id = "not-an-integer" });
@ -664,18 +635,15 @@ namespace Microsoft.AspNetCore.Routing
}
[Fact]
public void GetLink_InlineConstraints_CompositeInlineConstraint()
public void GetLink_InlineConstraints_MultipleInlineConstraints()
{
// Arrange
var linkGenerator = CreateLinkGenerator();
var endpoint = CreateEndpoint(
template: "Home/Index/{id}",
defaultValues: new { controller = "Home", action = "Index" },
matchProcessorReferences: new List<MatchProcessorReference>()
{
new MatchProcessorReference("id", "int"),
new MatchProcessorReference("id", "range(1,20)")
});
template: "Home/Index/{id:int:range(1,20)}",
defaults: new { controller = "Home", action = "Index" },
constraints: new { });
var context = CreateRouteValuesContext(
suppliedValues: new { action = "Index", controller = "Home", id = 14 });
@ -696,13 +664,10 @@ namespace Microsoft.AspNetCore.Routing
// Arrange
var linkGenerator = CreateLinkGenerator();
var endpoint = CreateEndpoint(
template: "Home/Index/{id}",
defaultValues: new { controller = "Home", action = "Index" },
matchProcessorReferences: new List<MatchProcessorReference>()
{
new MatchProcessorReference("id", "int"),
new MatchProcessorReference("id", "range(1,20)")
});
template: "Home/Index/{id:int:range(1,20)}",
defaults: new { controller = "Home", action = "Index" },
constraints: new { });
var context = CreateRouteValuesContext(
suppliedValues: new { action = "Index", controller = "Home", id = 50 });
@ -726,11 +691,9 @@ namespace Microsoft.AspNetCore.Routing
var linkGenerator = CreateLinkGenerator();
var endpoint = CreateEndpoint(
template: "Home/Index/{name}",
defaultValues: new { controller = "Home", action = "Index" },
matchProcessorReferences: new List<MatchProcessorReference>()
{
new MatchProcessorReference("name", constraint)
});
defaults: new { controller = "Home", action = "Index" },
constraints: new { name = constraint });
var context = CreateRouteValuesContext(
suppliedValues: new { action = "Index", controller = "Home", name = "products" });
@ -785,50 +748,6 @@ namespace Microsoft.AspNetCore.Routing
Assert.Equal("/Home/Index", link);
}
[Fact]
public void GetLink_OptionalParameter_ParameterPresentInValuesAndDefaults()
{
// Arrange
var endpoint = CreateEndpoint(
template: "{controller}/{action}/{name?}",
defaultValues: new { name = "default-products" });
var linkGenerator = CreateLinkGenerator();
var context = CreateRouteValuesContext(
suppliedValues: new { action = "Index", controller = "Home", name = "products" });
// Act
var link = linkGenerator.GetLink(
httpContext: null,
new[] { endpoint },
context.ExplicitValues,
context.AmbientValues);
// Assert
Assert.Equal("/Home/Index/products", link);
}
[Fact]
public void GetLink_OptionalParameter_ParameterNotPresentInValues_PresentInDefaults()
{
// Arrange
var endpoint = CreateEndpoint(
template: "{controller}/{action}/{name?}",
defaultValues: new { name = "products" });
var linkGenerator = CreateLinkGenerator();
var context = CreateRouteValuesContext(
suppliedValues: new { action = "Index", controller = "Home" });
// Act
var link = linkGenerator.GetLink(
httpContext: null,
new[] { endpoint },
context.ExplicitValues,
context.AmbientValues);
// Assert
Assert.Equal("/Home/Index", link);
}
[Fact]
public void GetLink_ParameterNotPresentInTemplate_PresentInValues()
{
@ -958,7 +877,7 @@ namespace Microsoft.AspNetCore.Routing
public void GetLink_TwoOptionalParametersAfterDefault_LastValueFromAmbientValues()
{
// Arrange
var endpoint = CreateEndpoint("a/{b=15}/{c?}/{d?}", defaultValues: new { });
var endpoint = CreateEndpoint("a/{b=15}/{c?}/{d?}");
var linkGenerator = CreateLinkGenerator();
var context = CreateRouteValuesContext(
suppliedValues: new { },
@ -985,23 +904,15 @@ namespace Microsoft.AspNetCore.Routing
private MatcherEndpoint CreateEndpoint(
string template,
object defaultValues = null,
object requiredValues = null,
List<MatchProcessorReference> matchProcessorReferences = null,
int order = 0,
EndpointMetadataCollection metadata = null)
object defaults = null,
object constraints = null,
int order = 0,
EndpointMetadataCollection metadata = null)
{
var defaults = defaultValues == null ? new RouteValueDictionary() : new RouteValueDictionary(defaultValues);
var required = requiredValues == null ? new RouteValueDictionary() : new RouteValueDictionary(requiredValues);
metadata = metadata ?? EndpointMetadataCollection.Empty;
matchProcessorReferences = matchProcessorReferences ?? new List<MatchProcessorReference>();
return new MatcherEndpoint(
next => (httpContext) => Task.CompletedTask,
template,
defaults,
required,
matchProcessorReferences,
MatcherEndpoint.EmptyInvoker,
RoutePatternFactory.Parse(template, defaults, constraints),
new RouteValueDictionary(),
order,
metadata,
null);

View File

@ -3,6 +3,7 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.AspNetCore.Routing.Template;
using static Microsoft.AspNetCore.Routing.Matchers.BarebonesMatcher;
@ -22,11 +23,11 @@ namespace Microsoft.AspNetCore.Routing.Matchers
var matchers = new InnerMatcher[_endpoints.Count];
for (var i = 0; i < _endpoints.Count; i++)
{
var parsed = TemplateParser.Parse(_endpoints[i].Template);
var segments = parsed.Segments
.Select(s => s.IsSimple && s.Parts[0].IsLiteral ? s.Parts[0].Text : null)
var endpoint = _endpoints[i];
var pathSegments = endpoint.RoutePattern.PathSegments
.Select(s => s.IsSimple && s.Parts[0] is RoutePatternLiteralPart literalPart ? literalPart.Content : null)
.ToArray();
matchers[i] = new InnerMatcher(segments, _endpoints[i]);
matchers[i] = new InnerMatcher(pathSegments, _endpoints[i]);
}
return new BarebonesMatcher(matchers);

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.AspNetCore.Routing.TestObjects;
using Xunit;
@ -35,8 +36,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
var endpoint = new MatcherEndpoint(
MatcherEndpoint.EmptyInvoker,
"a/b/c",
new RouteValueDictionary(),
RoutePatternFactory.Parse("a/b/c"),
new RouteValueDictionary(),
0,
EndpointMetadataCollection.Empty,

View File

@ -4,10 +4,10 @@
using System;
using System.Globalization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing.Constraints;
using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Routing.Matchers
@ -19,11 +19,153 @@ namespace Microsoft.AspNetCore.Routing.Matchers
{
// Arrange
var factory = GetMatchProcessorFactory();
var matchProcessorReference = new MatchProcessorReference("id", @"notpresent(\d+)");
// Act & Assert
// Act
var exception = Assert.Throws<InvalidOperationException>(
() => factory.Create(matchProcessorReference));
() => factory.Create("id", @"notpresent(\d+)", optional: false));
// Assert
Assert.Equal(
$"The constraint reference 'notpresent' could not be resolved to a type. " +
$"Register the constraint type with '{typeof(RouteOptions)}.{nameof(RouteOptions.ConstraintMap)}'.",
exception.Message);
}
[Fact]
public void Create_ThrowsException_OnInvalidType()
{
// Arrange
var options = new RouteOptions();
options.ConstraintMap.Add("bad", typeof(string));
var services = new ServiceCollection();
services.AddTransient<EndsWithStringMatchProcessor>();
var factory = GetMatchProcessorFactory(options, services);
// Act
var exception = Assert.Throws<InvalidOperationException>(
() => factory.Create("id", @"bad", optional: false));
// Assert
Assert.Equal(
$"Invalid constraint type '{typeof(string)}' registered as 'bad'. " +
$"A constraint type must either implement '{typeof(IRouteConstraint)}', or inherit from '{typeof(MatchProcessor)}'.",
exception.Message);
}
[Fact]
public void Create_CreatesMatchProcessor_FromRoutePattern_String()
{
// Arrange
var factory = GetMatchProcessorFactory();
var parameter = RoutePatternFactory.ParameterPart(
"id",
@default: null,
parameterKind: RoutePatternParameterKind.Standard,
constraints: new[] { RoutePatternFactory.Constraint("int"), });
// Act
var matchProcessor = factory.Create(parameter, parameter.Constraints[0]);
// Assert
Assert.IsType<IntRouteConstraint>(Assert.IsType<RouteConstraintMatchProcessor>(matchProcessor).Constraint);
}
[Fact]
public void Create_CreatesMatchProcessor_FromRoutePattern_String_Optional()
{
// Arrange
var factory = GetMatchProcessorFactory();
var parameter = RoutePatternFactory.ParameterPart(
"id",
@default: null,
parameterKind: RoutePatternParameterKind.Optional,
constraints: new[] { RoutePatternFactory.Constraint("int"), });
// Act
var matchProcessor = factory.Create(parameter, parameter.Constraints[0]);
// Assert
Assert.IsType<OptionalMatchProcessor>(matchProcessor);
}
[Fact]
public void Create_CreatesMatchProcessor_FromRoutePattern_Constraint()
{
// Arrange
var factory = GetMatchProcessorFactory();
var parameter = RoutePatternFactory.ParameterPart(
"id",
@default: null,
parameterKind: RoutePatternParameterKind.Standard,
constraints: new[] { RoutePatternFactory.Constraint(new IntRouteConstraint()), });
// Act
var matchProcessor = factory.Create(parameter, parameter.Constraints[0]);
// Assert
Assert.IsType<IntRouteConstraint>(Assert.IsType<RouteConstraintMatchProcessor>(matchProcessor).Constraint);
}
[Fact]
public void Create_CreatesMatchProcessor_FromRoutePattern_Constraint_Optional()
{
// Arrange
var factory = GetMatchProcessorFactory();
var parameter = RoutePatternFactory.ParameterPart(
"id",
@default: null,
parameterKind: RoutePatternParameterKind.Optional,
constraints: new[] { RoutePatternFactory.Constraint(new IntRouteConstraint()), });
// Act
var matchProcessor = factory.Create(parameter, parameter.Constraints[0]);
// Assert
Assert.IsType<OptionalMatchProcessor>(matchProcessor);
}
[Fact]
public void Create_CreatesMatchProcessor_FromRoutePattern_MatchProcessor()
{
// Arrange
var factory = GetMatchProcessorFactory();
var parameter = RoutePatternFactory.ParameterPart(
"id",
@default: null,
parameterKind: RoutePatternParameterKind.Standard,
constraints: new[] { RoutePatternFactory.Constraint(new EndsWithStringMatchProcessor()), });
// Act
var matchProcessor = factory.Create(parameter, parameter.Constraints[0]);
// Assert
Assert.IsType<EndsWithStringMatchProcessor>(matchProcessor);
}
[Fact]
public void Create_CreatesMatchProcessor_FromRoutePattern_MatchProcessor_Optional()
{
// Arrange
var factory = GetMatchProcessorFactory();
var parameter = RoutePatternFactory.ParameterPart(
"id",
@default: null,
parameterKind: RoutePatternParameterKind.Optional,
constraints: new[] { RoutePatternFactory.Constraint(new EndsWithStringMatchProcessor()), });
// Act
var matchProcessor = factory.Create(parameter, parameter.Constraints[0]);
// Assert
Assert.IsType<OptionalMatchProcessor>(matchProcessor);
}
[Fact]
@ -31,29 +173,12 @@ namespace Microsoft.AspNetCore.Routing.Matchers
{
// Arrange
var factory = GetMatchProcessorFactory();
var matchProcessorReference = new MatchProcessorReference("id", "int");
// Act 1
var processor = factory.Create(matchProcessorReference);
// Act
var matchProcessor = factory.Create("id", "int", optional: false);
// Assert 1
Assert.NotNull(processor);
// Act 2
var isMatch = processor.ProcessInbound(
new DefaultHttpContext(),
new RouteValueDictionary(new { id = 10 }));
// Assert 2
Assert.True(isMatch);
// Act 2
isMatch = processor.ProcessInbound(
new DefaultHttpContext(),
new RouteValueDictionary(new { id = "foo" }));
// Assert 2
Assert.False(isMatch);
// Assert
Assert.IsType<IntRouteConstraint>(Assert.IsType<RouteConstraintMatchProcessor>(matchProcessor).Constraint);
}
[Fact]
@ -61,84 +186,50 @@ namespace Microsoft.AspNetCore.Routing.Matchers
{
// Arrange
var factory = GetMatchProcessorFactory();
var matchProcessorReference = new MatchProcessorReference("id", true, "int");
// Act
var processor = factory.Create(matchProcessorReference);
var matchProcessor = factory.Create("id", "int", optional: true);
// Assert
Assert.IsType<OptionalMatchProcessor>(processor);
Assert.IsType<OptionalMatchProcessor>(matchProcessor);
}
[Fact]
public void Create_CreatesMatchProcessor_FromConstraintText_AndCustomMatchProcessor()
public void Create_CreatesMatchProcessor_FromConstraintText_AndMatchProcesor()
{
// Arrange
var options = new RouteOptions();
options.ConstraintMap.Add("endsWith", typeof(EndsWithStringMatchProcessor));
var services = new ServiceCollection();
services.AddTransient<EndsWithStringMatchProcessor>();
var factory = GetMatchProcessorFactory(options, services);
var matchProcessorReference = new MatchProcessorReference("id", "endsWith(_001)");
// Act 1
var processor = factory.Create(matchProcessorReference);
// Act
var matchProcessor = factory.Create("id", "endsWith", optional: false);
// Assert 1
Assert.NotNull(processor);
// Act 2
var isMatch = processor.ProcessInbound(
new DefaultHttpContext(),
new RouteValueDictionary(new { id = "555_001" }));
// Assert 2
Assert.True(isMatch);
// Act 2
isMatch = processor.ProcessInbound(
new DefaultHttpContext(),
new RouteValueDictionary(new { id = "444" }));
// Assert 2
Assert.False(isMatch);
// Assert
Assert.IsType<EndsWithStringMatchProcessor>(matchProcessor);
}
[Fact]
public void Create_ReturnsMatchProcessor_IfAvailable()
public void Create_CreatesMatchProcessor_FromConstraintText_AndMatchProcessor_Optional()
{
// Arrange
var factory = GetMatchProcessorFactory();
var matchProcessorReference = new MatchProcessorReference("id", Mock.Of<MatchProcessor>());
var expected = matchProcessorReference.MatchProcessor;
var options = new RouteOptions();
options.ConstraintMap.Add("endsWith", typeof(EndsWithStringMatchProcessor));
var services = new ServiceCollection();
services.AddTransient<EndsWithStringMatchProcessor>();
var factory = GetMatchProcessorFactory(options, services);
// Act
var processor = factory.Create(matchProcessorReference);
var matchProcessor = factory.Create("id", "endsWith", optional: true);
// Assert
Assert.Same(expected, processor);
}
[Fact]
public void Create_ReturnsMatchProcessor_WithSuppliedRouteConstraint()
{
// Arrange
var factory = GetMatchProcessorFactory();
var constraint = TestRouteConstraint.Create();
var matchProcessorReference = new MatchProcessorReference("id", constraint);
var processor = factory.Create(matchProcessorReference);
var expectedHttpContext = new DefaultHttpContext();
var expectedValues = new RouteValueDictionary();
// Act
processor.ProcessInbound(expectedHttpContext, expectedValues);
// Assert
Assert.Same(expectedHttpContext, constraint.HttpContext);
Assert.Same(expectedValues, constraint.Values);
Assert.Equal("id", constraint.RouteKey);
Assert.Equal(RouteDirection.IncomingRequest, constraint.RouteDirection);
Assert.Same(NullRouter.Instance, constraint.Route);
Assert.IsType<OptionalMatchProcessor>(matchProcessor);
}
private DefaultMatchProcessorFactory GetMatchProcessorFactory(

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Routing.Constraints;
using Microsoft.AspNetCore.Routing.EndpointConstraints;
using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Xunit;
@ -575,10 +576,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
public void CreateCandidate_MatchProcessors()
{
// Arrange
var endpoint = CreateEndpoint("/a/b/c", matchProcessors: new MatchProcessorReference[]
{
new MatchProcessorReference("a", new IntRouteConstraint()),
});
var endpoint = CreateEndpoint("/a/b/c", constraints: new { a = new IntRouteConstraint(), });
var builder = CreateDfaMatcherBuilder();
@ -606,18 +604,14 @@ namespace Microsoft.AspNetCore.Routing.Matchers
}
private MatcherEndpoint CreateEndpoint(
string template,
string template,
object defaults = null,
IEnumerable<MatchProcessorReference> matchProcessors = null)
object constraints = null)
{
matchProcessors = matchProcessors ?? Array.Empty<MatchProcessorReference>();
return new MatcherEndpoint(
MatcherEndpoint.EmptyInvoker,
template,
new RouteValueDictionary(defaults),
RoutePatternFactory.Parse(template, new RouteValueDictionary(defaults), new RouteValueDictionary(constraints)),
new RouteValueDictionary(),
matchProcessors.ToList(),
0,
new EndpointMetadataCollection(Array.Empty<object>()),
"test");

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing.EndpointConstraints;
using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
@ -18,14 +19,12 @@ namespace Microsoft.AspNetCore.Routing.Matchers
// so we're reusing the services here.
public class DfaMatcherTest
{
private MatcherEndpoint CreateEndpoint(string template, int order, object defaultValues = null, EndpointMetadataCollection metadata = null)
private MatcherEndpoint CreateEndpoint(string template, int order, object defaults = null, EndpointMetadataCollection metadata = null)
{
return new MatcherEndpoint(
(next) => null,
template,
new RouteValueDictionary(defaultValues),
MatcherEndpoint.EmptyInvoker,
RoutePatternFactory.Parse(template, defaults, constraints: null),
new RouteValueDictionary(),
new List<MatchProcessorReference>(),
order,
metadata ?? EndpointMetadataCollection.Empty,
template);

View File

@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.AspNetCore.Routing.Matchers
@ -35,16 +36,14 @@ namespace Microsoft.AspNetCore.Routing.Matchers
internal static MatcherEndpoint CreateEndpoint(
string template,
object defaultValues = null,
object defaults = null,
object constraints = null,
int? order = null)
{
var defaults = defaultValues == null ? new RouteValueDictionary() : new RouteValueDictionary(defaultValues);
return new MatcherEndpoint(
MatcherEndpoint.EmptyInvoker,
template,
defaults,
RoutePatternFactory.Parse(template, defaults, constraints),
new RouteValueDictionary(),
new List<MatchProcessorReference>(),
order ?? 0,
EndpointMetadataCollection.Empty,
"endpoint: " + template);

View File

@ -0,0 +1,62 @@
// 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 Microsoft.AspNetCore.Http;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Routing.Matchers
{
public class RouteConstraintMatchProcessorTest
{
[Fact]
public void MatchInbound_CallsRouteConstraint()
{
// Arrange
var constraint = new Mock<IRouteConstraint>();
constraint
.Setup(c => c.Match(
It.IsAny<HttpContext>(),
NullRouter.Instance,
"test",
It.IsAny<RouteValueDictionary>(),
RouteDirection.IncomingRequest))
.Returns(true)
.Verifiable();
var matchProcessor = new RouteConstraintMatchProcessor("test", constraint.Object);
// Act
var result = matchProcessor.ProcessInbound(new DefaultHttpContext(), new RouteValueDictionary());
// Assert
Assert.True(result);
constraint.Verify();
}
[Fact]
public void MatchOutput_CallsRouteConstraint()
{
// Arrange
var constraint = new Mock<IRouteConstraint>();
constraint
.Setup(c => c.Match(
It.IsAny<HttpContext>(),
NullRouter.Instance,
"test",
It.IsAny<RouteValueDictionary>(),
RouteDirection.UrlGeneration))
.Returns(true)
.Verifiable();
var matchProcessor = new RouteConstraintMatchProcessor("test", constraint.Object);
// Act
var result = matchProcessor.ProcessOutbound(new DefaultHttpContext(), new RouteValueDictionary());
// Assert
Assert.True(result);
constraint.Verify();
}
}
}

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Routing.EndpointConstraints;
using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.AspNetCore.Routing.Template;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
@ -38,7 +39,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
var selector = new EndpointSelector(null, cache, NullLoggerFactory.Instance);
var groups = _entries
.GroupBy(e => (e.Order, e.Precedence, e.Endpoint.Template))
.GroupBy(e => (e.Order, e.Precedence, e.Endpoint.RoutePattern.RawText))
.OrderBy(g => g.Key.Order)
.ThenBy(g => g.Key.Precedence);
@ -47,16 +48,19 @@ namespace Microsoft.AspNetCore.Routing.Matchers
foreach (var group in groups)
{
var candidates = group.Select(e => e.Endpoint).ToArray();
var endpoint = group.First().Endpoint;
// MatcherEndpoint.Values contains the default values parsed from the template
// RoutePattern.Defaults contains the default values parsed from the template
// as well as those specified with a literal. We need to separate those
// for legacy cases.
var endpoint = group.First().Endpoint;
var defaults = new RouteValueDictionary(endpoint.Defaults);
for (var i = 0; i < endpoint.ParsedTemplate.Parameters.Count; i++)
//
// To do this we re-parse the original text and compare.
var withoutDefaults = RoutePatternFactory.Parse(endpoint.RoutePattern.RawText);
var defaults = new RouteValueDictionary(endpoint.RoutePattern.Defaults);
for (var i = 0; i < withoutDefaults.Parameters.Count; i++)
{
var parameter = endpoint.ParsedTemplate.Parameters[i];
if (parameter.DefaultValue != null)
var parameter = withoutDefaults.Parameters[i];
if (parameter.Default != null)
{
defaults.Remove(parameter.Name);
}
@ -64,7 +68,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
routes.Add(new Route(
new SelectorRouter(selector, candidates),
endpoint.Template,
endpoint.RoutePattern.RawText,
defaults,
new Dictionary<string, object>(),
new RouteValueDictionary(),

View File

@ -7,6 +7,7 @@ using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Routing.EndpointConstraints;
using Microsoft.AspNetCore.Routing.Internal;
using Microsoft.AspNetCore.Routing.Template;
using Microsoft.AspNetCore.Routing.Tree;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.ObjectPool;
@ -43,7 +44,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
var selector = new EndpointSelector(null, cache, NullLoggerFactory.Instance);
var groups = _entries
.GroupBy(e => (e.Order, e.Precedence, e.Endpoint.Template))
.GroupBy(e => (e.Order, e.Precedence, e.Endpoint.RoutePattern.RawText))
.OrderBy(g => g.Key.Order)
.ThenBy(g => g.Key.Precedence);
@ -57,11 +58,11 @@ namespace Microsoft.AspNetCore.Routing.Matchers
// as well as those specified with a literal. We need to separate those
// for legacy cases.
var endpoint = group.First().Endpoint;
var defaults = new RouteValueDictionary(endpoint.Defaults);
for (var i = 0; i < endpoint.ParsedTemplate.Parameters.Count; i++)
var defaults = new RouteValueDictionary(endpoint.RoutePattern.Defaults);
for (var i = 0; i < endpoint.RoutePattern.Parameters.Count; i++)
{
var parameter = endpoint.ParsedTemplate.Parameters[i];
if (parameter.DefaultValue != null)
var parameter = endpoint.RoutePattern.Parameters[i];
if (parameter.Default != null)
{
defaults.Remove(parameter.Name);
}
@ -69,7 +70,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
builder.MapInbound(
new SelectorRouter(selector, candidates),
endpoint.ParsedTemplate,
new RouteTemplate(endpoint.RoutePattern),
routeName: null,
order: endpoint.Order);
}

View File

@ -4,6 +4,8 @@
using System;
using System.Linq;
using Microsoft.AspNetCore.Routing.Constraints;
using Microsoft.AspNetCore.Routing.Matchers;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Routing.Patterns
@ -192,6 +194,42 @@ namespace Microsoft.AspNetCore.Routing.Patterns
});
}
[Fact]
public void Pattern_ExtraConstraints_MatchProcessor()
{
// Arrange
var template = "{a}/{b}/{c}";
var defaults = new { };
var constraints = new { d = Mock.Of<MatchProcessor>(), e = Mock.Of<MatchProcessor>(), };
var original = RoutePatternFactory.Parse(template);
// Act
var actual = RoutePatternFactory.Pattern(
original.RawText,
defaults,
constraints,
original.PathSegments);
// Assert
Assert.Collection(
actual.Constraints.OrderBy(kvp => kvp.Key),
kvp =>
{
Assert.Equal("d", kvp.Key);
Assert.Collection(
kvp.Value,
c => Assert.NotNull(c.MatchProcessor));
},
kvp =>
{
Assert.Equal("e", kvp.Key);
Assert.Collection(
kvp.Value,
c => Assert.NotNull(c.MatchProcessor));
});
}
[Fact]
public void Pattern_CreatesConstraintFromString()
{
@ -239,8 +277,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns
// Assert
Assert.Equal(
"The constraint entry 'd' - '17' must have a string value or be of a type " +
"which implements 'Microsoft.AspNetCore.Routing.IRouteConstraint'.",
$"Invalid constraint '17'. A constraint must be of type 'string', '{typeof(IRouteConstraint)}', or '{typeof(MatchProcessor)}'.",
ex.Message);
}
}

View File

@ -340,7 +340,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns
"p1",
null,
RoutePatternParameterKind.Standard,
Constraint("p1", constraint))));
Constraint(constraint))));
// Act
var actual = RoutePatternParser.Parse(template);
@ -741,7 +741,6 @@ namespace Microsoft.AspNetCore.Routing.Patterns
public bool Equals(RoutePatternConstraintReference x, RoutePatternConstraintReference y)
{
return
x.ParameterName == y.ParameterName &&
x.Content == y.Content &&
x.Constraint == y.Constraint;
}

View File

@ -6,6 +6,7 @@ using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Routing.Internal;
using Microsoft.AspNetCore.Routing.Matchers;
using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.AspNetCore.Routing.TestObjects;
using Microsoft.AspNetCore.Routing.Tree;
using Microsoft.Extensions.ObjectPool;
@ -185,15 +186,12 @@ namespace Microsoft.AspNetCore.Routing
private MatcherEndpoint CreateEndpoint(
string template,
object defaultValues = null,
object defaults = null,
object requiredValues = null,
int order = 0,
string routeName = null,
EndpointMetadataCollection metadataCollection = null)
{
var defaults = defaultValues == null ? new RouteValueDictionary() : new RouteValueDictionary(defaultValues);
var required = requiredValues == null ? new RouteValueDictionary() : new RouteValueDictionary(requiredValues);
if (metadataCollection == null)
{
metadataCollection = EndpointMetadataCollection.Empty;
@ -204,10 +202,9 @@ namespace Microsoft.AspNetCore.Routing
}
return new MatcherEndpoint(
next => (httpContext) => Task.CompletedTask,
template,
defaults,
required,
MatcherEndpoint.EmptyInvoker,
RoutePatternFactory.Parse(template, defaults, constraints: null),
new RouteValueDictionary(requiredValues),
order,
metadataCollection,
null);