Merge pull request #643 from aspnet/release/2.2

Merge RoutePattern changes to master
This commit is contained in:
Ryan Nowak 2018-07-20 15:40:08 -07:00 committed by GitHub
commit ef60a60ee1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 862 additions and 677 deletions

View File

@ -3,10 +3,11 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>netcoreapp2.2</TargetFramework> <TargetFramework>netcoreapp2.2</TargetFramework>
<TargetFramework Condition="'$(BenchmarksTargetFramework)' != ''">$(BenchmarksTargetFramework)</TargetFramework> <TargetFramework Condition="'$(BenchmarksTargetFramework)' != ''">$(BenchmarksTargetFramework)</TargetFramework>
<UseP2PReferences Condition="'$(UseP2PReferences)'=='' AND '$(BenchmarksTargetFramework)'==''">true</UseP2PReferences>
</PropertyGroup> </PropertyGroup>
<!--These references are used when running locally--> <!--These references are used when running locally-->
<ItemGroup Condition="'$(BenchmarksTargetFramework)' == ''"> <ItemGroup Condition="'$(UseP2PReferences)'=='true'">
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.Routing\Microsoft.AspNetCore.Routing.csproj" /> <ProjectReference Include="..\..\src\Microsoft.AspNetCore.Routing\Microsoft.AspNetCore.Routing.csproj" />
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="$(MicrosoftAspNetCoreServerKestrelPackageVersion)" /> <PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="$(MicrosoftAspNetCoreServerKestrelPackageVersion)" />
<PackageReference Include="Microsoft.Extensions.Configuration.CommandLine" Version="$(MicrosoftExtensionsConfigurationCommandLinePackageVersion)" /> <PackageReference Include="Microsoft.Extensions.Configuration.CommandLine" Version="$(MicrosoftExtensionsConfigurationCommandLinePackageVersion)" />
@ -14,7 +15,7 @@
</ItemGroup> </ItemGroup>
<!--These references are used when running on the Benchmarks Server--> <!--These references are used when running on the Benchmarks Server-->
<ItemGroup Condition="'$(BenchmarksTargetFramework)' != ''"> <ItemGroup Condition="'$(UseP2PReferences)'!='true'">
<PackageReference Include="Microsoft.AspNetCore.App" Version="$(MicrosoftAspNetCoreAppPackageVersion)" /> <PackageReference Include="Microsoft.AspNetCore.App" Version="$(MicrosoftAspNetCoreAppPackageVersion)" />
</ItemGroup> </ItemGroup>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -20,145 +20,113 @@ namespace Microsoft.AspNetCore.Routing.Matchers
_serviceProvider = serviceProvider; _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: // Example:
// {productId:regex(\d+)} // {productId:regex(\d+)}
// //
// ParameterName: productId // ParameterName: productId
// ConstraintText: regex(\d+) // value: regex(\d+)
// ConstraintName: regex // name: regex
// ConstraintArgument: \d+ // argument: \d+
(var name, var argument) = Parse(value);
(var constraintName, var constraintArgument) = Parse(matchProcessorReference.ConstraintText); if (!_options.ConstraintMap.TryGetValue(name, out var type))
if (!_options.ConstraintMap.TryGetValue(constraintName, out var constraintType))
{ {
throw new InvalidOperationException( throw new InvalidOperationException(Resources.FormatRoutePattern_ConstraintReferenceNotFound(
$"No constraint has been registered with name '{constraintName}'."); name,
typeof(RouteOptions),
nameof(RouteOptions.ConstraintMap)));
} }
var processor = ResolveMatchProcessor( if (typeof(MatchProcessor).IsAssignableFrom(type))
matchProcessorReference.ParameterName,
matchProcessorReference.Optional,
constraintType,
constraintArgument);
if (processor != null)
{ {
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( var constraint = DefaultInlineConstraintResolver.CreateConstraint(type, argument);
Resources.FormatDefaultInlineConstraintResolver_TypeNotConstraint( return InitializeMatchProcessor(parameterName, optional, constraint, argument);
constraintType,
constraintName,
typeof(IRouteConstraint).Name));
} }
try var message = Resources.FormatRoutePattern_InvalidStringConstraintReference(
{ type,
return CreateMatchProcessorFromRouteConstraint( name,
matchProcessorReference.ParameterName, typeof(IRouteConstraint),
matchProcessorReference.Optional, typeof(MatchProcessor));
constraintType, throw new InvalidOperationException(message);
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);
}
} }
private MatchProcessor CreateMatchProcessorFromRouteConstraint( private MatchProcessor InitializeMatchProcessor(
string parameterName, string parameterName,
bool optional, bool optional,
Type constraintType, IRouteConstraint constraint,
string constraintArgument) 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) if (optional)
{ {
matchProcessor = new OptionalMatchProcessor(matchProcessor); matchProcessor = new OptionalMatchProcessor(matchProcessor);
} }
matchProcessor.Initialize(parameterName, constraintArgument); matchProcessor.Initialize(parameterName, argument);
return matchProcessor; return matchProcessor;
} }
private MatchProcessor ResolveMatchProcessor( private (string name, string argument) Parse(string text)
string parameterName,
bool optional,
Type constraintType,
string constraintArgument)
{ {
if (constraintType == null) string name;
string argument;
var indexOfFirstOpenParens = text.IndexOf('(');
if (indexOfFirstOpenParens >= 0 && text.EndsWith(")", StringComparison.Ordinal))
{ {
throw new ArgumentNullException(nameof(constraintType)); name = text.Substring(0, indexOfFirstOpenParens);
} argument = text.Substring(
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(
indexOfFirstOpenParens + 1, indexOfFirstOpenParens + 1,
constraintText.Length - indexOfFirstOpenParens - 2); text.Length - indexOfFirstOpenParens - 2);
} }
else else
{ {
constraintName = constraintText; name = text;
constraintArgument = null; argument = null;
} }
return (constraintName, constraintArgument); return (name, argument);
} }
} }
} }

View File

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

View File

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

View File

@ -1,86 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Routing.Matchers
{
public sealed class MatchProcessorReference
{
// Example:
// api/products/{productId:regex(\d+)}
//
// ParameterName = productId
// ConstraintText = regex(\d+)
// ConstraintArgument = \d+
public MatchProcessorReference(string parameterName, string constraintText)
{
ParameterName = parameterName;
ConstraintText = constraintText;
}
public MatchProcessorReference(string parameterName, bool optional, string constraintText)
{
ParameterName = parameterName;
Optional = optional;
ConstraintText = constraintText;
}
public MatchProcessorReference(string parameterName, MatchProcessor matchProcessor)
{
ParameterName = parameterName;
MatchProcessor = matchProcessor;
}
internal MatchProcessor MatchProcessor { get; private set; }
internal string ConstraintText { get; private set; }
internal string ParameterName { get; private set; }
internal bool Optional { get; private set; }
public MatchProcessorReference(string parameterName, IRouteConstraint routeConstraint)
: this(parameterName, new RouteConstraintMatchProcessorAdapter(parameterName, routeConstraint))
{
}
private class RouteConstraintMatchProcessorAdapter : MatchProcessor
{
public string ParameterName { get; private set; }
public IRouteConstraint RouteConstraint { get; }
public RouteConstraintMatchProcessorAdapter(string parameterName, IRouteConstraint routeConstraint)
{
ParameterName = parameterName;
RouteConstraint = routeConstraint;
}
public override void Initialize(string parameterName, string constraintArgument)
{
}
public override bool ProcessInbound(HttpContext httpContext, RouteValueDictionary routeValues)
{
return RouteConstraint.Match(
httpContext,
NullRouter.Instance,
ParameterName,
routeValues,
RouteDirection.IncomingRequest);
}
public override bool ProcessOutbound(HttpContext httpContext, RouteValueDictionary values)
{
return RouteConstraint.Match(
httpContext,
NullRouter.Instance,
ParameterName,
values,
RouteDirection.UrlGeneration);
}
}
}
}

View File

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

View File

@ -5,7 +5,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing.Template; using Microsoft.AspNetCore.Routing.Patterns;
namespace Microsoft.AspNetCore.Routing.Matchers namespace Microsoft.AspNetCore.Routing.Matchers
{ {
@ -18,25 +18,11 @@ namespace Microsoft.AspNetCore.Routing.Matchers
public MatcherEndpoint( public MatcherEndpoint(
Func<RequestDelegate, RequestDelegate> invoker, Func<RequestDelegate, RequestDelegate> invoker,
string template, RoutePattern routePattern,
RouteValueDictionary defaults,
RouteValueDictionary requiredValues, RouteValueDictionary requiredValues,
int order, int order,
EndpointMetadataCollection metadata, EndpointMetadataCollection metadata,
string displayName) 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) : base(metadata, displayName)
{ {
if (invoker == null) if (invoker == null)
@ -44,89 +30,24 @@ namespace Microsoft.AspNetCore.Routing.Matchers
throw new ArgumentNullException(nameof(invoker)); throw new ArgumentNullException(nameof(invoker));
} }
if (template == null) if (routePattern == null)
{ {
throw new ArgumentNullException(nameof(template)); throw new ArgumentNullException(nameof(routePattern));
} }
Invoker = invoker; Invoker = invoker;
Order = order; RoutePattern = routePattern;
Template = template;
ParsedTemplate = TemplateParser.Parse(template);
RequiredValues = requiredValues; RequiredValues = requiredValues;
var mergedDefaults = GetDefaults(ParsedTemplate, defaults); Order = order;
Defaults = mergedDefaults;
var mergedReferences = MergeMatchProcessorReferences(ParsedTemplate, nonInlineMatchProcessorReferences);
MatchProcessorReferences = mergedReferences.AsReadOnly();
} }
public Func<RequestDelegate, RequestDelegate> Invoker { get; }
public int Order { 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 // 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 RoutePattern RoutePattern { get; }
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;
}
} }
} }

View File

@ -0,0 +1,61 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Routing.Matchers
{
internal class RouteConstraintMatchProcessor : MatchProcessor
{
public RouteConstraintMatchProcessor(string parameterName, IRouteConstraint constraint)
{
ParameterName = parameterName;
Constraint = constraint;
}
public string ParameterName { get; }
public IRouteConstraint Constraint { get; }
public override bool ProcessInbound(HttpContext httpContext, RouteValueDictionary values)
{
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}
if (values == null)
{
throw new ArgumentNullException(nameof(values));
}
return Constraint.Match(
httpContext,
NullRouter.Instance,
ParameterName,
values,
RouteDirection.IncomingRequest);
}
public override bool ProcessOutbound(HttpContext httpContext, RouteValueDictionary values)
{
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}
if (values == null)
{
throw new ArgumentNullException(nameof(values));
}
return Constraint.Match(
httpContext,
NullRouter.Instance,
ParameterName,
values,
RouteDirection.UrlGeneration);
}
}
}

View File

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

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Diagnostics; using System.Diagnostics;
using Microsoft.AspNetCore.Routing.Matchers;
namespace Microsoft.AspNetCore.Routing.Patterns namespace Microsoft.AspNetCore.Routing.Patterns
{ {
@ -11,18 +12,21 @@ namespace Microsoft.AspNetCore.Routing.Patterns
[DebuggerDisplay("{DebuggerToString()}")] [DebuggerDisplay("{DebuggerToString()}")]
public sealed class RoutePatternConstraintReference public sealed class RoutePatternConstraintReference
{ {
internal RoutePatternConstraintReference(string parameterName, string content) internal RoutePatternConstraintReference(string content)
{ {
ParameterName = parameterName;
Content = content; Content = content;
} }
internal RoutePatternConstraintReference(string parameterName, IRouteConstraint constraint) internal RoutePatternConstraintReference(IRouteConstraint constraint)
{ {
ParameterName = parameterName;
Constraint = constraint; Constraint = constraint;
} }
internal RoutePatternConstraintReference(MatchProcessor matchProcessor)
{
MatchProcessor = matchProcessor;
}
/// <summary> /// <summary>
/// Gets the constraint text. /// Gets the constraint text.
/// </summary> /// </summary>
@ -34,9 +38,9 @@ namespace Microsoft.AspNetCore.Routing.Patterns
public IRouteConstraint Constraint { get; } public IRouteConstraint Constraint { get; }
/// <summary> /// <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> /// </summary>
public string ParameterName { get; } public MatchProcessor MatchProcessor { get; }
private string DebuggerToString() private string DebuggerToString()
{ {

View File

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

View File

@ -458,6 +458,62 @@ namespace Microsoft.AspNetCore.Routing
internal static string FormatConstraintMustBeStringOrConstraint(object p0, object p1, object p2) internal static string FormatConstraintMustBeStringOrConstraint(object p0, object p1, object p2)
=> string.Format(CultureInfo.CurrentCulture, GetString("ConstraintMustBeStringOrConstraint"), p0, p1, 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) private static string GetString(string name, params string[] formatterNames)
{ {
var value = _resourceManager.GetString(name); var value = _resourceManager.GetString(name);

View File

@ -213,4 +213,16 @@
<data name="ConstraintMustBeStringOrConstraint" xml:space="preserve"> <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> <value>The constraint entry '{0}' - '{1}' must have a string value or be of a type which implements '{2}'.</value>
</data> </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> </root>

View File

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

View File

@ -4,6 +4,7 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using Microsoft.AspNetCore.Routing.Patterns;
namespace Microsoft.AspNetCore.Routing.Template namespace Microsoft.AspNetCore.Routing.Template
{ {
@ -36,6 +37,24 @@ namespace Microsoft.AspNetCore.Routing.Template
return precedence; 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 // Compute the precedence for generating a url
// e.g.: /api/template == 5.5 // e.g.: /api/template == 5.5
// /api/template/{id} == 5.53 // /api/template/{id} == 5.53
@ -60,6 +79,26 @@ namespace Microsoft.AspNetCore.Routing.Template
return precedence; 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: // Segments have the following order:
// 5 - Literal segments // 5 - Literal segments
// 4 - Multi-part segments && Constrained parameter 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: // Segments have the following order:
// 1 - Literal segments // 1 - Literal segments
// 2 - Constrained parameter segments / Multi-part segments // 2 - Constrained parameter segments / Multi-part segments
@ -127,5 +198,40 @@ namespace Microsoft.AspNetCore.Routing.Template
return digit; return digit;
} }
} }
// see description on ComputeInboundPrecedenceDigit(TemplateSegment segment)
private static int ComputeInboundPrecedenceDigit(RoutePatternPathSegment pathSegment)
{
if (pathSegment.Parts.Count > 1)
{
// Multi-part segments should appear after literal segments and along with parameter segments
return 2;
}
var part = pathSegment.Parts[0];
// Literal segments always go first
if (part.IsLiteral)
{
return 1;
}
else if (part is RoutePatternParameterPart parameterPart)
{
var digit = parameterPart.IsCatchAll ? 5 : 3;
// If there is a route constraint for the parameter, reduce order by 1
// Constrained parameters end up with order 2, Constrained catch alls end up with order 4
if (parameterPart.Constraints.Count > 0)
{
digit--;
}
return digit;
}
else
{
// Unreachable
throw new NotSupportedException();
}
}
} }
} }

View File

@ -118,7 +118,7 @@ namespace Microsoft.AspNetCore.Routing.Template
RoutePatternParameterKind.Optional : RoutePatternParameterKind.Optional :
RoutePatternParameterKind.Standard; 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); return RoutePatternFactory.ParameterPart(Name, DefaultValue, kind, constraints);
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -4,10 +4,10 @@
using System; using System;
using System.Globalization; using System.Globalization;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing.Constraints;
using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Moq;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Routing.Matchers namespace Microsoft.AspNetCore.Routing.Matchers
@ -19,11 +19,153 @@ namespace Microsoft.AspNetCore.Routing.Matchers
{ {
// Arrange // Arrange
var factory = GetMatchProcessorFactory(); var factory = GetMatchProcessorFactory();
var matchProcessorReference = new MatchProcessorReference("id", @"notpresent(\d+)");
// Act & Assert // Act
var exception = Assert.Throws<InvalidOperationException>( 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] [Fact]
@ -31,29 +173,12 @@ namespace Microsoft.AspNetCore.Routing.Matchers
{ {
// Arrange // Arrange
var factory = GetMatchProcessorFactory(); var factory = GetMatchProcessorFactory();
var matchProcessorReference = new MatchProcessorReference("id", "int");
// Act 1 // Act
var processor = factory.Create(matchProcessorReference); var matchProcessor = factory.Create("id", "int", optional: false);
// Assert 1 // Assert
Assert.NotNull(processor); Assert.IsType<IntRouteConstraint>(Assert.IsType<RouteConstraintMatchProcessor>(matchProcessor).Constraint);
// 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);
} }
[Fact] [Fact]
@ -61,84 +186,50 @@ namespace Microsoft.AspNetCore.Routing.Matchers
{ {
// Arrange // Arrange
var factory = GetMatchProcessorFactory(); var factory = GetMatchProcessorFactory();
var matchProcessorReference = new MatchProcessorReference("id", true, "int");
// Act // Act
var processor = factory.Create(matchProcessorReference); var matchProcessor = factory.Create("id", "int", optional: true);
// Assert // Assert
Assert.IsType<OptionalMatchProcessor>(processor); Assert.IsType<OptionalMatchProcessor>(matchProcessor);
} }
[Fact] [Fact]
public void Create_CreatesMatchProcessor_FromConstraintText_AndCustomMatchProcessor() public void Create_CreatesMatchProcessor_FromConstraintText_AndMatchProcesor()
{ {
// Arrange // Arrange
var options = new RouteOptions(); var options = new RouteOptions();
options.ConstraintMap.Add("endsWith", typeof(EndsWithStringMatchProcessor)); options.ConstraintMap.Add("endsWith", typeof(EndsWithStringMatchProcessor));
var services = new ServiceCollection(); var services = new ServiceCollection();
services.AddTransient<EndsWithStringMatchProcessor>(); services.AddTransient<EndsWithStringMatchProcessor>();
var factory = GetMatchProcessorFactory(options, services); var factory = GetMatchProcessorFactory(options, services);
var matchProcessorReference = new MatchProcessorReference("id", "endsWith(_001)");
// Act 1 // Act
var processor = factory.Create(matchProcessorReference); var matchProcessor = factory.Create("id", "endsWith", optional: false);
// Assert 1 // Assert
Assert.NotNull(processor); Assert.IsType<EndsWithStringMatchProcessor>(matchProcessor);
// 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);
} }
[Fact] [Fact]
public void Create_ReturnsMatchProcessor_IfAvailable() public void Create_CreatesMatchProcessor_FromConstraintText_AndMatchProcessor_Optional()
{ {
// Arrange // Arrange
var factory = GetMatchProcessorFactory(); var options = new RouteOptions();
var matchProcessorReference = new MatchProcessorReference("id", Mock.Of<MatchProcessor>()); options.ConstraintMap.Add("endsWith", typeof(EndsWithStringMatchProcessor));
var expected = matchProcessorReference.MatchProcessor;
var services = new ServiceCollection();
services.AddTransient<EndsWithStringMatchProcessor>();
var factory = GetMatchProcessorFactory(options, services);
// Act // Act
var processor = factory.Create(matchProcessorReference); var matchProcessor = factory.Create("id", "endsWith", optional: true);
// Assert // Assert
Assert.Same(expected, processor); Assert.IsType<OptionalMatchProcessor>(matchProcessor);
}
[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);
} }
private DefaultMatchProcessorFactory GetMatchProcessorFactory( private DefaultMatchProcessorFactory GetMatchProcessorFactory(

View File

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

View File

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

View File

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

View File

@ -0,0 +1,62 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Http;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Routing.Matchers
{
public class RouteConstraintMatchProcessorTest
{
[Fact]
public void MatchInbound_CallsRouteConstraint()
{
// Arrange
var constraint = new Mock<IRouteConstraint>();
constraint
.Setup(c => c.Match(
It.IsAny<HttpContext>(),
NullRouter.Instance,
"test",
It.IsAny<RouteValueDictionary>(),
RouteDirection.IncomingRequest))
.Returns(true)
.Verifiable();
var matchProcessor = new RouteConstraintMatchProcessor("test", constraint.Object);
// Act
var result = matchProcessor.ProcessInbound(new DefaultHttpContext(), new RouteValueDictionary());
// Assert
Assert.True(result);
constraint.Verify();
}
[Fact]
public void MatchOutput_CallsRouteConstraint()
{
// Arrange
var constraint = new Mock<IRouteConstraint>();
constraint
.Setup(c => c.Match(
It.IsAny<HttpContext>(),
NullRouter.Instance,
"test",
It.IsAny<RouteValueDictionary>(),
RouteDirection.UrlGeneration))
.Returns(true)
.Verifiable();
var matchProcessor = new RouteConstraintMatchProcessor("test", constraint.Object);
// Act
var result = matchProcessor.ProcessOutbound(new DefaultHttpContext(), new RouteValueDictionary());
// Assert
Assert.True(result);
constraint.Verify();
}
}
}

View File

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

View File

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

View File

@ -4,6 +4,8 @@
using System; using System;
using System.Linq; using System.Linq;
using Microsoft.AspNetCore.Routing.Constraints; using Microsoft.AspNetCore.Routing.Constraints;
using Microsoft.AspNetCore.Routing.Matchers;
using Moq;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Routing.Patterns 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] [Fact]
public void Pattern_CreatesConstraintFromString() public void Pattern_CreatesConstraintFromString()
{ {
@ -239,8 +277,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns
// Assert // Assert
Assert.Equal( Assert.Equal(
"The constraint entry 'd' - '17' must have a string value or be of a type " + $"Invalid constraint '17'. A constraint must be of type 'string', '{typeof(IRouteConstraint)}', or '{typeof(MatchProcessor)}'.",
"which implements 'Microsoft.AspNetCore.Routing.IRouteConstraint'.",
ex.Message); ex.Message);
} }
} }

View File

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

View File

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