Merge release/2.2
This commit is contained in:
commit
aadc31d9a5
|
|
@ -3,12 +3,11 @@
|
||||||
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BenchmarkDotNet.Attributes;
|
using BenchmarkDotNet.Attributes;
|
||||||
using Microsoft.AspNetCore.Http.Features;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Routing.Matching
|
namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
{
|
{
|
||||||
// Generated from https://github.com/Azure/azure-rest-api-specs
|
// Generated from https://github.com/Azure/azure-rest-api-specs
|
||||||
public partial class MatcherAzureBenchmark : EndpointRoutingBenchmarkBase
|
public class MatcherAzureBenchmark : MatcherAzureBenchmarkBase
|
||||||
{
|
{
|
||||||
private const int SampleCount = 100;
|
private const int SampleCount = 100;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,11 @@ using Microsoft.AspNetCore.Http;
|
||||||
namespace Microsoft.AspNetCore.Routing.Matching
|
namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
{
|
{
|
||||||
// This code was generated by the Swaggatherer
|
// 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 = new RouteEndpoint[5160];
|
||||||
Endpoints[0] = CreateEndpoint("/account", "GET");
|
Endpoints[0] = CreateEndpoint("/account", "GET");
|
||||||
|
|
@ -5176,7 +5176,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
Endpoints[5159] = CreateEndpoint("/{tenantID}/groups/{groupObjectId}/$links/members/{memberObjectId}", "DELETE");
|
Endpoints[5159] = CreateEndpoint("/{tenantID}/groups/{groupObjectId}/$links/members/{memberObjectId}", "DELETE");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetupRequests()
|
private protected void SetupRequests()
|
||||||
{
|
{
|
||||||
Requests = new HttpContext[5160];
|
Requests = new HttpContext[5160];
|
||||||
Requests[0] = new DefaultHttpContext();
|
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-";
|
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[0]);
|
||||||
builder.AddEndpoint(Endpoints[1]);
|
builder.AddEndpoint(Endpoints[1]);
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
{
|
{
|
||||||
// Generated from https://github.com/APIs-guru/openapi-directory
|
// Generated from https://github.com/APIs-guru/openapi-directory
|
||||||
// Use https://editor2.swagger.io/ to convert from yaml to json-
|
// Use https://editor2.swagger.io/ to convert from yaml to json-
|
||||||
public partial class MatcherGithubBenchmark : EndpointRoutingBenchmarkBase
|
public class MatcherGithubBenchmark : MatcherGithubBenchmarkBase
|
||||||
{
|
{
|
||||||
private BarebonesMatcher _baseline;
|
private BarebonesMatcher _baseline;
|
||||||
private Matcher _dfa;
|
private Matcher _dfa;
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,11 @@ using Microsoft.AspNetCore.Http;
|
||||||
namespace Microsoft.AspNetCore.Routing.Matching
|
namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
{
|
{
|
||||||
// This code was generated by the Swaggatherer
|
// 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 = new RouteEndpoint[243];
|
||||||
Endpoints[0] = CreateEndpoint("/emojis", "GET");
|
Endpoints[0] = CreateEndpoint("/emojis", "GET");
|
||||||
|
|
@ -259,7 +259,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
Endpoints[242] = CreateEndpoint("/repos/{owner}/{repo}/{archive_format}/{path}", "GET");
|
Endpoints[242] = CreateEndpoint("/repos/{owner}/{repo}/{archive_format}/{path}", "GET");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetupRequests()
|
private protected void SetupRequests()
|
||||||
{
|
{
|
||||||
Requests = new HttpContext[243];
|
Requests = new HttpContext[243];
|
||||||
Requests[0] = new DefaultHttpContext();
|
Requests[0] = new DefaultHttpContext();
|
||||||
|
|
@ -1236,7 +1236,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
Requests[242].Request.Path = "/repos/21a74/f36c8/805b0492-b723-/680ad";
|
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[0]);
|
||||||
builder.AddEndpoint(Endpoints[1]);
|
builder.AddEndpoint(Endpoints[1]);
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -47,7 +47,7 @@ namespace Microsoft.AspNetCore.Routing.Internal
|
||||||
// Assign each node a sequential index.
|
// Assign each node a sequential index.
|
||||||
var visited = new Dictionary<DfaNode, int>();
|
var visited = new Dictionary<DfaNode, int>();
|
||||||
|
|
||||||
var tree = builder.BuildDfaTree();
|
var tree = builder.BuildDfaTree(includeLabel: true);
|
||||||
|
|
||||||
writer.WriteLine("digraph DFA {");
|
writer.WriteLine("digraph DFA {");
|
||||||
tree.Visit(WriteNode);
|
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,
|
// 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.
|
// 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)
|
if (node.Parameters != null)
|
||||||
|
|
@ -79,9 +82,12 @@ namespace Microsoft.AspNetCore.Routing.Internal
|
||||||
writer.WriteLine($"{label} -> {visited[node.CatchAll]} [label=\"/**\"]");
|
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}\"]");
|
writer.WriteLine($"{label} [label=\"{node.Label}\"]");
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,15 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
private readonly INodeBuilderPolicy[] _nodeBuilders;
|
private readonly INodeBuilderPolicy[] _nodeBuilders;
|
||||||
private readonly EndpointComparer _comparer;
|
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(
|
public DfaMatcherBuilder(
|
||||||
ParameterPolicyFactory parameterPolicyFactory,
|
ParameterPolicyFactory parameterPolicyFactory,
|
||||||
EndpointSelector selector,
|
EndpointSelector selector,
|
||||||
|
|
@ -31,6 +40,12 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
// Taking care to use _policies, which has been sorted.
|
// Taking care to use _policies, which has been sorted.
|
||||||
_nodeBuilders = _policies.OfType<INodeBuilderPolicy>().ToArray();
|
_nodeBuilders = _policies.OfType<INodeBuilderPolicy>().ToArray();
|
||||||
_comparer = new EndpointComparer(_policies.OfType<IEndpointComparerPolicy>().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)
|
public override void AddEndpoint(RouteEndpoint endpoint)
|
||||||
|
|
@ -38,7 +53,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
_endpoints.Add(endpoint);
|
_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
|
// 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
|
// 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
|
// 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
|
// this list will hold the set of items we need to process at the current
|
||||||
// stage.
|
// 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
|
// To prepare for this we need to compute the max depth, as well as
|
||||||
// a seed list of items to process (entry, root).
|
// 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, }));
|
work.Add((endpoint, new List<DfaNode>() { root, }));
|
||||||
}
|
}
|
||||||
|
var workCount = work.Count;
|
||||||
|
|
||||||
// Now we process the entries a level at a time.
|
// Now we process the entries a level at a time.
|
||||||
for (var depth = 0; depth <= maxDepth; depth++)
|
for (var depth = 0; depth <= maxDepth; depth++)
|
||||||
{
|
{
|
||||||
// As we process items, collect the next set of items.
|
// 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];
|
var (endpoint, parents) = work[i];
|
||||||
|
|
||||||
|
|
@ -79,12 +107,28 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
for (var j = 0; j < parents.Count; j++)
|
for (var j = 0; j < parents.Count; j++)
|
||||||
{
|
{
|
||||||
var parent = parents[j];
|
var parent = parents[j];
|
||||||
parent.Matches.Add(endpoint);
|
parent.AddMatch(endpoint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the parents of this edge at the current depth
|
// 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);
|
var segment = GetCurrentSegment(endpoint, depth);
|
||||||
if (segment == null)
|
if (segment == null)
|
||||||
{
|
{
|
||||||
|
|
@ -97,15 +141,17 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
var part = segment.Parts[0];
|
var part = segment.Parts[0];
|
||||||
if (segment.IsSimple && part is RoutePatternLiteralPart literalPart)
|
if (segment.IsSimple && part is RoutePatternLiteralPart literalPart)
|
||||||
{
|
{
|
||||||
|
DfaNode next = null;
|
||||||
var literal = literalPart.Content;
|
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()
|
next = new DfaNode()
|
||||||
{
|
{
|
||||||
PathDepth = parent.PathDepth + 1,
|
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);
|
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
|
// 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
|
||||||
// all catchalls will be processed after all parameters.
|
// 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)
|
if (parent.Parameters != null)
|
||||||
{
|
{
|
||||||
nextParents.Add(parent.Parameters);
|
nextParents.Add(parent.Parameters);
|
||||||
|
|
@ -131,7 +180,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
parent.CatchAll = new DfaNode()
|
parent.CatchAll = new DfaNode()
|
||||||
{
|
{
|
||||||
PathDepth = parent.PathDepth + 1,
|
PathDepth = parent.PathDepth + 1,
|
||||||
Label = parent.Label + "{*...}/",
|
Label = includeLabel ? parent.Label + "{*...}/" : null,
|
||||||
};
|
};
|
||||||
|
|
||||||
// The catchall node just loops.
|
// The catchall node just loops.
|
||||||
|
|
@ -139,7 +188,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
parent.CatchAll.CatchAll = parent.CatchAll;
|
parent.CatchAll.CatchAll = parent.CatchAll;
|
||||||
}
|
}
|
||||||
|
|
||||||
parent.CatchAll.Matches.Add(endpoint);
|
parent.CatchAll.AddMatch(endpoint);
|
||||||
}
|
}
|
||||||
else if (segment.IsSimple && part.IsParameter)
|
else if (segment.IsSimple && part.IsParameter)
|
||||||
{
|
{
|
||||||
|
|
@ -148,12 +197,15 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
parent.Parameters = new DfaNode()
|
parent.Parameters = new DfaNode()
|
||||||
{
|
{
|
||||||
PathDepth = parent.PathDepth + 1,
|
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
|
// 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);
|
nextParents.Add(parent.Parameters);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|
@ -167,23 +219,28 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
parent.Parameters = new DfaNode()
|
parent.Parameters = new DfaNode()
|
||||||
{
|
{
|
||||||
PathDepth = parent.PathDepth + 1,
|
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);
|
nextParents.Add(parent.Parameters);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nextParents.Count > 0)
|
if (nextParents.Count > 0)
|
||||||
{
|
{
|
||||||
nextWork.Add((endpoint, nextParents));
|
nextWorkCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare the process the next stage.
|
// Prepare the process the next stage.
|
||||||
|
previousWork = work;
|
||||||
work = nextWork;
|
work = nextWork;
|
||||||
|
workCount = nextWorkCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build the trees of policy nodes (like HTTP methods). Post-order traversal
|
// 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()
|
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;
|
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 max segment count is the maximum path-node-depth +1. We need
|
||||||
// the +1 to capture any additional content after the 'last' segment.
|
// the +1 to capture any additional content after the 'last' segment.
|
||||||
maxSegmentCount++;
|
maxSegmentCount++;
|
||||||
|
|
||||||
var states = new List<DfaState>();
|
var states = new DfaState[stateCount];
|
||||||
var tableBuilders = new List<(JumpTableBuilder pathBuilder, PolicyJumpTableBuilder policyBuilder)>();
|
var exitDestination = stateCount - 1;
|
||||||
AddNode(root, states, tableBuilders);
|
AddNode(root, states, exitDestination);
|
||||||
|
|
||||||
var exit = states.Count;
|
states[exitDestination] = new DfaState(
|
||||||
states.Add(new DfaState(Array.Empty<Candidate>(), null, null));
|
Array.Empty<Candidate>(),
|
||||||
tableBuilders.Add((new JumpTableBuilder() { DefaultDestination = exit, ExitDestination = exit, }, null));
|
JumpTableBuilder.Build(exitDestination, exitDestination, null),
|
||||||
|
null);
|
||||||
|
|
||||||
for (var i = 0; i < tableBuilders.Count; i++)
|
return new DfaMatcher(_selector, states, maxSegmentCount);
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private int AddNode(
|
private int AddNode(
|
||||||
DfaNode node,
|
DfaNode node,
|
||||||
List<DfaState> states,
|
DfaState[] states,
|
||||||
List<(JumpTableBuilder pathBuilder, PolicyJumpTableBuilder policyBuilder)> tableBuilders)
|
int exitDestination)
|
||||||
{
|
{
|
||||||
node.Matches.Sort(_comparer);
|
node.Matches?.Sort(_comparer);
|
||||||
|
|
||||||
var stateIndex = states.Count;
|
var currentStateIndex = _stateIndex;
|
||||||
|
|
||||||
var candidates = CreateCandidates(node.Matches);
|
var currentDefaultDestination = exitDestination;
|
||||||
states.Add(new DfaState(candidates, null, null));
|
var currentExitDestination = exitDestination;
|
||||||
|
(string text, int destination)[] pathEntries = null;
|
||||||
|
PolicyJumpTableEdge[] policyEntries = null;
|
||||||
|
|
||||||
var pathBuilder = new JumpTableBuilder();
|
if (node.Literals != null)
|
||||||
tableBuilders.Add((pathBuilder, null));
|
|
||||||
|
|
||||||
foreach (var kvp in node.Literals)
|
|
||||||
{
|
{
|
||||||
if (kvp.Key == null)
|
pathEntries = new (string text, int destination)[node.Literals.Count];
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var transition = Transition(kvp.Value);
|
var index = 0;
|
||||||
pathBuilder.AddEntry(kvp.Key, transition);
|
foreach (var kvp in node.Literals)
|
||||||
|
{
|
||||||
|
var transition = Transition(kvp.Value);
|
||||||
|
pathEntries[index++] = (kvp.Key, transition);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node.Parameters != null &&
|
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 node has a single transition to but it should accept zero-width segments
|
||||||
// this can happen when a node only has catchall parameters.
|
// this can happen when a node only has catchall parameters.
|
||||||
pathBuilder.DefaultDestination = Transition(node.Parameters);
|
currentExitDestination = currentDefaultDestination = Transition(node.Parameters);
|
||||||
pathBuilder.ExitDestination = pathBuilder.DefaultDestination;
|
|
||||||
}
|
}
|
||||||
else if (node.Parameters != null && node.CatchAll != null)
|
else if (node.Parameters != null && node.CatchAll != null)
|
||||||
{
|
{
|
||||||
// This node has a separate transition for zero-width segments
|
// This node has a separate transition for zero-width segments
|
||||||
// this can happen when a node has both parameters and catchall parameters.
|
// this can happen when a node has both parameters and catchall parameters.
|
||||||
pathBuilder.DefaultDestination = Transition(node.Parameters);
|
currentDefaultDestination = Transition(node.Parameters);
|
||||||
pathBuilder.ExitDestination = Transition(node.CatchAll);
|
currentExitDestination = Transition(node.CatchAll);
|
||||||
}
|
}
|
||||||
else if (node.Parameters != null)
|
else if (node.Parameters != null)
|
||||||
{
|
{
|
||||||
// This node has paramters but no catchall.
|
// This node has paramters but no catchall.
|
||||||
pathBuilder.DefaultDestination = Transition(node.Parameters);
|
currentDefaultDestination = Transition(node.Parameters);
|
||||||
}
|
}
|
||||||
else if (node.CatchAll != null)
|
else if (node.CatchAll != null)
|
||||||
{
|
{
|
||||||
// This node has a catchall but no parameters
|
// This node has a catchall but no parameters
|
||||||
pathBuilder.DefaultDestination = Transition(node.CatchAll);
|
currentExitDestination = currentDefaultDestination = Transition(node.CatchAll);
|
||||||
pathBuilder.ExitDestination = pathBuilder.DefaultDestination;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node.PolicyEdges.Count > 0)
|
if (node.PolicyEdges != null && node.PolicyEdges.Count > 0)
|
||||||
{
|
{
|
||||||
var policyBuilder = new PolicyJumpTableBuilder(node.NodeBuilder);
|
policyEntries = new PolicyJumpTableEdge[node.PolicyEdges.Count];
|
||||||
tableBuilders[stateIndex] = (pathBuilder, policyBuilder);
|
|
||||||
|
|
||||||
|
var index = 0;
|
||||||
foreach (var kvp in node.PolicyEdges)
|
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)
|
int Transition(DfaNode next)
|
||||||
{
|
{
|
||||||
// Break cycles
|
// 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
|
// Builds an array of candidates for a node, assigns a 'score' for each
|
||||||
// endpoint.
|
// endpoint.
|
||||||
internal Candidate[] CreateCandidates(IReadOnlyList<Endpoint> endpoints)
|
internal Candidate[] CreateCandidates(IReadOnlyList<Endpoint> endpoints)
|
||||||
{
|
{
|
||||||
if (endpoints.Count == 0)
|
if (endpoints == null || endpoints.Count == 0)
|
||||||
{
|
{
|
||||||
return Array.Empty<Candidate>();
|
return Array.Empty<Candidate>();
|
||||||
}
|
}
|
||||||
|
|
@ -370,20 +437,20 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
// internal for tests
|
// internal for tests
|
||||||
internal Candidate CreateCandidate(Endpoint endpoint, int score)
|
internal Candidate CreateCandidate(Endpoint endpoint, int score)
|
||||||
{
|
{
|
||||||
var assignments = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
_assignments.Clear();
|
||||||
var slots = new List<KeyValuePair<string, object>>();
|
_slots.Clear();
|
||||||
var captures = new List<(string parameterName, int segmentIndex, int slotIndex)>();
|
_captures.Clear();
|
||||||
(string parameterName, int segmentIndex, int slotIndex) catchAll = default;
|
_complexSegments.Clear();
|
||||||
|
_constraints.Clear();
|
||||||
|
|
||||||
var complexSegments = new List<(RoutePatternPathSegment pathSegment, int segmentIndex)>();
|
(string parameterName, int segmentIndex, int slotIndex) catchAll = default;
|
||||||
var constraints = new List<KeyValuePair<string, IRouteConstraint>>();
|
|
||||||
|
|
||||||
if (endpoint is RouteEndpoint routeEndpoint)
|
if (endpoint is RouteEndpoint routeEndpoint)
|
||||||
{
|
{
|
||||||
foreach (var kvp in routeEndpoint.RoutePattern.Defaults)
|
foreach (var kvp in routeEndpoint.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 < routeEndpoint.RoutePattern.PathSegments.Count; i++)
|
for (var i = 0; i < routeEndpoint.RoutePattern.PathSegments.Count; i++)
|
||||||
|
|
@ -400,13 +467,13 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!assignments.TryGetValue(parameterPart.Name, out var slotIndex))
|
if (!_assignments.TryGetValue(parameterPart.Name, out var slotIndex))
|
||||||
{
|
{
|
||||||
slotIndex = assignments.Count;
|
slotIndex = _assignments.Count;
|
||||||
assignments.Add(parameterPart.Name, slotIndex);
|
_assignments.Add(parameterPart.Name, slotIndex);
|
||||||
|
|
||||||
var hasDefaultValue = parameterPart.Default != null || parameterPart.IsCatchAll;
|
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)
|
if (parameterPart.IsCatchAll)
|
||||||
|
|
@ -415,7 +482,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
captures.Add((parameterPart.Name, i, slotIndex));
|
_captures.Add((parameterPart.Name, i, slotIndex));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -427,7 +494,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
complexSegments.Add((segment, i));
|
_complexSegments.Add((segment, i));
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var kvp in routeEndpoint.RoutePattern.ParameterPolicies)
|
foreach (var kvp in routeEndpoint.RoutePattern.ParameterPolicies)
|
||||||
|
|
@ -440,7 +507,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
var parameterPolicy = _parameterPolicyFactory.Create(parameter, reference);
|
var parameterPolicy = _parameterPolicyFactory.Create(parameter, reference);
|
||||||
if (parameterPolicy is IRouteConstraint routeConstraint)
|
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(
|
return new Candidate(
|
||||||
endpoint,
|
endpoint,
|
||||||
score,
|
score,
|
||||||
slots.ToArray(),
|
_slots.ToArray(),
|
||||||
captures.ToArray(),
|
_captures.ToArray(),
|
||||||
catchAll,
|
catchAll,
|
||||||
complexSegments.ToArray(),
|
_complexSegments.ToArray(),
|
||||||
constraints.ToArray());
|
_constraints.ToArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
private int[] GetGroupLengths(DfaNode node)
|
private int[] GetGroupLengths(DfaNode node)
|
||||||
{
|
{
|
||||||
if (node.Matches.Count == 0)
|
var nodeMatches = node.Matches;
|
||||||
|
if (nodeMatches == null || nodeMatches.Count == 0)
|
||||||
{
|
{
|
||||||
return Array.Empty<int>();
|
return Array.Empty<int>();
|
||||||
}
|
}
|
||||||
|
|
@ -466,16 +534,16 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
var groups = new List<int>();
|
var groups = new List<int>();
|
||||||
|
|
||||||
var length = 1;
|
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);
|
groups.Add(length);
|
||||||
length = 0;
|
length = 0;
|
||||||
|
|
||||||
exemplar = node.Matches[i];
|
exemplar = nodeMatches[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
length++;
|
length++;
|
||||||
|
|
@ -517,24 +585,35 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
|
|
||||||
private void ApplyPolicies(DfaNode node)
|
private void ApplyPolicies(DfaNode node)
|
||||||
{
|
{
|
||||||
if (node.Matches.Count == 0)
|
if (node.Matches == null || node.Matches.Count == 0)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start with the current node as the root.
|
// Start with the current node as the root.
|
||||||
var work = new List<DfaNode>() { node, };
|
var work = new List<DfaNode>() { node, };
|
||||||
|
List<DfaNode> previousWork = null;
|
||||||
for (var i = 0; i < _nodeBuilders.Length; i++)
|
for (var i = 0; i < _nodeBuilders.Length; i++)
|
||||||
{
|
{
|
||||||
var nodeBuilder = _nodeBuilders[i];
|
var nodeBuilder = _nodeBuilders[i];
|
||||||
|
|
||||||
// Build a list of each
|
// 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++)
|
for (var j = 0; j < work.Count; j++)
|
||||||
{
|
{
|
||||||
var parent = work[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
|
// This node-builder doesn't care about this node, so add it to the list
|
||||||
// to be processed by the next node-builder.
|
// 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,
|
// 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.
|
// 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++)
|
for (var k = 0; k < edges.Count; k++)
|
||||||
{
|
{
|
||||||
var edge = edges[k];
|
var edge = edges[k];
|
||||||
|
|
||||||
var next = new DfaNode()
|
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);
|
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.
|
// Associate the node-builder so we can build a jump table later.
|
||||||
parent.NodeBuilder = nodeBuilder;
|
parent.NodeBuilder = nodeBuilder;
|
||||||
|
|
||||||
// The parent no longer has matches, it's not considered a terminal node.
|
// The parent no longer has matches, it's not considered a terminal node.
|
||||||
parent.Matches.Clear();
|
parent.Matches?.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
previousWork = work;
|
||||||
work = nextWork;
|
work = nextWork;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,13 +14,6 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
[DebuggerDisplay("{DebuggerToString(),nq}")]
|
[DebuggerDisplay("{DebuggerToString(),nq}")]
|
||||||
internal class DfaNode
|
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
|
// The depth of the node. The depth indicates the number of segments
|
||||||
// that must be processed to arrive at this node.
|
// that must be processed to arrive at this node.
|
||||||
//
|
//
|
||||||
|
|
@ -30,9 +23,9 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
// Just for diagnostics and debugging
|
// Just for diagnostics and debugging
|
||||||
public string Label { get; set; }
|
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; }
|
public DfaNode Parameters { get; set; }
|
||||||
|
|
||||||
|
|
@ -40,13 +33,58 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
|
|
||||||
public INodeBuilderPolicy NodeBuilder { get; set; }
|
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)
|
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
|
// Break cycles
|
||||||
|
|
@ -61,9 +99,12 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
CatchAll.Visit(visitor);
|
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);
|
visitor(this);
|
||||||
|
|
@ -76,9 +117,12 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
builder.Append(" d:");
|
builder.Append(" d:");
|
||||||
builder.Append(PathDepth);
|
builder.Append(PathDepth);
|
||||||
builder.Append(" m:");
|
builder.Append(" m:");
|
||||||
builder.Append(Matches.Count);
|
builder.Append(Matches?.Count ?? 0);
|
||||||
builder.Append(" c: ");
|
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();
|
return builder.ToString();
|
||||||
|
|
||||||
// DfaNodes can be self-referential, don't traverse cycles.
|
// DfaNodes can be self-referential, don't traverse cycles.
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
|
|
||||||
for (var i = 0; i < endpoints.Count; i++)
|
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;
|
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
|
// 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.
|
// 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>>();
|
var edges = new Dictionary<EdgeKey, List<Endpoint>>();
|
||||||
for (var i = 0; i < endpoints.Count; i++)
|
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.
|
// Also if it's not the *any* method key, then track it.
|
||||||
if (!string.Equals(AnyMethod, httpMethod, StringComparison.OrdinalIgnoreCase))
|
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
|
// 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.
|
// the states, so we want to see which states this endpoint matches.
|
||||||
for (var i = 0; i < endpoints.Count; i++)
|
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))
|
if (!edges.TryGetValue(new EdgeKey(AnyMethod, false), out var matches))
|
||||||
{
|
{
|
||||||
// Methods sorted for testability.
|
// Methods sorted for testability.
|
||||||
var endpoint = CreateRejectionEndpoint(allHttpMethods.OrderBy(m => m));
|
var endpoint = CreateRejectionEndpoint(allHttpMethods);
|
||||||
matches = new List<Endpoint>() { endpoint, };
|
matches = new List<Endpoint>() { endpoint, };
|
||||||
edges[new EdgeKey(AnyMethod, false)] = matches;
|
edges[new EdgeKey(AnyMethod, false)] = matches;
|
||||||
}
|
}
|
||||||
|
|
||||||
return edges
|
var policyNodeEdges = new PolicyNodeEdge[edges.Count];
|
||||||
.Select(kvp => new PolicyNodeEdge(kvp.Key, kvp.Value))
|
var index = 0;
|
||||||
.ToArray();
|
foreach (var kvp in edges)
|
||||||
|
{
|
||||||
|
policyNodeEdges[index++] = new PolicyNodeEdge(kvp.Key, kvp.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return policyNodeEdges;
|
||||||
|
|
||||||
(IReadOnlyList<string> httpMethods, bool acceptCorsPreflight) GetHttpMethods(Endpoint e)
|
(IReadOnlyList<string> httpMethods, bool acceptCorsPreflight) GetHttpMethods(Endpoint e)
|
||||||
{
|
{
|
||||||
|
|
@ -209,31 +219,41 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public PolicyJumpTable BuildJumpTable(int exitDestination, IReadOnlyList<PolicyJumpTableEdge> edges)
|
public PolicyJumpTable BuildJumpTable(int exitDestination, IReadOnlyList<PolicyJumpTableEdge> edges)
|
||||||
{
|
{
|
||||||
var destinations = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
Dictionary<string, int> destinations = null;
|
||||||
var corsPreflightDestinations = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
Dictionary<string, int> corsPreflightDestinations = null;
|
||||||
for (var i = 0; i < edges.Count; i++)
|
for (var i = 0; i < edges.Count; i++)
|
||||||
{
|
{
|
||||||
// We create this data, so it's safe to cast it.
|
// We create this data, so it's safe to cast it.
|
||||||
var key = (EdgeKey)edges[i].State;
|
var key = (EdgeKey)edges[i].State;
|
||||||
if (key.IsCorsPreflightRequest)
|
if (key.IsCorsPreflightRequest)
|
||||||
{
|
{
|
||||||
|
if (corsPreflightDestinations == null)
|
||||||
|
{
|
||||||
|
corsPreflightDestinations = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
corsPreflightDestinations.Add(key.HttpMethod, edges[i].Destination);
|
corsPreflightDestinations.Add(key.HttpMethod, edges[i].Destination);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
if (destinations == null)
|
||||||
|
{
|
||||||
|
destinations = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
destinations.Add(key.HttpMethod, edges[i].Destination);
|
destinations.Add(key.HttpMethod, edges[i].Destination);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int corsPreflightExitDestination = exitDestination;
|
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.
|
// If we have endpoints that match any HTTP method, use that as the exit.
|
||||||
corsPreflightExitDestination = matchesAnyVerb;
|
corsPreflightExitDestination = matchesAnyVerb;
|
||||||
corsPreflightDestinations.Remove(AnyMethod);
|
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.
|
// If we have endpoints that match any HTTP method, use that as the exit.
|
||||||
exitDestination = matchesAnyVerb;
|
exitDestination = matchesAnyVerb;
|
||||||
|
|
@ -261,6 +281,19 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
Http405EndpointDisplayName);
|
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 class HttpMethodPolicyJumpTable : PolicyJumpTable
|
||||||
{
|
{
|
||||||
private readonly int _exitDestination;
|
private readonly int _exitDestination;
|
||||||
|
|
@ -281,7 +314,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
_corsPreflightExitDestination = corsPreflightExitDestination;
|
_corsPreflightExitDestination = corsPreflightExitDestination;
|
||||||
_corsPreflightDestinations = corsPreflightDestinations;
|
_corsPreflightDestinations = corsPreflightDestinations;
|
||||||
|
|
||||||
_supportsCorsPreflight = _corsPreflightDestinations.Count > 0;
|
_supportsCorsPreflight = _corsPreflightDestinations != null && _corsPreflightDestinations.Count > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override int GetDestination(HttpContext httpContext)
|
public override int GetDestination(HttpContext httpContext)
|
||||||
|
|
@ -295,12 +328,14 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
httpContext.Request.Headers.TryGetValue(AccessControlRequestMethod, out var accessControlRequestMethod) &&
|
httpContext.Request.Headers.TryGetValue(AccessControlRequestMethod, out var accessControlRequestMethod) &&
|
||||||
!StringValues.IsNullOrEmpty(accessControlRequestMethod))
|
!StringValues.IsNullOrEmpty(accessControlRequestMethod))
|
||||||
{
|
{
|
||||||
return _corsPreflightDestinations.TryGetValue(accessControlRequestMethod, out destination)
|
return _corsPreflightDestinations != null &&
|
||||||
|
_corsPreflightDestinations.TryGetValue(accessControlRequestMethod, out destination)
|
||||||
? destination
|
? destination
|
||||||
: _corsPreflightExitDestination;
|
: _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()
|
public override int GetHashCode()
|
||||||
{
|
{
|
||||||
var hash = new HashCodeCombiner();
|
var hash = new HashCodeCombiner();
|
||||||
hash.Add(IsCorsPreflightRequest);
|
hash.Add(IsCorsPreflightRequest ? 1 : 0);
|
||||||
hash.Add(HttpMethod, StringComparer.Ordinal);
|
hash.Add(HttpMethod, StringComparer.Ordinal);
|
||||||
return hash;
|
return hash;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,39 +2,24 @@
|
||||||
// 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.Collections.Generic;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Routing.Matching
|
namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
{
|
{
|
||||||
internal class JumpTableBuilder
|
internal static class JumpTableBuilder
|
||||||
{
|
{
|
||||||
public static readonly int InvalidDestination = -1;
|
public static readonly int InvalidDestination = -1;
|
||||||
|
|
||||||
private readonly List<(string text, int destination)> _entries = new List<(string text, int destination)>();
|
public static JumpTable Build(int defaultDestination, int exitDestination, (string text, int destination)[] pathEntries)
|
||||||
|
|
||||||
// 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)
|
|
||||||
{
|
{
|
||||||
_entries.Add((text, destination));
|
if (defaultDestination == InvalidDestination)
|
||||||
}
|
|
||||||
|
|
||||||
public JumpTable Build()
|
|
||||||
{
|
|
||||||
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);
|
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);
|
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
|
// We have an optimized fast path for zero entries since we don't have to
|
||||||
// do any string comparisons.
|
// 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
|
// The IL Emit jump table is not faster for a single entry
|
||||||
if (_entries.Count == 1)
|
if (pathEntries.Length == 1)
|
||||||
{
|
{
|
||||||
var entry = _entries[0];
|
var entry = pathEntries[0];
|
||||||
return new SingleEntryJumpTable(DefaultDestination, ExitDestination, entry.text, entry.destination);
|
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
|
// 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
|
// Additionally if we're on 32bit, the scalability is worse, so switch to the dictionary at 50
|
||||||
// entries.
|
// entries.
|
||||||
var threshold = IntPtr.Size == 8 ? 100 : 50;
|
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
|
// 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;
|
JumpTable fallback;
|
||||||
|
|
||||||
// Based on our testing a linear search is still faster than a dictionary at ten entries.
|
// 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
|
else
|
||||||
{
|
{
|
||||||
fallback = new DictionaryJumpTable(DefaultDestination, ExitDestination, _entries.ToArray());
|
fallback = new DictionaryJumpTable(defaultDestination, exitDestination, pathEntries);
|
||||||
}
|
}
|
||||||
|
|
||||||
#if IL_EMIT
|
#if IL_EMIT
|
||||||
|
return new ILEmitTrieJumpTable(defaultDestination, exitDestination, pathEntries, vectorize: null, fallback);
|
||||||
return new ILEmitTrieJumpTable(DefaultDestination, ExitDestination, _entries.ToArray(), vectorize: null, fallback);
|
|
||||||
#else
|
#else
|
||||||
return fallback;
|
return fallback;
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -2,7 +2,9 @@
|
||||||
// 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.Collections;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Microsoft.AspNetCore.Routing.Constraints;
|
using Microsoft.AspNetCore.Routing.Constraints;
|
||||||
using Microsoft.AspNetCore.Routing.Matching;
|
using Microsoft.AspNetCore.Routing.Matching;
|
||||||
|
|
@ -16,6 +18,12 @@ namespace Microsoft.AspNetCore.Routing.Patterns
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class RoutePatternFactory
|
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>
|
/// <summary>
|
||||||
/// Creates a <see cref="RoutePattern"/> from its string representation.
|
/// Creates a <see cref="RoutePattern"/> from its string representation.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -242,8 +250,8 @@ namespace Microsoft.AspNetCore.Routing.Patterns
|
||||||
|
|
||||||
private static RoutePattern PatternCore(
|
private static RoutePattern PatternCore(
|
||||||
string rawText,
|
string rawText,
|
||||||
IDictionary<string, object> defaults,
|
RouteValueDictionary defaults,
|
||||||
IDictionary<string, object> parameterPolicies,
|
RouteValueDictionary parameterPolicies,
|
||||||
IEnumerable<RoutePatternPathSegment> segments)
|
IEnumerable<RoutePatternPathSegment> segments)
|
||||||
{
|
{
|
||||||
// We want to merge the segment data with the 'out of line' defaults and parameter policies.
|
// 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
|
// 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.
|
// values specified out of line to have a different behavior.
|
||||||
|
|
||||||
var updatedDefaults = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
Dictionary<string, object> updatedDefaults = null;
|
||||||
if (defaults != null)
|
if (defaults != null && defaults.Count > 0)
|
||||||
{
|
{
|
||||||
|
updatedDefaults = new Dictionary<string, object>(defaults.Count, StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
foreach (var kvp in defaults)
|
foreach (var kvp in defaults)
|
||||||
{
|
{
|
||||||
updatedDefaults.Add(kvp.Key, kvp.Value);
|
updatedDefaults.Add(kvp.Key, kvp.Value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var updatedParameterPolicies = new Dictionary<string, List<RoutePatternParameterPolicyReference>>(StringComparer.OrdinalIgnoreCase);
|
Dictionary<string, List<RoutePatternParameterPolicyReference>> updatedParameterPolicies = null;
|
||||||
if (parameterPolicies != null)
|
if (parameterPolicies != null && parameterPolicies.Count > 0)
|
||||||
{
|
{
|
||||||
|
updatedParameterPolicies = new Dictionary<string, List<RoutePatternParameterPolicyReference>>(parameterPolicies.Count, StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
foreach (var kvp in parameterPolicies)
|
foreach (var kvp in parameterPolicies)
|
||||||
{
|
{
|
||||||
updatedParameterPolicies.Add(kvp.Key, new List<RoutePatternParameterPolicyReference>()
|
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();
|
var updatedSegments = segments.ToArray();
|
||||||
for (var i = 0; i < updatedSegments.Length; i++)
|
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 (segment.Parts[j] is RoutePatternParameterPart parameter)
|
||||||
{
|
{
|
||||||
|
if (parameters == null)
|
||||||
|
{
|
||||||
|
parameters = new List<RoutePatternParameterPart>();
|
||||||
|
}
|
||||||
|
|
||||||
parameters.Add(parameter);
|
parameters.Add(parameter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -298,9 +315,11 @@ namespace Microsoft.AspNetCore.Routing.Patterns
|
||||||
|
|
||||||
return new RoutePattern(
|
return new RoutePattern(
|
||||||
rawText,
|
rawText,
|
||||||
updatedDefaults,
|
updatedDefaults ?? EmptyDefaultsDictionary,
|
||||||
updatedParameterPolicies.ToDictionary(kvp => kvp.Key, kvp => (IReadOnlyList<RoutePatternParameterPolicyReference>)kvp.Value.ToArray()),
|
updatedParameterPolicies != null
|
||||||
parameters,
|
? updatedParameterPolicies.ToDictionary(kvp => kvp.Key, kvp => (IReadOnlyList<RoutePatternParameterPolicyReference>)kvp.Value.ToArray())
|
||||||
|
: EmptyPoliciesDictionary,
|
||||||
|
(IReadOnlyList<RoutePatternParameterPart>)parameters ?? Array.Empty<RoutePatternParameterPart>(),
|
||||||
updatedSegments);
|
updatedSegments);
|
||||||
|
|
||||||
RoutePatternPathSegment VisitSegment(RoutePatternPathSegment segment)
|
RoutePatternPathSegment VisitSegment(RoutePatternPathSegment segment)
|
||||||
|
|
@ -341,7 +360,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns
|
||||||
var parameter = (RoutePatternParameterPart)part;
|
var parameter = (RoutePatternParameterPart)part;
|
||||||
var @default = parameter.Default;
|
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))
|
if (parameter.Default != null && !Equals(newDefault, parameter.Default))
|
||||||
{
|
{
|
||||||
|
|
@ -360,12 +379,23 @@ namespace Microsoft.AspNetCore.Routing.Patterns
|
||||||
|
|
||||||
if (parameter.Default != null)
|
if (parameter.Default != null)
|
||||||
{
|
{
|
||||||
|
if (updatedDefaults == null)
|
||||||
|
{
|
||||||
|
updatedDefaults = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
updatedDefaults[parameter.Name] = parameter.Default;
|
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)
|
parameter.ParameterPolicies.Count > 0)
|
||||||
{
|
{
|
||||||
|
if (updatedParameterPolicies == null)
|
||||||
|
{
|
||||||
|
updatedParameterPolicies = new Dictionary<string, List<RoutePatternParameterPolicyReference>>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
parameterConstraints = new List<RoutePatternParameterPolicyReference>();
|
parameterConstraints = new List<RoutePatternParameterPolicyReference>();
|
||||||
updatedParameterPolicies.Add(parameter.Name, parameterConstraints);
|
updatedParameterPolicies.Add(parameter.Name, parameterConstraints);
|
||||||
}
|
}
|
||||||
|
|
@ -391,6 +421,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns
|
||||||
parameter.EncodeSlashes);
|
parameter.EncodeSlashes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a <see cref="RoutePatternPathSegment"/> from the provided collection
|
/// Creates a <see cref="RoutePatternPathSegment"/> from the provided collection
|
||||||
/// of parts.
|
/// of parts.
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Same(endpoint, Assert.Single(root.Matches));
|
Assert.Same(endpoint, Assert.Single(root.Matches));
|
||||||
Assert.Null(root.Parameters);
|
Assert.Null(root.Parameters);
|
||||||
Assert.Empty(root.Literals);
|
Assert.Null(root.Literals);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -46,21 +46,21 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
var root = builder.BuildDfaTree();
|
var root = builder.BuildDfaTree();
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Empty(root.Matches);
|
Assert.Null(root.Matches);
|
||||||
Assert.Null(root.Parameters);
|
Assert.Null(root.Parameters);
|
||||||
|
|
||||||
var next = Assert.Single(root.Literals);
|
var next = Assert.Single(root.Literals);
|
||||||
Assert.Equal("a", next.Key);
|
Assert.Equal("a", next.Key);
|
||||||
|
|
||||||
var a = next.Value;
|
var a = next.Value;
|
||||||
Assert.Empty(a.Matches);
|
Assert.Null(a.Matches);
|
||||||
Assert.Null(a.Parameters);
|
Assert.Null(a.Parameters);
|
||||||
|
|
||||||
next = Assert.Single(a.Literals);
|
next = Assert.Single(a.Literals);
|
||||||
Assert.Equal("b", next.Key);
|
Assert.Equal("b", next.Key);
|
||||||
|
|
||||||
var b = next.Value;
|
var b = next.Value;
|
||||||
Assert.Empty(b.Matches);
|
Assert.Null(b.Matches);
|
||||||
Assert.Null(b.Parameters);
|
Assert.Null(b.Parameters);
|
||||||
|
|
||||||
next = Assert.Single(b.Literals);
|
next = Assert.Single(b.Literals);
|
||||||
|
|
@ -69,7 +69,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
var c = next.Value;
|
var c = next.Value;
|
||||||
Assert.Same(endpoint, Assert.Single(c.Matches));
|
Assert.Same(endpoint, Assert.Single(c.Matches));
|
||||||
Assert.Null(c.Parameters);
|
Assert.Null(c.Parameters);
|
||||||
Assert.Empty(c.Literals);
|
Assert.Null(c.Literals);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -85,21 +85,21 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
var root = builder.BuildDfaTree();
|
var root = builder.BuildDfaTree();
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Empty(root.Matches);
|
Assert.Null(root.Matches);
|
||||||
Assert.Empty(root.Literals);
|
Assert.Null(root.Literals);
|
||||||
|
|
||||||
var a = root.Parameters;
|
var a = root.Parameters;
|
||||||
Assert.Empty(a.Matches);
|
Assert.Null(a.Matches);
|
||||||
Assert.Empty(a.Literals);
|
Assert.Null(a.Literals);
|
||||||
|
|
||||||
var b = a.Parameters;
|
var b = a.Parameters;
|
||||||
Assert.Empty(b.Matches);
|
Assert.Null(b.Matches);
|
||||||
Assert.Empty(b.Literals);
|
Assert.Null(b.Literals);
|
||||||
|
|
||||||
var c = b.Parameters;
|
var c = b.Parameters;
|
||||||
Assert.Same(endpoint, Assert.Single(c.Matches));
|
Assert.Same(endpoint, Assert.Single(c.Matches));
|
||||||
Assert.Null(c.Parameters);
|
Assert.Null(c.Parameters);
|
||||||
Assert.Empty(c.Literals);
|
Assert.Null(c.Literals);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -115,21 +115,21 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
var root = builder.BuildDfaTree();
|
var root = builder.BuildDfaTree();
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Empty(root.Matches);
|
Assert.Null(root.Matches);
|
||||||
Assert.Empty(root.Literals);
|
Assert.Null(root.Literals);
|
||||||
|
|
||||||
var a = root.Parameters;
|
var a = root.Parameters;
|
||||||
|
|
||||||
// The catch all can match a path like '/a'
|
// The catch all can match a path like '/a'
|
||||||
Assert.Same(endpoint, Assert.Single(a.Matches));
|
Assert.Same(endpoint, Assert.Single(a.Matches));
|
||||||
Assert.Empty(a.Literals);
|
Assert.Null(a.Literals);
|
||||||
Assert.Null(a.Parameters);
|
Assert.Null(a.Parameters);
|
||||||
|
|
||||||
// Catch-all nodes include an extra transition that loops to process
|
// Catch-all nodes include an extra transition that loops to process
|
||||||
// extra segments.
|
// extra segments.
|
||||||
var catchAll = a.CatchAll;
|
var catchAll = a.CatchAll;
|
||||||
Assert.Same(endpoint, Assert.Single(catchAll.Matches));
|
Assert.Same(endpoint, Assert.Single(catchAll.Matches));
|
||||||
Assert.Empty(catchAll.Literals);
|
Assert.Null(catchAll.Literals);
|
||||||
Assert.Same(catchAll, catchAll.Parameters);
|
Assert.Same(catchAll, catchAll.Parameters);
|
||||||
Assert.Same(catchAll, catchAll.CatchAll);
|
Assert.Same(catchAll, catchAll.CatchAll);
|
||||||
}
|
}
|
||||||
|
|
@ -148,13 +148,13 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Same(endpoint, Assert.Single(root.Matches));
|
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
|
// Catch-all nodes include an extra transition that loops to process
|
||||||
// extra segments.
|
// extra segments.
|
||||||
var catchAll = root.CatchAll;
|
var catchAll = root.CatchAll;
|
||||||
Assert.Same(endpoint, Assert.Single(catchAll.Matches));
|
Assert.Same(endpoint, Assert.Single(catchAll.Matches));
|
||||||
Assert.Empty(catchAll.Literals);
|
Assert.Null(catchAll.Literals);
|
||||||
Assert.Same(catchAll, catchAll.Parameters);
|
Assert.Same(catchAll, catchAll.Parameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -174,19 +174,19 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
var root = builder.BuildDfaTree();
|
var root = builder.BuildDfaTree();
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Empty(root.Matches);
|
Assert.Null(root.Matches);
|
||||||
Assert.Null(root.Parameters);
|
Assert.Null(root.Parameters);
|
||||||
|
|
||||||
var next = Assert.Single(root.Literals);
|
var next = Assert.Single(root.Literals);
|
||||||
Assert.Equal("a", next.Key);
|
Assert.Equal("a", next.Key);
|
||||||
|
|
||||||
var a = next.Value;
|
var a = next.Value;
|
||||||
Assert.Empty(a.Matches);
|
Assert.Null(a.Matches);
|
||||||
|
|
||||||
Assert.Equal(2, a.Literals.Count);
|
Assert.Equal(2, a.Literals.Count);
|
||||||
|
|
||||||
var b1 = a.Literals["b1"];
|
var b1 = a.Literals["b1"];
|
||||||
Assert.Empty(b1.Matches);
|
Assert.Null(b1.Matches);
|
||||||
Assert.Null(b1.Parameters);
|
Assert.Null(b1.Parameters);
|
||||||
|
|
||||||
next = Assert.Single(b1.Literals);
|
next = Assert.Single(b1.Literals);
|
||||||
|
|
@ -195,10 +195,10 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
var c1 = next.Value;
|
var c1 = next.Value;
|
||||||
Assert.Same(endpoint1, Assert.Single(c1.Matches));
|
Assert.Same(endpoint1, Assert.Single(c1.Matches));
|
||||||
Assert.Null(c1.Parameters);
|
Assert.Null(c1.Parameters);
|
||||||
Assert.Empty(c1.Literals);
|
Assert.Null(c1.Literals);
|
||||||
|
|
||||||
var b2 = a.Literals["b2"];
|
var b2 = a.Literals["b2"];
|
||||||
Assert.Empty(b2.Matches);
|
Assert.Null(b2.Matches);
|
||||||
Assert.Null(b2.Parameters);
|
Assert.Null(b2.Parameters);
|
||||||
|
|
||||||
next = Assert.Single(b2.Literals);
|
next = Assert.Single(b2.Literals);
|
||||||
|
|
@ -207,7 +207,59 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
var c2 = next.Value;
|
var c2 = next.Value;
|
||||||
Assert.Same(endpoint2, Assert.Single(c2.Matches));
|
Assert.Same(endpoint2, Assert.Single(c2.Matches));
|
||||||
Assert.Null(c2.Parameters);
|
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]
|
[Fact]
|
||||||
|
|
@ -226,20 +278,20 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
var root = builder.BuildDfaTree();
|
var root = builder.BuildDfaTree();
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Empty(root.Matches);
|
Assert.Null(root.Matches);
|
||||||
Assert.Null(root.Parameters);
|
Assert.Null(root.Parameters);
|
||||||
|
|
||||||
var next = Assert.Single(root.Literals);
|
var next = Assert.Single(root.Literals);
|
||||||
Assert.Equal("a", next.Key);
|
Assert.Equal("a", next.Key);
|
||||||
|
|
||||||
var a = next.Value;
|
var a = next.Value;
|
||||||
Assert.Empty(a.Matches);
|
Assert.Null(a.Matches);
|
||||||
|
|
||||||
next = Assert.Single(a.Literals);
|
next = Assert.Single(a.Literals);
|
||||||
Assert.Equal("b", next.Key);
|
Assert.Equal("b", next.Key);
|
||||||
|
|
||||||
var b = next.Value;
|
var b = next.Value;
|
||||||
Assert.Empty(b.Matches);
|
Assert.Null(b.Matches);
|
||||||
Assert.Null(b.Parameters);
|
Assert.Null(b.Parameters);
|
||||||
|
|
||||||
next = Assert.Single(b.Literals);
|
next = Assert.Single(b.Literals);
|
||||||
|
|
@ -251,10 +303,10 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
e => Assert.Same(endpoint1, e),
|
e => Assert.Same(endpoint1, e),
|
||||||
e => Assert.Same(endpoint2, e));
|
e => Assert.Same(endpoint2, e));
|
||||||
Assert.Null(c1.Parameters);
|
Assert.Null(c1.Parameters);
|
||||||
Assert.Empty(c1.Literals);
|
Assert.Null(c1.Literals);
|
||||||
|
|
||||||
var b2 = a.Parameters;
|
var b2 = a.Parameters;
|
||||||
Assert.Empty(b2.Matches);
|
Assert.Null(b2.Matches);
|
||||||
Assert.Null(b2.Parameters);
|
Assert.Null(b2.Parameters);
|
||||||
|
|
||||||
next = Assert.Single(b2.Literals);
|
next = Assert.Single(b2.Literals);
|
||||||
|
|
@ -263,7 +315,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
var c2 = next.Value;
|
var c2 = next.Value;
|
||||||
Assert.Same(endpoint2, Assert.Single(c2.Matches));
|
Assert.Same(endpoint2, Assert.Single(c2.Matches));
|
||||||
Assert.Null(c2.Parameters);
|
Assert.Null(c2.Parameters);
|
||||||
Assert.Empty(c2.Literals);
|
Assert.Null(c2.Literals);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -282,18 +334,18 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
var root = builder.BuildDfaTree();
|
var root = builder.BuildDfaTree();
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Empty(root.Matches);
|
Assert.Null(root.Matches);
|
||||||
Assert.Null(root.Parameters);
|
Assert.Null(root.Parameters);
|
||||||
|
|
||||||
var next = Assert.Single(root.Literals);
|
var next = Assert.Single(root.Literals);
|
||||||
Assert.Equal("a", next.Key);
|
Assert.Equal("a", next.Key);
|
||||||
|
|
||||||
var a = next.Value;
|
var a = next.Value;
|
||||||
Assert.Empty(a.Matches);
|
Assert.Null(a.Matches);
|
||||||
Assert.Empty(a.Literals);
|
Assert.Null(a.Literals);
|
||||||
|
|
||||||
var b = a.Parameters;
|
var b = a.Parameters;
|
||||||
Assert.Empty(b.Matches);
|
Assert.Null(b.Matches);
|
||||||
Assert.Null(b.Parameters);
|
Assert.Null(b.Parameters);
|
||||||
|
|
||||||
next = Assert.Single(b.Literals);
|
next = Assert.Single(b.Literals);
|
||||||
|
|
@ -305,7 +357,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
e => Assert.Same(endpoint1, e),
|
e => Assert.Same(endpoint1, e),
|
||||||
e => Assert.Same(endpoint2, e));
|
e => Assert.Same(endpoint2, e));
|
||||||
Assert.Null(c.Parameters);
|
Assert.Null(c.Parameters);
|
||||||
Assert.Empty(c.Literals);
|
Assert.Null(c.Literals);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -324,7 +376,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
var root = builder.BuildDfaTree();
|
var root = builder.BuildDfaTree();
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Empty(root.Matches);
|
Assert.Null(root.Matches);
|
||||||
Assert.Null(root.Parameters);
|
Assert.Null(root.Parameters);
|
||||||
|
|
||||||
var next = Assert.Single(root.Literals);
|
var next = Assert.Single(root.Literals);
|
||||||
|
|
@ -349,7 +401,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
e => Assert.Same(endpoint1, e),
|
e => Assert.Same(endpoint1, e),
|
||||||
e => Assert.Same(endpoint2, e));
|
e => Assert.Same(endpoint2, e));
|
||||||
Assert.Null(c1.Parameters);
|
Assert.Null(c1.Parameters);
|
||||||
Assert.Empty(c1.Literals);
|
Assert.Null(c1.Literals);
|
||||||
|
|
||||||
var catchAll = a.CatchAll;
|
var catchAll = a.CatchAll;
|
||||||
Assert.Same(endpoint2, Assert.Single(catchAll.Matches));
|
Assert.Same(endpoint2, Assert.Single(catchAll.Matches));
|
||||||
|
|
@ -373,7 +425,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
var root = builder.BuildDfaTree();
|
var root = builder.BuildDfaTree();
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Empty(root.Matches);
|
Assert.Null(root.Matches);
|
||||||
Assert.Null(root.Parameters);
|
Assert.Null(root.Parameters);
|
||||||
|
|
||||||
var next = Assert.Single(root.Literals);
|
var next = Assert.Single(root.Literals);
|
||||||
|
|
@ -381,7 +433,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
|
|
||||||
var a = next.Value;
|
var a = next.Value;
|
||||||
Assert.Same(endpoint2, Assert.Single(a.Matches));
|
Assert.Same(endpoint2, Assert.Single(a.Matches));
|
||||||
Assert.Empty(a.Literals);
|
Assert.Null(a.Literals);
|
||||||
|
|
||||||
var b1 = a.Parameters;
|
var b1 = a.Parameters;
|
||||||
Assert.Same(endpoint2, Assert.Single(a.Matches));
|
Assert.Same(endpoint2, Assert.Single(a.Matches));
|
||||||
|
|
@ -396,7 +448,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
e => Assert.Same(endpoint1, e),
|
e => Assert.Same(endpoint1, e),
|
||||||
e => Assert.Same(endpoint2, e));
|
e => Assert.Same(endpoint2, e));
|
||||||
Assert.Null(c1.Parameters);
|
Assert.Null(c1.Parameters);
|
||||||
Assert.Empty(c1.Literals);
|
Assert.Null(c1.Literals);
|
||||||
|
|
||||||
var catchAll = a.CatchAll;
|
var catchAll = a.CatchAll;
|
||||||
Assert.Same(endpoint2, Assert.Single(catchAll.Matches));
|
Assert.Same(endpoint2, Assert.Single(catchAll.Matches));
|
||||||
|
|
@ -417,7 +469,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
var root = builder.BuildDfaTree();
|
var root = builder.BuildDfaTree();
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Empty(root.Matches);
|
Assert.Null(root.Matches);
|
||||||
Assert.Null(root.Parameters);
|
Assert.Null(root.Parameters);
|
||||||
|
|
||||||
var next = Assert.Single(root.Literals);
|
var next = Assert.Single(root.Literals);
|
||||||
|
|
@ -440,7 +492,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
var test2_true = test1_0.PolicyEdges[true];
|
var test2_true = test1_0.PolicyEdges[true];
|
||||||
Assert.Same(endpoint1, Assert.Single(test2_true.Matches));
|
Assert.Same(endpoint1, Assert.Single(test2_true.Matches));
|
||||||
Assert.Null(test2_true.NodeBuilder);
|
Assert.Null(test2_true.NodeBuilder);
|
||||||
Assert.Empty(test2_true.PolicyEdges);
|
Assert.Null(test2_true.PolicyEdges);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -462,7 +514,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
var root = builder.BuildDfaTree();
|
var root = builder.BuildDfaTree();
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Empty(root.Matches);
|
Assert.Null(root.Matches);
|
||||||
Assert.Null(root.Parameters);
|
Assert.Null(root.Parameters);
|
||||||
|
|
||||||
var next = Assert.Single(root.Literals);
|
var next = Assert.Single(root.Literals);
|
||||||
|
|
@ -486,7 +538,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
var test2_true = test1_0.PolicyEdges[true];
|
var test2_true = test1_0.PolicyEdges[true];
|
||||||
Assert.Same(endpoint1, Assert.Single(test2_true.Matches));
|
Assert.Same(endpoint1, Assert.Single(test2_true.Matches));
|
||||||
Assert.Null(test2_true.NodeBuilder);
|
Assert.Null(test2_true.NodeBuilder);
|
||||||
Assert.Empty(test2_true.PolicyEdges);
|
Assert.Null(test2_true.PolicyEdges);
|
||||||
|
|
||||||
var test1_1 = a.PolicyEdges[1];
|
var test1_1 = a.PolicyEdges[1];
|
||||||
Assert.Empty(test1_1.Matches);
|
Assert.Empty(test1_1.Matches);
|
||||||
|
|
@ -499,12 +551,12 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
test2_true = test1_1.PolicyEdges[true];
|
test2_true = test1_1.PolicyEdges[true];
|
||||||
Assert.Same(endpoint2, Assert.Single(test2_true.Matches));
|
Assert.Same(endpoint2, Assert.Single(test2_true.Matches));
|
||||||
Assert.Null(test2_true.NodeBuilder);
|
Assert.Null(test2_true.NodeBuilder);
|
||||||
Assert.Empty(test2_true.PolicyEdges);
|
Assert.Null(test2_true.PolicyEdges);
|
||||||
|
|
||||||
var test2_false = test1_1.PolicyEdges[false];
|
var test2_false = test1_1.PolicyEdges[false];
|
||||||
Assert.Same(endpoint3, Assert.Single(test2_false.Matches));
|
Assert.Same(endpoint3, Assert.Single(test2_false.Matches));
|
||||||
Assert.Null(test2_false.NodeBuilder);
|
Assert.Null(test2_false.NodeBuilder);
|
||||||
Assert.Empty(test2_false.PolicyEdges);
|
Assert.Null(test2_false.PolicyEdges);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -526,7 +578,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
var root = builder.BuildDfaTree();
|
var root = builder.BuildDfaTree();
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Empty(root.Matches);
|
Assert.Null(root.Matches);
|
||||||
Assert.Null(root.Parameters);
|
Assert.Null(root.Parameters);
|
||||||
|
|
||||||
var next = Assert.Single(root.Literals);
|
var next = Assert.Single(root.Literals);
|
||||||
|
|
@ -543,12 +595,12 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
var test2_true = a.PolicyEdges[true];
|
var test2_true = a.PolicyEdges[true];
|
||||||
Assert.Equal(new[] { endpoint1, endpoint2, }, test2_true.Matches);
|
Assert.Equal(new[] { endpoint1, endpoint2, }, test2_true.Matches);
|
||||||
Assert.Null(test2_true.NodeBuilder);
|
Assert.Null(test2_true.NodeBuilder);
|
||||||
Assert.Empty(test2_true.PolicyEdges);
|
Assert.Null(test2_true.PolicyEdges);
|
||||||
|
|
||||||
var test2_false = a.PolicyEdges[false];
|
var test2_false = a.PolicyEdges[false];
|
||||||
Assert.Equal(new[] { endpoint3, }, test2_false.Matches);
|
Assert.Equal(new[] { endpoint3, }, test2_false.Matches);
|
||||||
Assert.Null(test2_false.NodeBuilder);
|
Assert.Null(test2_false.NodeBuilder);
|
||||||
Assert.Empty(test2_false.PolicyEdges);
|
Assert.Null(test2_false.PolicyEdges);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -570,7 +622,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
var root = builder.BuildDfaTree();
|
var root = builder.BuildDfaTree();
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Empty(root.Matches);
|
Assert.Null(root.Matches);
|
||||||
Assert.Null(root.Parameters);
|
Assert.Null(root.Parameters);
|
||||||
|
|
||||||
var next = Assert.Single(root.Literals);
|
var next = Assert.Single(root.Literals);
|
||||||
|
|
@ -587,12 +639,12 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
var test1_0 = a.PolicyEdges[0];
|
var test1_0 = a.PolicyEdges[0];
|
||||||
Assert.Equal(new[] { endpoint1, }, test1_0.Matches);
|
Assert.Equal(new[] { endpoint1, }, test1_0.Matches);
|
||||||
Assert.Null(test1_0.NodeBuilder);
|
Assert.Null(test1_0.NodeBuilder);
|
||||||
Assert.Empty(test1_0.PolicyEdges);
|
Assert.Null(test1_0.PolicyEdges);
|
||||||
|
|
||||||
var test1_1 = a.PolicyEdges[1];
|
var test1_1 = a.PolicyEdges[1];
|
||||||
Assert.Equal(new[] { endpoint2, endpoint3, }, test1_1.Matches);
|
Assert.Equal(new[] { endpoint2, endpoint3, }, test1_1.Matches);
|
||||||
Assert.Null(test1_1.NodeBuilder);
|
Assert.Null(test1_1.NodeBuilder);
|
||||||
Assert.Empty(test1_1.PolicyEdges);
|
Assert.Null(test1_1.PolicyEdges);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -614,7 +666,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
var root = builder.BuildDfaTree();
|
var root = builder.BuildDfaTree();
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Empty(root.Matches);
|
Assert.Null(root.Matches);
|
||||||
Assert.Null(root.Parameters);
|
Assert.Null(root.Parameters);
|
||||||
|
|
||||||
var next = Assert.Single(root.Literals);
|
var next = Assert.Single(root.Literals);
|
||||||
|
|
@ -632,12 +684,12 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
var test1_0 = a.PolicyEdges[0];
|
var test1_0 = a.PolicyEdges[0];
|
||||||
Assert.Equal(new[] { endpoint1, }, test1_0.Matches);
|
Assert.Equal(new[] { endpoint1, }, test1_0.Matches);
|
||||||
Assert.Null(test1_0.NodeBuilder);
|
Assert.Null(test1_0.NodeBuilder);
|
||||||
Assert.Empty(test1_0.PolicyEdges);
|
Assert.Null(test1_0.PolicyEdges);
|
||||||
|
|
||||||
var test1_1 = a.PolicyEdges[1];
|
var test1_1 = a.PolicyEdges[1];
|
||||||
Assert.Equal(new[] { endpoint2, endpoint3, }, test1_1.Matches);
|
Assert.Equal(new[] { endpoint2, endpoint3, }, test1_1.Matches);
|
||||||
Assert.Null(test1_1.NodeBuilder);
|
Assert.Null(test1_1.NodeBuilder);
|
||||||
Assert.Empty(test1_1.PolicyEdges);
|
Assert.Null(test1_1.PolicyEdges);
|
||||||
|
|
||||||
var nonRouteEndpoint = a.PolicyEdges[int.MaxValue];
|
var nonRouteEndpoint = a.PolicyEdges[int.MaxValue];
|
||||||
Assert.Equal("MaxValueEndpoint", Assert.Single(nonRouteEndpoint.Matches).DisplayName);
|
Assert.Equal("MaxValueEndpoint", Assert.Single(nonRouteEndpoint.Matches).DisplayName);
|
||||||
|
|
@ -662,7 +714,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
var root = builder.BuildDfaTree();
|
var root = builder.BuildDfaTree();
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Empty(root.Matches);
|
Assert.Null(root.Matches);
|
||||||
Assert.Null(root.Parameters);
|
Assert.Null(root.Parameters);
|
||||||
|
|
||||||
var next = Assert.Single(root.Literals);
|
var next = Assert.Single(root.Literals);
|
||||||
|
|
@ -671,7 +723,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
var a = next.Value;
|
var a = next.Value;
|
||||||
Assert.Equal(new[] { endpoint1, endpoint2, endpoint3, }, a.Matches);
|
Assert.Equal(new[] { endpoint1, endpoint2, endpoint3, }, a.Matches);
|
||||||
Assert.Null(a.NodeBuilder);
|
Assert.Null(a.NodeBuilder);
|
||||||
Assert.Empty(a.PolicyEdges);
|
Assert.Null(a.PolicyEdges);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
|
||||||
|
|
@ -74,21 +74,21 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
// This code was generated by the Swaggatherer
|
// This code was generated by the Swaggatherer
|
||||||
public partial class GeneratedBenchmark : EndpointRoutingBenchmarkBase
|
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}];
|
Endpoints = new RouteEndpoint[{3}];
|
||||||
{0}
|
{0}
|
||||||
}}
|
}}
|
||||||
|
|
||||||
private void SetupRequests()
|
private protected void SetupRequests()
|
||||||
{{
|
{{
|
||||||
Requests = new HttpContext[{3}];
|
Requests = new HttpContext[{3}];
|
||||||
{1}
|
{1}
|
||||||
}}
|
}}
|
||||||
|
|
||||||
private Matcher SetupMatcher(MatcherBuilder builder)
|
private protected Matcher SetupMatcher(MatcherBuilder builder)
|
||||||
{{
|
{{
|
||||||
{2}
|
{2}
|
||||||
return builder.Build();
|
return builder.Build();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue