Merge release/2.2

This commit is contained in:
James Newton-King 2018-09-06 14:07:11 +12:00
commit aadc31d9a5
No known key found for this signature in database
GPG Key ID: 0A66B2F456BF5526
17 changed files with 633 additions and 302 deletions

View File

@ -3,12 +3,11 @@
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using Microsoft.AspNetCore.Http.Features;
namespace Microsoft.AspNetCore.Routing.Matching
{
// Generated from https://github.com/Azure/azure-rest-api-specs
public partial class MatcherAzureBenchmark : EndpointRoutingBenchmarkBase
public class MatcherAzureBenchmark : MatcherAzureBenchmarkBase
{
private const int SampleCount = 100;

View File

@ -7,11 +7,11 @@ using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Routing.Matching
{
// This code was generated by the Swaggatherer
public partial class MatcherAzureBenchmark : EndpointRoutingBenchmarkBase
public abstract partial class MatcherAzureBenchmarkBase : EndpointRoutingBenchmarkBase
{
private const int EndpointCount = 5160;
private protected const int EndpointCount = 5160;
private void SetupEndpoints()
private protected void SetupEndpoints()
{
Endpoints = new RouteEndpoint[5160];
Endpoints[0] = CreateEndpoint("/account", "GET");
@ -5176,7 +5176,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
Endpoints[5159] = CreateEndpoint("/{tenantID}/groups/{groupObjectId}/$links/members/{memberObjectId}", "DELETE");
}
private void SetupRequests()
private protected void SetupRequests()
{
Requests = new HttpContext[5160];
Requests[0] = new DefaultHttpContext();
@ -25821,7 +25821,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
Requests[5159].Request.Path = "/67fb987d/groups/04d01a8c-6135/$links/members/71b75dbe-0076-";
}
private Matcher SetupMatcher(MatcherBuilder builder)
private protected Matcher SetupMatcher(MatcherBuilder builder)
{
builder.AddEndpoint(Endpoints[0]);
builder.AddEndpoint(Endpoints[1]);

View File

@ -0,0 +1,31 @@
// 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 BenchmarkDotNet.Attributes;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.AspNetCore.Routing.Matching
{
// Generated from https://github.com/APIs-guru/openapi-directory
// Use https://editor2.swagger.io/ to convert from yaml to json-
public class MatcherBuilderAzureBenchmark : MatcherAzureBenchmarkBase
{
private IServiceProvider _services;
[GlobalSetup]
public void Setup()
{
SetupEndpoints();
_services = CreateServices();
}
[Benchmark]
public void Dfa()
{
var builder = _services.GetRequiredService<DfaMatcherBuilder>();
SetupMatcher(builder);
}
}
}

View File

@ -0,0 +1,31 @@
// 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 BenchmarkDotNet.Attributes;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.AspNetCore.Routing.Matching
{
// Generated from https://github.com/APIs-guru/openapi-directory
// Use https://editor2.swagger.io/ to convert from yaml to json-
public class MatcherBuilderGithubBenchmark : MatcherGithubBenchmarkBase
{
private IServiceProvider _services;
[GlobalSetup]
public void Setup()
{
SetupEndpoints();
_services = CreateServices();
}
[Benchmark]
public void Dfa()
{
var builder = _services.GetRequiredService<DfaMatcherBuilder>();
SetupMatcher(builder);
}
}
}

View File

@ -0,0 +1,50 @@
// 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 BenchmarkDotNet.Attributes;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.AspNetCore.Routing.Matching
{
public partial class MatcherBuilderMultipleEntryBenchmark : EndpointRoutingBenchmarkBase
{
private IServiceProvider _services;
[GlobalSetup]
public void Setup()
{
Endpoints = new RouteEndpoint[10];
Endpoints[0] = CreateEndpoint("/product", "GET");
Endpoints[1] = CreateEndpoint("/product/{id}", "GET");
Endpoints[2] = CreateEndpoint("/account", "GET");
Endpoints[3] = CreateEndpoint("/account/{id}");
Endpoints[4] = CreateEndpoint("/account/{id}", "POST");
Endpoints[5] = CreateEndpoint("/account/{id}", "UPDATE");
Endpoints[6] = CreateEndpoint("/v2/account", "GET");
Endpoints[7] = CreateEndpoint("/v2/account/{id}");
Endpoints[8] = CreateEndpoint("/v2/account/{id}", "POST");
Endpoints[9] = CreateEndpoint("/v2/account/{id}", "UPDATE");
_services = CreateServices();
}
private Matcher SetupMatcher(MatcherBuilder builder)
{
for (int i = 0; i < Endpoints.Length; i++)
{
builder.AddEndpoint(Endpoints[i]);
}
return builder.Build();
}
[Benchmark]
public void Dfa()
{
var builder = _services.GetRequiredService<DfaMatcherBuilder>();
SetupMatcher(builder);
}
}
}

View File

@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
{
// Generated from https://github.com/APIs-guru/openapi-directory
// Use https://editor2.swagger.io/ to convert from yaml to json-
public partial class MatcherGithubBenchmark : EndpointRoutingBenchmarkBase
public class MatcherGithubBenchmark : MatcherGithubBenchmarkBase
{
private BarebonesMatcher _baseline;
private Matcher _dfa;

View File

@ -7,11 +7,11 @@ using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Routing.Matching
{
// This code was generated by the Swaggatherer
public partial class MatcherGithubBenchmark : EndpointRoutingBenchmarkBase
public partial class MatcherGithubBenchmarkBase : EndpointRoutingBenchmarkBase
{
private const int EndpointCount = 243;
private protected const int EndpointCount = 243;
private void SetupEndpoints()
private protected void SetupEndpoints()
{
Endpoints = new RouteEndpoint[243];
Endpoints[0] = CreateEndpoint("/emojis", "GET");
@ -259,7 +259,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
Endpoints[242] = CreateEndpoint("/repos/{owner}/{repo}/{archive_format}/{path}", "GET");
}
private void SetupRequests()
private protected void SetupRequests()
{
Requests = new HttpContext[243];
Requests[0] = new DefaultHttpContext();
@ -1236,7 +1236,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
Requests[242].Request.Path = "/repos/21a74/f36c8/805b0492-b723-/680ad";
}
private Matcher SetupMatcher(MatcherBuilder builder)
private protected Matcher SetupMatcher(MatcherBuilder builder)
{
builder.AddEndpoint(Endpoints[0]);
builder.AddEndpoint(Endpoints[1]);

View File

@ -0,0 +1,16 @@
// 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 BenchmarkDotNet.Attributes;
namespace Microsoft.AspNetCore.Routing.Matching
{
public class RouteEndpointAzureBenchmark : MatcherAzureBenchmarkBase
{
[Benchmark]
public void CreateEndpoints()
{
SetupEndpoints();
}
}
}

View File

@ -47,7 +47,7 @@ namespace Microsoft.AspNetCore.Routing.Internal
// Assign each node a sequential index.
var visited = new Dictionary<DfaNode, int>();
var tree = builder.BuildDfaTree();
var tree = builder.BuildDfaTree(includeLabel: true);
writer.WriteLine("digraph DFA {");
tree.Visit(WriteNode);
@ -64,9 +64,12 @@ namespace Microsoft.AspNetCore.Routing.Internal
// We can safely index into visited because this is a post-order traversal,
// all of the children of this node are already in the dictionary.
foreach (var literal in node.Literals)
if (node.Literals != null)
{
writer.WriteLine($"{label} -> {visited[literal.Value]} [label=\"/{literal.Key}\"]");
foreach (var literal in node.Literals)
{
writer.WriteLine($"{label} -> {visited[literal.Value]} [label=\"/{literal.Key}\"]");
}
}
if (node.Parameters != null)
@ -79,9 +82,12 @@ namespace Microsoft.AspNetCore.Routing.Internal
writer.WriteLine($"{label} -> {visited[node.CatchAll]} [label=\"/**\"]");
}
foreach (var policy in node.PolicyEdges)
if (node.PolicyEdges != null)
{
writer.WriteLine($"{label} -> {visited[policy.Value]} [label=\"{policy.Key}\"]");
foreach (var policy in node.PolicyEdges)
{
writer.WriteLine($"{label} -> {visited[policy.Value]} [label=\"{policy.Key}\"]");
}
}
writer.WriteLine($"{label} [label=\"{node.Label}\"]");

View File

@ -19,6 +19,15 @@ namespace Microsoft.AspNetCore.Routing.Matching
private readonly INodeBuilderPolicy[] _nodeBuilders;
private readonly EndpointComparer _comparer;
// These collections are reused when building candidates
private readonly Dictionary<string, int> _assignments;
private readonly List<KeyValuePair<string, object>> _slots;
private readonly List<(string parameterName, int segmentIndex, int slotIndex)> _captures;
private readonly List<(RoutePatternPathSegment pathSegment, int segmentIndex)> _complexSegments;
private readonly List<KeyValuePair<string, IRouteConstraint>> _constraints;
private int _stateIndex;
public DfaMatcherBuilder(
ParameterPolicyFactory parameterPolicyFactory,
EndpointSelector selector,
@ -31,6 +40,12 @@ namespace Microsoft.AspNetCore.Routing.Matching
// Taking care to use _policies, which has been sorted.
_nodeBuilders = _policies.OfType<INodeBuilderPolicy>().ToArray();
_comparer = new EndpointComparer(_policies.OfType<IEndpointComparerPolicy>().ToArray());
_assignments = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
_slots = new List<KeyValuePair<string, object>>();
_captures = new List<(string parameterName, int segmentIndex, int slotIndex)>();
_complexSegments = new List<(RoutePatternPathSegment pathSegment, int segmentIndex)>();
_constraints = new List<KeyValuePair<string, IRouteConstraint>>();
}
public override void AddEndpoint(RouteEndpoint endpoint)
@ -38,7 +53,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
_endpoints.Add(endpoint);
}
public DfaNode BuildDfaTree()
public DfaNode BuildDfaTree(bool includeLabel = false)
{
// We build the tree by doing a BFS over the list of entries. This is important
// because a 'parameter' node can also traverse the same paths that literal nodes
@ -49,9 +64,10 @@ namespace Microsoft.AspNetCore.Routing.Matching
// Since we're doing a BFS we will process each 'level' of the tree in stages
// this list will hold the set of items we need to process at the current
// stage.
var work = new List<(RouteEndpoint endpoint, List<DfaNode> parents)>();
var work = new List<(RouteEndpoint endpoint, List<DfaNode> parents)>(_endpoints.Count);
List<(RouteEndpoint endpoint, List<DfaNode> parents)> previousWork = null;
var root = new DfaNode() { PathDepth = 0, Label = "/" };
var root = new DfaNode() { PathDepth = 0, Label = includeLabel ? "/" : null };
// To prepare for this we need to compute the max depth, as well as
// a seed list of items to process (entry, root).
@ -63,14 +79,26 @@ namespace Microsoft.AspNetCore.Routing.Matching
work.Add((endpoint, new List<DfaNode>() { root, }));
}
var workCount = work.Count;
// Now we process the entries a level at a time.
for (var depth = 0; depth <= maxDepth; depth++)
{
// As we process items, collect the next set of items.
var nextWork = new List<(RouteEndpoint endpoint, List<DfaNode> parents)>();
List<(RouteEndpoint endpoint, List<DfaNode> parents)> nextWork;
var nextWorkCount = 0;
if (previousWork == null)
{
nextWork = new List<(RouteEndpoint endpoint, List<DfaNode> parents)>();
}
else
{
// Reuse previous collection for the next collection
// Don't clear the list so nested lists can be reused
nextWork = previousWork;
}
for (var i = 0; i < work.Count; i++)
for (var i = 0; i < workCount; i++)
{
var (endpoint, parents) = work[i];
@ -79,12 +107,28 @@ namespace Microsoft.AspNetCore.Routing.Matching
for (var j = 0; j < parents.Count; j++)
{
var parent = parents[j];
parent.Matches.Add(endpoint);
parent.AddMatch(endpoint);
}
}
// Find the parents of this edge at the current depth
var nextParents = new List<DfaNode>();
List<DfaNode> nextParents;
if (nextWorkCount < nextWork.Count)
{
nextParents = nextWork[nextWorkCount].parents;
nextParents.Clear();
nextWork[nextWorkCount] = (endpoint, nextParents);
}
else
{
nextParents = new List<DfaNode>();
// Add to the next set of work now so the list will be reused
// even if there are no parents
nextWork.Add((endpoint, nextParents));
}
var segment = GetCurrentSegment(endpoint, depth);
if (segment == null)
{
@ -97,15 +141,17 @@ namespace Microsoft.AspNetCore.Routing.Matching
var part = segment.Parts[0];
if (segment.IsSimple && part is RoutePatternLiteralPart literalPart)
{
DfaNode next = null;
var literal = literalPart.Content;
if (!parent.Literals.TryGetValue(literal, out var next))
if (parent.Literals == null ||
!parent.Literals.TryGetValue(literal, out next))
{
next = new DfaNode()
{
PathDepth = parent.PathDepth + 1,
Label = parent.Label + literal + "/",
Label = includeLabel ? parent.Label + literal + "/" : null,
};
parent.Literals.Add(literal, next);
parent.AddLiteral(literal, next);
}
nextParents.Add(next);
@ -115,7 +161,10 @@ namespace Microsoft.AspNetCore.Routing.Matching
// 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
// all catchalls will be processed after all parameters.
nextParents.AddRange(parent.Literals.Values);
if (parent.Literals != null)
{
nextParents.AddRange(parent.Literals.Values);
}
if (parent.Parameters != null)
{
nextParents.Add(parent.Parameters);
@ -131,7 +180,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
parent.CatchAll = new DfaNode()
{
PathDepth = parent.PathDepth + 1,
Label = parent.Label + "{*...}/",
Label = includeLabel ? parent.Label + "{*...}/" : null,
};
// The catchall node just loops.
@ -139,7 +188,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
parent.CatchAll.CatchAll = parent.CatchAll;
}
parent.CatchAll.Matches.Add(endpoint);
parent.CatchAll.AddMatch(endpoint);
}
else if (segment.IsSimple && part.IsParameter)
{
@ -148,12 +197,15 @@ namespace Microsoft.AspNetCore.Routing.Matching
parent.Parameters = new DfaNode()
{
PathDepth = parent.PathDepth + 1,
Label = parent.Label + "{...}/",
Label = includeLabel ? parent.Label + "{...}/" : null,
};
}
// A parameter should traverse all literal nodes as well as the parameter node
nextParents.AddRange(parent.Literals.Values);
if (parent.Literals != null)
{
nextParents.AddRange(parent.Literals.Values);
}
nextParents.Add(parent.Parameters);
}
else
@ -167,23 +219,28 @@ namespace Microsoft.AspNetCore.Routing.Matching
parent.Parameters = new DfaNode()
{
PathDepth = parent.PathDepth + 1,
Label = parent.Label + "{...}/",
Label = includeLabel ? parent.Label + "{...}/" : null,
};
}
nextParents.AddRange(parent.Literals.Values);
if (parent.Literals != null)
{
nextParents.AddRange(parent.Literals.Values);
}
nextParents.Add(parent.Parameters);
}
}
if (nextParents.Count > 0)
{
nextWork.Add((endpoint, nextParents));
nextWorkCount++;
}
}
// Prepare the process the next stage.
previousWork = work;
work = nextWork;
workCount = nextWorkCount;
}
// Build the trees of policy nodes (like HTTP methods). Post-order traversal
@ -216,76 +273,64 @@ namespace Microsoft.AspNetCore.Routing.Matching
public override Matcher Build()
{
var root = BuildDfaTree();
#if DEBUG
var includeLabel = true;
#else
var includeLabel = false;
#endif
var root = BuildDfaTree(includeLabel);
// State count is the number of nodes plus an exit state
var stateCount = 1;
var maxSegmentCount = 0;
root.Visit((node) => maxSegmentCount = Math.Max(maxSegmentCount, node.PathDepth));
root.Visit((node) =>
{
stateCount++;
maxSegmentCount = Math.Max(maxSegmentCount, node.PathDepth);
});
_stateIndex = 0;
// The max segment count is the maximum path-node-depth +1. We need
// the +1 to capture any additional content after the 'last' segment.
maxSegmentCount++;
var states = new List<DfaState>();
var tableBuilders = new List<(JumpTableBuilder pathBuilder, PolicyJumpTableBuilder policyBuilder)>();
AddNode(root, states, tableBuilders);
var states = new DfaState[stateCount];
var exitDestination = stateCount - 1;
AddNode(root, states, exitDestination);
var exit = states.Count;
states.Add(new DfaState(Array.Empty<Candidate>(), null, null));
tableBuilders.Add((new JumpTableBuilder() { DefaultDestination = exit, ExitDestination = exit, }, null));
states[exitDestination] = new DfaState(
Array.Empty<Candidate>(),
JumpTableBuilder.Build(exitDestination, exitDestination, null),
null);
for (var i = 0; i < tableBuilders.Count; i++)
{
if (tableBuilders[i].pathBuilder?.DefaultDestination == JumpTableBuilder.InvalidDestination)
{
tableBuilders[i].pathBuilder.DefaultDestination = exit;
}
if (tableBuilders[i].pathBuilder?.ExitDestination == JumpTableBuilder.InvalidDestination)
{
tableBuilders[i].pathBuilder.ExitDestination = exit;
}
if (tableBuilders[i].policyBuilder?.ExitDestination == JumpTableBuilder.InvalidDestination)
{
tableBuilders[i].policyBuilder.ExitDestination = exit;
}
}
for (var i = 0; i < states.Count; i++)
{
states[i] = new DfaState(
states[i].Candidates,
tableBuilders[i].pathBuilder?.Build(),
tableBuilders[i].policyBuilder?.Build());
}
return new DfaMatcher(_selector, states.ToArray(), maxSegmentCount);
return new DfaMatcher(_selector, states, maxSegmentCount);
}
private int AddNode(
DfaNode node,
List<DfaState> states,
List<(JumpTableBuilder pathBuilder, PolicyJumpTableBuilder policyBuilder)> tableBuilders)
DfaState[] states,
int exitDestination)
{
node.Matches.Sort(_comparer);
node.Matches?.Sort(_comparer);
var stateIndex = states.Count;
var currentStateIndex = _stateIndex;
var candidates = CreateCandidates(node.Matches);
states.Add(new DfaState(candidates, null, null));
var currentDefaultDestination = exitDestination;
var currentExitDestination = exitDestination;
(string text, int destination)[] pathEntries = null;
PolicyJumpTableEdge[] policyEntries = null;
var pathBuilder = new JumpTableBuilder();
tableBuilders.Add((pathBuilder, null));
foreach (var kvp in node.Literals)
if (node.Literals != null)
{
if (kvp.Key == null)
{
continue;
}
pathEntries = new (string text, int destination)[node.Literals.Count];
var transition = Transition(kvp.Value);
pathBuilder.AddEntry(kvp.Key, transition);
var index = 0;
foreach (var kvp in node.Literals)
{
var transition = Transition(kvp.Value);
pathEntries[index++] = (kvp.Key, transition);
}
}
if (node.Parameters != null &&
@ -294,53 +339,75 @@ namespace Microsoft.AspNetCore.Routing.Matching
{
// This node has a single transition to but it should accept zero-width segments
// this can happen when a node only has catchall parameters.
pathBuilder.DefaultDestination = Transition(node.Parameters);
pathBuilder.ExitDestination = pathBuilder.DefaultDestination;
currentExitDestination = currentDefaultDestination = Transition(node.Parameters);
}
else if (node.Parameters != null && node.CatchAll != null)
{
// This node has a separate transition for zero-width segments
// this can happen when a node has both parameters and catchall parameters.
pathBuilder.DefaultDestination = Transition(node.Parameters);
pathBuilder.ExitDestination = Transition(node.CatchAll);
currentDefaultDestination = Transition(node.Parameters);
currentExitDestination = Transition(node.CatchAll);
}
else if (node.Parameters != null)
{
// This node has paramters but no catchall.
pathBuilder.DefaultDestination = Transition(node.Parameters);
currentDefaultDestination = Transition(node.Parameters);
}
else if (node.CatchAll != null)
{
// This node has a catchall but no parameters
pathBuilder.DefaultDestination = Transition(node.CatchAll);
pathBuilder.ExitDestination = pathBuilder.DefaultDestination;
currentExitDestination = currentDefaultDestination = Transition(node.CatchAll);
}
if (node.PolicyEdges.Count > 0)
if (node.PolicyEdges != null && node.PolicyEdges.Count > 0)
{
var policyBuilder = new PolicyJumpTableBuilder(node.NodeBuilder);
tableBuilders[stateIndex] = (pathBuilder, policyBuilder);
policyEntries = new PolicyJumpTableEdge[node.PolicyEdges.Count];
var index = 0;
foreach (var kvp in node.PolicyEdges)
{
policyBuilder.AddEntry(kvp.Key, Transition(kvp.Value));
policyEntries[index++] = new PolicyJumpTableEdge(kvp.Key, Transition(kvp.Value));
}
}
return stateIndex;
var candidates = CreateCandidates(node.Matches);
states[currentStateIndex] = new DfaState(
candidates,
JumpTableBuilder.Build(currentDefaultDestination, currentExitDestination, pathEntries),
BuildPolicy(currentExitDestination, node.NodeBuilder, policyEntries));
return currentStateIndex;
int Transition(DfaNode next)
{
// Break cycles
return ReferenceEquals(node, next) ? stateIndex : AddNode(next, states, tableBuilders);
if (ReferenceEquals(node, next))
{
return _stateIndex;
}
else
{
_stateIndex++;
return AddNode(next, states, exitDestination);
}
}
}
private static PolicyJumpTable BuildPolicy(int exitDestination, INodeBuilderPolicy nodeBuilder, PolicyJumpTableEdge[] policyEntries)
{
if (policyEntries == null)
{
return null;
}
return nodeBuilder.BuildJumpTable(exitDestination, policyEntries);
}
// Builds an array of candidates for a node, assigns a 'score' for each
// endpoint.
internal Candidate[] CreateCandidates(IReadOnlyList<Endpoint> endpoints)
{
if (endpoints.Count == 0)
if (endpoints == null || endpoints.Count == 0)
{
return Array.Empty<Candidate>();
}
@ -370,20 +437,20 @@ namespace Microsoft.AspNetCore.Routing.Matching
// internal for tests
internal Candidate CreateCandidate(Endpoint endpoint, int score)
{
var assignments = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
var slots = new List<KeyValuePair<string, object>>();
var captures = new List<(string parameterName, int segmentIndex, int slotIndex)>();
(string parameterName, int segmentIndex, int slotIndex) catchAll = default;
_assignments.Clear();
_slots.Clear();
_captures.Clear();
_complexSegments.Clear();
_constraints.Clear();
var complexSegments = new List<(RoutePatternPathSegment pathSegment, int segmentIndex)>();
var constraints = new List<KeyValuePair<string, IRouteConstraint>>();
(string parameterName, int segmentIndex, int slotIndex) catchAll = default;
if (endpoint is RouteEndpoint routeEndpoint)
{
foreach (var kvp in routeEndpoint.RoutePattern.Defaults)
{
assignments.Add(kvp.Key, assignments.Count);
slots.Add(kvp);
_assignments.Add(kvp.Key, _assignments.Count);
_slots.Add(kvp);
}
for (var i = 0; i < routeEndpoint.RoutePattern.PathSegments.Count; i++)
@ -400,13 +467,13 @@ namespace Microsoft.AspNetCore.Routing.Matching
continue;
}
if (!assignments.TryGetValue(parameterPart.Name, out var slotIndex))
if (!_assignments.TryGetValue(parameterPart.Name, out var slotIndex))
{
slotIndex = assignments.Count;
assignments.Add(parameterPart.Name, slotIndex);
slotIndex = _assignments.Count;
_assignments.Add(parameterPart.Name, slotIndex);
var hasDefaultValue = parameterPart.Default != null || parameterPart.IsCatchAll;
slots.Add(hasDefaultValue ? new KeyValuePair<string, object>(parameterPart.Name, parameterPart.Default) : default);
_slots.Add(hasDefaultValue ? new KeyValuePair<string, object>(parameterPart.Name, parameterPart.Default) : default);
}
if (parameterPart.IsCatchAll)
@ -415,7 +482,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
}
else
{
captures.Add((parameterPart.Name, i, slotIndex));
_captures.Add((parameterPart.Name, i, slotIndex));
}
}
@ -427,7 +494,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
continue;
}
complexSegments.Add((segment, i));
_complexSegments.Add((segment, i));
}
foreach (var kvp in routeEndpoint.RoutePattern.ParameterPolicies)
@ -440,7 +507,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
var parameterPolicy = _parameterPolicyFactory.Create(parameter, reference);
if (parameterPolicy is IRouteConstraint routeConstraint)
{
constraints.Add(new KeyValuePair<string, IRouteConstraint>(kvp.Key, routeConstraint));
_constraints.Add(new KeyValuePair<string, IRouteConstraint>(kvp.Key, routeConstraint));
}
}
}
@ -449,16 +516,17 @@ namespace Microsoft.AspNetCore.Routing.Matching
return new Candidate(
endpoint,
score,
slots.ToArray(),
captures.ToArray(),
_slots.ToArray(),
_captures.ToArray(),
catchAll,
complexSegments.ToArray(),
constraints.ToArray());
_complexSegments.ToArray(),
_constraints.ToArray());
}
private int[] GetGroupLengths(DfaNode node)
{
if (node.Matches.Count == 0)
var nodeMatches = node.Matches;
if (nodeMatches == null || nodeMatches.Count == 0)
{
return Array.Empty<int>();
}
@ -466,16 +534,16 @@ namespace Microsoft.AspNetCore.Routing.Matching
var groups = new List<int>();
var length = 1;
var exemplar = node.Matches[0];
var exemplar = nodeMatches[0];
for (var i = 1; i < node.Matches.Count; i++)
for (var i = 1; i < nodeMatches.Count; i++)
{
if (!_comparer.Equals(exemplar, node.Matches[i]))
if (!_comparer.Equals(exemplar, nodeMatches[i]))
{
groups.Add(length);
length = 0;
exemplar = node.Matches[i];
exemplar = nodeMatches[i];
}
length++;
@ -517,24 +585,35 @@ namespace Microsoft.AspNetCore.Routing.Matching
private void ApplyPolicies(DfaNode node)
{
if (node.Matches.Count == 0)
if (node.Matches == null || node.Matches.Count == 0)
{
return;
}
// Start with the current node as the root.
var work = new List<DfaNode>() { node, };
List<DfaNode> previousWork = null;
for (var i = 0; i < _nodeBuilders.Length; i++)
{
var nodeBuilder = _nodeBuilders[i];
// Build a list of each
var nextWork = new List<DfaNode>();
List<DfaNode> nextWork;
if (previousWork == null)
{
nextWork = new List<DfaNode>();
}
else
{
// Reuse previous collection for the next collection
previousWork.Clear();
nextWork = previousWork;
}
for (var j = 0; j < work.Count; j++)
{
var parent = work[j];
if (!nodeBuilder.AppliesToNode(parent.Matches))
if (!nodeBuilder.AppliesToNode(parent.Matches ?? (IReadOnlyList<Endpoint>)Array.Empty<Endpoint>()))
{
// This node-builder doesn't care about this node, so add it to the list
// to be processed by the next node-builder.
@ -544,29 +623,34 @@ namespace Microsoft.AspNetCore.Routing.Matching
// This node-builder does apply to this node, so we need to create new nodes for each edge,
// and then attach them to the parent.
var edges = nodeBuilder.GetEdges(parent.Matches);
var edges = nodeBuilder.GetEdges(parent.Matches ?? (IReadOnlyList<Endpoint>)Array.Empty<Endpoint>());
for (var k = 0; k < edges.Count; k++)
{
var edge = edges[k];
var next = new DfaNode()
{
Label = parent.Label + " " + edge.State.ToString(),
// If parent label is null then labels are not being included
Label = (parent.Label != null) ? parent.Label + " " + edge.State.ToString() : null,
};
next.Matches.AddRange(edge.Endpoints);
if (edge.Endpoints.Count > 0)
{
next.AddMatches(edge.Endpoints);
}
nextWork.Add(next);
parent.PolicyEdges.Add(edge.State, next);
parent.AddPolicyEdge(edge.State, next);
}
// Associate the node-builder so we can build a jump table later.
parent.NodeBuilder = nodeBuilder;
// The parent no longer has matches, it's not considered a terminal node.
parent.Matches.Clear();
parent.Matches?.Clear();
}
previousWork = work;
work = nextWork;
}
}

View File

@ -14,13 +14,6 @@ namespace Microsoft.AspNetCore.Routing.Matching
[DebuggerDisplay("{DebuggerToString(),nq}")]
internal class DfaNode
{
public DfaNode()
{
Literals = new Dictionary<string, DfaNode>(StringComparer.OrdinalIgnoreCase);
Matches = new List<Endpoint>();
PolicyEdges = new Dictionary<object, DfaNode>();
}
// The depth of the node. The depth indicates the number of segments
// that must be processed to arrive at this node.
//
@ -30,9 +23,9 @@ namespace Microsoft.AspNetCore.Routing.Matching
// Just for diagnostics and debugging
public string Label { get; set; }
public List<Endpoint> Matches { get; }
public List<Endpoint> Matches { get; private set; }
public Dictionary<string, DfaNode> Literals { get; }
public Dictionary<string, DfaNode> Literals { get; private set; }
public DfaNode Parameters { get; set; }
@ -40,13 +33,58 @@ namespace Microsoft.AspNetCore.Routing.Matching
public INodeBuilderPolicy NodeBuilder { get; set; }
public Dictionary<object, DfaNode> PolicyEdges { get; }
public Dictionary<object, DfaNode> PolicyEdges { get; private set; }
public void AddPolicyEdge(object state, DfaNode node)
{
if (PolicyEdges == null)
{
PolicyEdges = new Dictionary<object, DfaNode>();
}
PolicyEdges.Add(state, node);
}
public void AddLiteral(string literal, DfaNode node)
{
if (Literals == null)
{
Literals = new Dictionary<string, DfaNode>(StringComparer.OrdinalIgnoreCase);
}
Literals.Add(literal, node);
}
public void AddMatch(Endpoint endpoint)
{
if (Matches == null)
{
Matches = new List<Endpoint>();
}
Matches.Add(endpoint);
}
public void AddMatches(IEnumerable<Endpoint> endpoints)
{
if (Matches == null)
{
Matches = new List<Endpoint>(endpoints);
}
else
{
Matches.AddRange(endpoints);
}
}
public void Visit(Action<DfaNode> visitor)
{
foreach (var kvp in Literals)
if (Literals != null)
{
kvp.Value.Visit(visitor);
foreach (var kvp in Literals)
{
kvp.Value.Visit(visitor);
}
}
// Break cycles
@ -61,9 +99,12 @@ namespace Microsoft.AspNetCore.Routing.Matching
CatchAll.Visit(visitor);
}
foreach (var kvp in PolicyEdges)
if (PolicyEdges != null)
{
kvp.Value.Visit(visitor);
foreach (var kvp in PolicyEdges)
{
kvp.Value.Visit(visitor);
}
}
visitor(this);
@ -76,9 +117,12 @@ namespace Microsoft.AspNetCore.Routing.Matching
builder.Append(" d:");
builder.Append(PathDepth);
builder.Append(" m:");
builder.Append(Matches.Count);
builder.Append(Matches?.Count ?? 0);
builder.Append(" c: ");
builder.Append(string.Join(", ", Literals.Select(kvp => $"{kvp.Key}->({FormatNode(kvp.Value)})")));
if (Literals != null)
{
builder.Append(string.Join(", ", Literals.Select(kvp => $"{kvp.Key}->({FormatNode(kvp.Value)})")));
}
return builder.ToString();
// DfaNodes can be self-referential, don't traverse cycles.

View File

@ -56,7 +56,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
for (var i = 0; i < endpoints.Count; i++)
{
if (endpoints[i].Metadata.GetMetadata<IHttpMethodMetadata>()?.HttpMethods.Any() == true)
if (endpoints[i].Metadata.GetMetadata<IHttpMethodMetadata>()?.HttpMethods.Count > 0)
{
return true;
}
@ -80,7 +80,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
//
// For now we're just building up the set of keys. We don't add any endpoints
// to lists now because we don't want ordering problems.
var allHttpMethods = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var allHttpMethods = new List<string>();
var edges = new Dictionary<EdgeKey, List<Endpoint>>();
for (var i = 0; i < endpoints.Count; i++)
{
@ -120,11 +120,16 @@ namespace Microsoft.AspNetCore.Routing.Matching
// Also if it's not the *any* method key, then track it.
if (!string.Equals(AnyMethod, httpMethod, StringComparison.OrdinalIgnoreCase))
{
allHttpMethods.Add(httpMethod);
if (!ContainsHttpMethod(allHttpMethods, httpMethod))
{
allHttpMethods.Add(httpMethod);
}
}
}
}
allHttpMethods.Sort(StringComparer.OrdinalIgnoreCase);
// Now in a second loop, add endpoints to these lists. We've enumerated all of
// the states, so we want to see which states this endpoint matches.
for (var i = 0; i < endpoints.Count; i++)
@ -185,14 +190,19 @@ namespace Microsoft.AspNetCore.Routing.Matching
if (!edges.TryGetValue(new EdgeKey(AnyMethod, false), out var matches))
{
// Methods sorted for testability.
var endpoint = CreateRejectionEndpoint(allHttpMethods.OrderBy(m => m));
var endpoint = CreateRejectionEndpoint(allHttpMethods);
matches = new List<Endpoint>() { endpoint, };
edges[new EdgeKey(AnyMethod, false)] = matches;
}
return edges
.Select(kvp => new PolicyNodeEdge(kvp.Key, kvp.Value))
.ToArray();
var policyNodeEdges = new PolicyNodeEdge[edges.Count];
var index = 0;
foreach (var kvp in edges)
{
policyNodeEdges[index++] = new PolicyNodeEdge(kvp.Key, kvp.Value);
}
return policyNodeEdges;
(IReadOnlyList<string> httpMethods, bool acceptCorsPreflight) GetHttpMethods(Endpoint e)
{
@ -209,31 +219,41 @@ namespace Microsoft.AspNetCore.Routing.Matching
/// <returns></returns>
public PolicyJumpTable BuildJumpTable(int exitDestination, IReadOnlyList<PolicyJumpTableEdge> edges)
{
var destinations = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
var corsPreflightDestinations = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
Dictionary<string, int> destinations = null;
Dictionary<string, int> corsPreflightDestinations = null;
for (var i = 0; i < edges.Count; i++)
{
// We create this data, so it's safe to cast it.
var key = (EdgeKey)edges[i].State;
if (key.IsCorsPreflightRequest)
{
if (corsPreflightDestinations == null)
{
corsPreflightDestinations = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
}
corsPreflightDestinations.Add(key.HttpMethod, edges[i].Destination);
}
else
{
if (destinations == null)
{
destinations = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
}
destinations.Add(key.HttpMethod, edges[i].Destination);
}
}
int corsPreflightExitDestination = exitDestination;
if (corsPreflightDestinations.TryGetValue(AnyMethod, out var matchesAnyVerb))
if (corsPreflightDestinations != null && corsPreflightDestinations.TryGetValue(AnyMethod, out var matchesAnyVerb))
{
// If we have endpoints that match any HTTP method, use that as the exit.
corsPreflightExitDestination = matchesAnyVerb;
corsPreflightDestinations.Remove(AnyMethod);
}
if (destinations.TryGetValue(AnyMethod, out matchesAnyVerb))
if (destinations != null && destinations.TryGetValue(AnyMethod, out matchesAnyVerb))
{
// If we have endpoints that match any HTTP method, use that as the exit.
exitDestination = matchesAnyVerb;
@ -261,6 +281,19 @@ namespace Microsoft.AspNetCore.Routing.Matching
Http405EndpointDisplayName);
}
private static bool ContainsHttpMethod(List<string> httpMethods, string httpMethod)
{
for (var i = 0; i < httpMethods.Count; i++)
{
if (StringComparer.OrdinalIgnoreCase.Equals(httpMethods[i], httpMethod))
{
return true;
}
}
return false;
}
private class HttpMethodPolicyJumpTable : PolicyJumpTable
{
private readonly int _exitDestination;
@ -281,7 +314,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
_corsPreflightExitDestination = corsPreflightExitDestination;
_corsPreflightDestinations = corsPreflightDestinations;
_supportsCorsPreflight = _corsPreflightDestinations.Count > 0;
_supportsCorsPreflight = _corsPreflightDestinations != null && _corsPreflightDestinations.Count > 0;
}
public override int GetDestination(HttpContext httpContext)
@ -295,12 +328,14 @@ namespace Microsoft.AspNetCore.Routing.Matching
httpContext.Request.Headers.TryGetValue(AccessControlRequestMethod, out var accessControlRequestMethod) &&
!StringValues.IsNullOrEmpty(accessControlRequestMethod))
{
return _corsPreflightDestinations.TryGetValue(accessControlRequestMethod, out destination)
return _corsPreflightDestinations != null &&
_corsPreflightDestinations.TryGetValue(accessControlRequestMethod, out destination)
? destination
: _corsPreflightExitDestination;
}
return _destinations.TryGetValue(httpMethod, out destination) ? destination : _exitDestination;
return _destinations != null &&
_destinations.TryGetValue(httpMethod, out destination) ? destination : _exitDestination;
}
}
@ -362,7 +397,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
public override int GetHashCode()
{
var hash = new HashCodeCombiner();
hash.Add(IsCorsPreflightRequest);
hash.Add(IsCorsPreflightRequest ? 1 : 0);
hash.Add(HttpMethod, StringComparer.Ordinal);
return hash;
}

View File

@ -2,39 +2,24 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
namespace Microsoft.AspNetCore.Routing.Matching
{
internal class JumpTableBuilder
internal static class JumpTableBuilder
{
public static readonly int InvalidDestination = -1;
private readonly List<(string text, int destination)> _entries = new List<(string text, int destination)>();
// The destination state when none of the text entries match.
public int DefaultDestination { get; set; } = InvalidDestination;
// The destination state for a zero-length segment. This is a special
// case because parameters don't match a zero-length segment.
public int ExitDestination { get; set; } = InvalidDestination;
public void AddEntry(string text, int destination)
public static JumpTable Build(int defaultDestination, int exitDestination, (string text, int destination)[] pathEntries)
{
_entries.Add((text, destination));
}
public JumpTable Build()
{
if (DefaultDestination == InvalidDestination)
if (defaultDestination == InvalidDestination)
{
var message = $"{nameof(DefaultDestination)} is not set. Please report this as a bug.";
var message = $"{nameof(defaultDestination)} is not set. Please report this as a bug.";
throw new InvalidOperationException(message);
}
if (ExitDestination == InvalidDestination)
if (exitDestination == InvalidDestination)
{
var message = $"{nameof(ExitDestination)} is not set. Please report this as a bug.";
var message = $"{nameof(exitDestination)} is not set. Please report this as a bug.";
throw new InvalidOperationException(message);
}
@ -49,16 +34,16 @@ namespace Microsoft.AspNetCore.Routing.Matching
// We have an optimized fast path for zero entries since we don't have to
// do any string comparisons.
if (_entries.Count == 0)
if (pathEntries == null || pathEntries.Length == 0)
{
return new ZeroEntryJumpTable(DefaultDestination, ExitDestination);
return new ZeroEntryJumpTable(defaultDestination, exitDestination);
}
// The IL Emit jump table is not faster for a single entry
if (_entries.Count == 1)
if (pathEntries.Length == 1)
{
var entry = _entries[0];
return new SingleEntryJumpTable(DefaultDestination, ExitDestination, entry.text, entry.destination);
var entry = pathEntries[0];
return new SingleEntryJumpTable(defaultDestination, exitDestination, entry.text, entry.destination);
}
// We choose a hard upper bound of 100 as the limit for when we switch to a dictionary
@ -72,9 +57,9 @@ namespace Microsoft.AspNetCore.Routing.Matching
// Additionally if we're on 32bit, the scalability is worse, so switch to the dictionary at 50
// entries.
var threshold = IntPtr.Size == 8 ? 100 : 50;
if (_entries.Count >= threshold)
if (pathEntries.Length >= threshold)
{
return new DictionaryJumpTable(DefaultDestination, ExitDestination, _entries.ToArray());
return new DictionaryJumpTable(defaultDestination, exitDestination, pathEntries);
}
// If we have more than a single string, the IL emit strategy is the fastest - but we need to decide
@ -82,18 +67,17 @@ namespace Microsoft.AspNetCore.Routing.Matching
JumpTable fallback;
// Based on our testing a linear search is still faster than a dictionary at ten entries.
if (_entries.Count <= 10)
if (pathEntries.Length <= 10)
{
fallback = new LinearSearchJumpTable(DefaultDestination, ExitDestination, _entries.ToArray());
fallback = new LinearSearchJumpTable(defaultDestination, exitDestination, pathEntries);
}
else
{
fallback = new DictionaryJumpTable(DefaultDestination, ExitDestination, _entries.ToArray());
fallback = new DictionaryJumpTable(defaultDestination, exitDestination, pathEntries);
}
#if IL_EMIT
return new ILEmitTrieJumpTable(DefaultDestination, ExitDestination, _entries.ToArray(), vectorize: null, fallback);
return new ILEmitTrieJumpTable(defaultDestination, exitDestination, pathEntries, vectorize: null, fallback);
#else
return fallback;
#endif

View File

@ -1,32 +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 System.Collections.Generic;
namespace Microsoft.AspNetCore.Routing.Matching
{
internal class PolicyJumpTableBuilder
{
private readonly INodeBuilderPolicy _nodeBuilder;
private readonly List<PolicyJumpTableEdge> _entries;
public PolicyJumpTableBuilder(INodeBuilderPolicy nodeBuilder)
{
_nodeBuilder = nodeBuilder;
_entries = new List<PolicyJumpTableEdge>();
}
// The destination state for a non-match.
public int ExitDestination { get; set; } = JumpTableBuilder.InvalidDestination;
public void AddEntry(object state, int destination)
{
_entries.Add(new PolicyJumpTableEdge(state, destination));
}
public PolicyJumpTable Build()
{
return _nodeBuilder.BuildJumpTable(ExitDestination, _entries);
}
}
}

View File

@ -2,7 +2,9 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Microsoft.AspNetCore.Routing.Constraints;
using Microsoft.AspNetCore.Routing.Matching;
@ -16,6 +18,12 @@ namespace Microsoft.AspNetCore.Routing.Patterns
/// </summary>
public static class RoutePatternFactory
{
private static readonly IReadOnlyDictionary<string, object> EmptyDefaultsDictionary =
new ReadOnlyDictionary<string, object>(new Dictionary<string, object>());
private static readonly IReadOnlyDictionary<string, IReadOnlyList<RoutePatternParameterPolicyReference>> EmptyPoliciesDictionary =
new ReadOnlyDictionary<string, IReadOnlyList<RoutePatternParameterPolicyReference>>(new Dictionary<string, IReadOnlyList<RoutePatternParameterPolicyReference>>());
/// <summary>
/// Creates a <see cref="RoutePattern"/> from its string representation.
/// </summary>
@ -242,8 +250,8 @@ namespace Microsoft.AspNetCore.Routing.Patterns
private static RoutePattern PatternCore(
string rawText,
IDictionary<string, object> defaults,
IDictionary<string, object> parameterPolicies,
RouteValueDictionary defaults,
RouteValueDictionary parameterPolicies,
IEnumerable<RoutePatternPathSegment> segments)
{
// We want to merge the segment data with the 'out of line' defaults and parameter policies.
@ -257,18 +265,22 @@ namespace Microsoft.AspNetCore.Routing.Patterns
// It's important that these two views of the data are consistent. We don't want
// values specified out of line to have a different behavior.
var updatedDefaults = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
if (defaults != null)
Dictionary<string, object> updatedDefaults = null;
if (defaults != null && defaults.Count > 0)
{
updatedDefaults = new Dictionary<string, object>(defaults.Count, StringComparer.OrdinalIgnoreCase);
foreach (var kvp in defaults)
{
updatedDefaults.Add(kvp.Key, kvp.Value);
}
}
var updatedParameterPolicies = new Dictionary<string, List<RoutePatternParameterPolicyReference>>(StringComparer.OrdinalIgnoreCase);
if (parameterPolicies != null)
Dictionary<string, List<RoutePatternParameterPolicyReference>> updatedParameterPolicies = null;
if (parameterPolicies != null && parameterPolicies.Count > 0)
{
updatedParameterPolicies = new Dictionary<string, List<RoutePatternParameterPolicyReference>>(parameterPolicies.Count, StringComparer.OrdinalIgnoreCase);
foreach (var kvp in parameterPolicies)
{
updatedParameterPolicies.Add(kvp.Key, new List<RoutePatternParameterPolicyReference>()
@ -280,7 +292,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns
}
}
var parameters = new List<RoutePatternParameterPart>();
List<RoutePatternParameterPart> parameters = null;
var updatedSegments = segments.ToArray();
for (var i = 0; i < updatedSegments.Length; i++)
{
@ -291,6 +303,11 @@ namespace Microsoft.AspNetCore.Routing.Patterns
{
if (segment.Parts[j] is RoutePatternParameterPart parameter)
{
if (parameters == null)
{
parameters = new List<RoutePatternParameterPart>();
}
parameters.Add(parameter);
}
}
@ -298,9 +315,11 @@ namespace Microsoft.AspNetCore.Routing.Patterns
return new RoutePattern(
rawText,
updatedDefaults,
updatedParameterPolicies.ToDictionary(kvp => kvp.Key, kvp => (IReadOnlyList<RoutePatternParameterPolicyReference>)kvp.Value.ToArray()),
parameters,
updatedDefaults ?? EmptyDefaultsDictionary,
updatedParameterPolicies != null
? updatedParameterPolicies.ToDictionary(kvp => kvp.Key, kvp => (IReadOnlyList<RoutePatternParameterPolicyReference>)kvp.Value.ToArray())
: EmptyPoliciesDictionary,
(IReadOnlyList<RoutePatternParameterPart>)parameters ?? Array.Empty<RoutePatternParameterPart>(),
updatedSegments);
RoutePatternPathSegment VisitSegment(RoutePatternPathSegment segment)
@ -341,7 +360,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns
var parameter = (RoutePatternParameterPart)part;
var @default = parameter.Default;
if (updatedDefaults.TryGetValue(parameter.Name, out var newDefault))
if (updatedDefaults != null && updatedDefaults.TryGetValue(parameter.Name, out var newDefault))
{
if (parameter.Default != null && !Equals(newDefault, parameter.Default))
{
@ -360,12 +379,23 @@ namespace Microsoft.AspNetCore.Routing.Patterns
if (parameter.Default != null)
{
if (updatedDefaults == null)
{
updatedDefaults = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
}
updatedDefaults[parameter.Name] = parameter.Default;
}
if (!updatedParameterPolicies.TryGetValue(parameter.Name, out var parameterConstraints) &&
List<RoutePatternParameterPolicyReference> parameterConstraints = null;
if ((updatedParameterPolicies == null || !updatedParameterPolicies.TryGetValue(parameter.Name, out parameterConstraints)) &&
parameter.ParameterPolicies.Count > 0)
{
if (updatedParameterPolicies == null)
{
updatedParameterPolicies = new Dictionary<string, List<RoutePatternParameterPolicyReference>>(StringComparer.OrdinalIgnoreCase);
}
parameterConstraints = new List<RoutePatternParameterPolicyReference>();
updatedParameterPolicies.Add(parameter.Name, parameterConstraints);
}
@ -391,6 +421,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns
parameter.EncodeSlashes);
}
}
/// <summary>
/// Creates a <see cref="RoutePatternPathSegment"/> from the provided collection
/// of parts.

View File

@ -30,7 +30,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
// Assert
Assert.Same(endpoint, Assert.Single(root.Matches));
Assert.Null(root.Parameters);
Assert.Empty(root.Literals);
Assert.Null(root.Literals);
}
[Fact]
@ -46,21 +46,21 @@ namespace Microsoft.AspNetCore.Routing.Matching
var root = builder.BuildDfaTree();
// Assert
Assert.Empty(root.Matches);
Assert.Null(root.Matches);
Assert.Null(root.Parameters);
var next = Assert.Single(root.Literals);
Assert.Equal("a", next.Key);
var a = next.Value;
Assert.Empty(a.Matches);
Assert.Null(a.Matches);
Assert.Null(a.Parameters);
next = Assert.Single(a.Literals);
Assert.Equal("b", next.Key);
var b = next.Value;
Assert.Empty(b.Matches);
Assert.Null(b.Matches);
Assert.Null(b.Parameters);
next = Assert.Single(b.Literals);
@ -69,7 +69,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
var c = next.Value;
Assert.Same(endpoint, Assert.Single(c.Matches));
Assert.Null(c.Parameters);
Assert.Empty(c.Literals);
Assert.Null(c.Literals);
}
[Fact]
@ -85,21 +85,21 @@ namespace Microsoft.AspNetCore.Routing.Matching
var root = builder.BuildDfaTree();
// Assert
Assert.Empty(root.Matches);
Assert.Empty(root.Literals);
Assert.Null(root.Matches);
Assert.Null(root.Literals);
var a = root.Parameters;
Assert.Empty(a.Matches);
Assert.Empty(a.Literals);
Assert.Null(a.Matches);
Assert.Null(a.Literals);
var b = a.Parameters;
Assert.Empty(b.Matches);
Assert.Empty(b.Literals);
Assert.Null(b.Matches);
Assert.Null(b.Literals);
var c = b.Parameters;
Assert.Same(endpoint, Assert.Single(c.Matches));
Assert.Null(c.Parameters);
Assert.Empty(c.Literals);
Assert.Null(c.Literals);
}
[Fact]
@ -115,21 +115,21 @@ namespace Microsoft.AspNetCore.Routing.Matching
var root = builder.BuildDfaTree();
// Assert
Assert.Empty(root.Matches);
Assert.Empty(root.Literals);
Assert.Null(root.Matches);
Assert.Null(root.Literals);
var a = root.Parameters;
// The catch all can match a path like '/a'
Assert.Same(endpoint, Assert.Single(a.Matches));
Assert.Empty(a.Literals);
Assert.Null(a.Literals);
Assert.Null(a.Parameters);
// Catch-all nodes include an extra transition that loops to process
// extra segments.
var catchAll = a.CatchAll;
Assert.Same(endpoint, Assert.Single(catchAll.Matches));
Assert.Empty(catchAll.Literals);
Assert.Null(catchAll.Literals);
Assert.Same(catchAll, catchAll.Parameters);
Assert.Same(catchAll, catchAll.CatchAll);
}
@ -148,13 +148,13 @@ namespace Microsoft.AspNetCore.Routing.Matching
// Assert
Assert.Same(endpoint, Assert.Single(root.Matches));
Assert.Empty(root.Literals);
Assert.Null(root.Literals);
// Catch-all nodes include an extra transition that loops to process
// extra segments.
var catchAll = root.CatchAll;
Assert.Same(endpoint, Assert.Single(catchAll.Matches));
Assert.Empty(catchAll.Literals);
Assert.Null(catchAll.Literals);
Assert.Same(catchAll, catchAll.Parameters);
}
@ -174,19 +174,19 @@ namespace Microsoft.AspNetCore.Routing.Matching
var root = builder.BuildDfaTree();
// Assert
Assert.Empty(root.Matches);
Assert.Null(root.Matches);
Assert.Null(root.Parameters);
var next = Assert.Single(root.Literals);
Assert.Equal("a", next.Key);
var a = next.Value;
Assert.Empty(a.Matches);
Assert.Null(a.Matches);
Assert.Equal(2, a.Literals.Count);
var b1 = a.Literals["b1"];
Assert.Empty(b1.Matches);
Assert.Null(b1.Matches);
Assert.Null(b1.Parameters);
next = Assert.Single(b1.Literals);
@ -195,10 +195,10 @@ namespace Microsoft.AspNetCore.Routing.Matching
var c1 = next.Value;
Assert.Same(endpoint1, Assert.Single(c1.Matches));
Assert.Null(c1.Parameters);
Assert.Empty(c1.Literals);
Assert.Null(c1.Literals);
var b2 = a.Literals["b2"];
Assert.Empty(b2.Matches);
Assert.Null(b2.Matches);
Assert.Null(b2.Parameters);
next = Assert.Single(b2.Literals);
@ -207,7 +207,59 @@ namespace Microsoft.AspNetCore.Routing.Matching
var c2 = next.Value;
Assert.Same(endpoint2, Assert.Single(c2.Matches));
Assert.Null(c2.Parameters);
Assert.Empty(c2.Literals);
Assert.Null(c2.Literals);
}
[Fact]
public void BuildDfaTree_MultipleEndpoint_LiteralDifferentCase()
{
// Arrange
var builder = CreateDfaMatcherBuilder();
var endpoint1 = CreateEndpoint("a/b1/c");
builder.AddEndpoint(endpoint1);
var endpoint2 = CreateEndpoint("A/b2/c");
builder.AddEndpoint(endpoint2);
// Act
var root = builder.BuildDfaTree();
// Assert
Assert.Null(root.Matches);
Assert.Null(root.Parameters);
var next = Assert.Single(root.Literals);
Assert.Equal("a", next.Key);
var a = next.Value;
Assert.Null(a.Matches);
Assert.Equal(2, a.Literals.Count);
var b1 = a.Literals["b1"];
Assert.Null(b1.Matches);
Assert.Null(b1.Parameters);
next = Assert.Single(b1.Literals);
Assert.Equal("c", next.Key);
var c1 = next.Value;
Assert.Same(endpoint1, Assert.Single(c1.Matches));
Assert.Null(c1.Parameters);
Assert.Null(c1.Literals);
var b2 = a.Literals["b2"];
Assert.Null(b2.Matches);
Assert.Null(b2.Parameters);
next = Assert.Single(b2.Literals);
Assert.Equal("c", next.Key);
var c2 = next.Value;
Assert.Same(endpoint2, Assert.Single(c2.Matches));
Assert.Null(c2.Parameters);
Assert.Null(c2.Literals);
}
[Fact]
@ -226,20 +278,20 @@ namespace Microsoft.AspNetCore.Routing.Matching
var root = builder.BuildDfaTree();
// Assert
Assert.Empty(root.Matches);
Assert.Null(root.Matches);
Assert.Null(root.Parameters);
var next = Assert.Single(root.Literals);
Assert.Equal("a", next.Key);
var a = next.Value;
Assert.Empty(a.Matches);
Assert.Null(a.Matches);
next = Assert.Single(a.Literals);
Assert.Equal("b", next.Key);
var b = next.Value;
Assert.Empty(b.Matches);
Assert.Null(b.Matches);
Assert.Null(b.Parameters);
next = Assert.Single(b.Literals);
@ -251,10 +303,10 @@ namespace Microsoft.AspNetCore.Routing.Matching
e => Assert.Same(endpoint1, e),
e => Assert.Same(endpoint2, e));
Assert.Null(c1.Parameters);
Assert.Empty(c1.Literals);
Assert.Null(c1.Literals);
var b2 = a.Parameters;
Assert.Empty(b2.Matches);
Assert.Null(b2.Matches);
Assert.Null(b2.Parameters);
next = Assert.Single(b2.Literals);
@ -263,7 +315,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
var c2 = next.Value;
Assert.Same(endpoint2, Assert.Single(c2.Matches));
Assert.Null(c2.Parameters);
Assert.Empty(c2.Literals);
Assert.Null(c2.Literals);
}
[Fact]
@ -282,18 +334,18 @@ namespace Microsoft.AspNetCore.Routing.Matching
var root = builder.BuildDfaTree();
// Assert
Assert.Empty(root.Matches);
Assert.Null(root.Matches);
Assert.Null(root.Parameters);
var next = Assert.Single(root.Literals);
Assert.Equal("a", next.Key);
var a = next.Value;
Assert.Empty(a.Matches);
Assert.Empty(a.Literals);
Assert.Null(a.Matches);
Assert.Null(a.Literals);
var b = a.Parameters;
Assert.Empty(b.Matches);
Assert.Null(b.Matches);
Assert.Null(b.Parameters);
next = Assert.Single(b.Literals);
@ -305,7 +357,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
e => Assert.Same(endpoint1, e),
e => Assert.Same(endpoint2, e));
Assert.Null(c.Parameters);
Assert.Empty(c.Literals);
Assert.Null(c.Literals);
}
[Fact]
@ -324,7 +376,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
var root = builder.BuildDfaTree();
// Assert
Assert.Empty(root.Matches);
Assert.Null(root.Matches);
Assert.Null(root.Parameters);
var next = Assert.Single(root.Literals);
@ -349,7 +401,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
e => Assert.Same(endpoint1, e),
e => Assert.Same(endpoint2, e));
Assert.Null(c1.Parameters);
Assert.Empty(c1.Literals);
Assert.Null(c1.Literals);
var catchAll = a.CatchAll;
Assert.Same(endpoint2, Assert.Single(catchAll.Matches));
@ -373,7 +425,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
var root = builder.BuildDfaTree();
// Assert
Assert.Empty(root.Matches);
Assert.Null(root.Matches);
Assert.Null(root.Parameters);
var next = Assert.Single(root.Literals);
@ -381,7 +433,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
var a = next.Value;
Assert.Same(endpoint2, Assert.Single(a.Matches));
Assert.Empty(a.Literals);
Assert.Null(a.Literals);
var b1 = a.Parameters;
Assert.Same(endpoint2, Assert.Single(a.Matches));
@ -396,7 +448,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
e => Assert.Same(endpoint1, e),
e => Assert.Same(endpoint2, e));
Assert.Null(c1.Parameters);
Assert.Empty(c1.Literals);
Assert.Null(c1.Literals);
var catchAll = a.CatchAll;
Assert.Same(endpoint2, Assert.Single(catchAll.Matches));
@ -417,7 +469,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
var root = builder.BuildDfaTree();
// Assert
Assert.Empty(root.Matches);
Assert.Null(root.Matches);
Assert.Null(root.Parameters);
var next = Assert.Single(root.Literals);
@ -440,7 +492,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
var test2_true = test1_0.PolicyEdges[true];
Assert.Same(endpoint1, Assert.Single(test2_true.Matches));
Assert.Null(test2_true.NodeBuilder);
Assert.Empty(test2_true.PolicyEdges);
Assert.Null(test2_true.PolicyEdges);
}
[Fact]
@ -462,7 +514,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
var root = builder.BuildDfaTree();
// Assert
Assert.Empty(root.Matches);
Assert.Null(root.Matches);
Assert.Null(root.Parameters);
var next = Assert.Single(root.Literals);
@ -486,7 +538,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
var test2_true = test1_0.PolicyEdges[true];
Assert.Same(endpoint1, Assert.Single(test2_true.Matches));
Assert.Null(test2_true.NodeBuilder);
Assert.Empty(test2_true.PolicyEdges);
Assert.Null(test2_true.PolicyEdges);
var test1_1 = a.PolicyEdges[1];
Assert.Empty(test1_1.Matches);
@ -499,12 +551,12 @@ namespace Microsoft.AspNetCore.Routing.Matching
test2_true = test1_1.PolicyEdges[true];
Assert.Same(endpoint2, Assert.Single(test2_true.Matches));
Assert.Null(test2_true.NodeBuilder);
Assert.Empty(test2_true.PolicyEdges);
Assert.Null(test2_true.PolicyEdges);
var test2_false = test1_1.PolicyEdges[false];
Assert.Same(endpoint3, Assert.Single(test2_false.Matches));
Assert.Null(test2_false.NodeBuilder);
Assert.Empty(test2_false.PolicyEdges);
Assert.Null(test2_false.PolicyEdges);
}
[Fact]
@ -526,7 +578,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
var root = builder.BuildDfaTree();
// Assert
Assert.Empty(root.Matches);
Assert.Null(root.Matches);
Assert.Null(root.Parameters);
var next = Assert.Single(root.Literals);
@ -543,12 +595,12 @@ namespace Microsoft.AspNetCore.Routing.Matching
var test2_true = a.PolicyEdges[true];
Assert.Equal(new[] { endpoint1, endpoint2, }, test2_true.Matches);
Assert.Null(test2_true.NodeBuilder);
Assert.Empty(test2_true.PolicyEdges);
Assert.Null(test2_true.PolicyEdges);
var test2_false = a.PolicyEdges[false];
Assert.Equal(new[] { endpoint3, }, test2_false.Matches);
Assert.Null(test2_false.NodeBuilder);
Assert.Empty(test2_false.PolicyEdges);
Assert.Null(test2_false.PolicyEdges);
}
[Fact]
@ -570,7 +622,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
var root = builder.BuildDfaTree();
// Assert
Assert.Empty(root.Matches);
Assert.Null(root.Matches);
Assert.Null(root.Parameters);
var next = Assert.Single(root.Literals);
@ -587,12 +639,12 @@ namespace Microsoft.AspNetCore.Routing.Matching
var test1_0 = a.PolicyEdges[0];
Assert.Equal(new[] { endpoint1, }, test1_0.Matches);
Assert.Null(test1_0.NodeBuilder);
Assert.Empty(test1_0.PolicyEdges);
Assert.Null(test1_0.PolicyEdges);
var test1_1 = a.PolicyEdges[1];
Assert.Equal(new[] { endpoint2, endpoint3, }, test1_1.Matches);
Assert.Null(test1_1.NodeBuilder);
Assert.Empty(test1_1.PolicyEdges);
Assert.Null(test1_1.PolicyEdges);
}
[Fact]
@ -614,7 +666,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
var root = builder.BuildDfaTree();
// Assert
Assert.Empty(root.Matches);
Assert.Null(root.Matches);
Assert.Null(root.Parameters);
var next = Assert.Single(root.Literals);
@ -632,12 +684,12 @@ namespace Microsoft.AspNetCore.Routing.Matching
var test1_0 = a.PolicyEdges[0];
Assert.Equal(new[] { endpoint1, }, test1_0.Matches);
Assert.Null(test1_0.NodeBuilder);
Assert.Empty(test1_0.PolicyEdges);
Assert.Null(test1_0.PolicyEdges);
var test1_1 = a.PolicyEdges[1];
Assert.Equal(new[] { endpoint2, endpoint3, }, test1_1.Matches);
Assert.Null(test1_1.NodeBuilder);
Assert.Empty(test1_1.PolicyEdges);
Assert.Null(test1_1.PolicyEdges);
var nonRouteEndpoint = a.PolicyEdges[int.MaxValue];
Assert.Equal("MaxValueEndpoint", Assert.Single(nonRouteEndpoint.Matches).DisplayName);
@ -662,7 +714,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
var root = builder.BuildDfaTree();
// Assert
Assert.Empty(root.Matches);
Assert.Null(root.Matches);
Assert.Null(root.Parameters);
var next = Assert.Single(root.Literals);
@ -671,7 +723,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
var a = next.Value;
Assert.Equal(new[] { endpoint1, endpoint2, endpoint3, }, a.Matches);
Assert.Null(a.NodeBuilder);
Assert.Empty(a.PolicyEdges);
Assert.Null(a.PolicyEdges);
}
[Fact]

View File

@ -74,21 +74,21 @@ namespace Microsoft.AspNetCore.Routing
// This code was generated by the Swaggatherer
public partial class GeneratedBenchmark : EndpointRoutingBenchmarkBase
{{
private const int EndpointCount = {3};
private protected const int EndpointCount = {3};
private void SetupEndpoints()
private protected void SetupEndpoints()
{{
Endpoints = new RouteEndpoint[{3}];
{0}
}}
private void SetupRequests()
private protected void SetupRequests()
{{
Requests = new HttpContext[{3}];
{1}
}}
private Matcher SetupMatcher(MatcherBuilder builder)
private protected Matcher SetupMatcher(MatcherBuilder builder)
{{
{2}
return builder.Build();