RoutePattern everywhere
This commit is contained in:
parent
90395c933d
commit
f1c060bf3d
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in New Issue