Fix remaining feature gaps in DfaMatcher (#621)
* Fix remaining feature gaps in DfaMatcher * addressed minor feedback * missed one
This commit is contained in:
parent
1196349bf4
commit
400d243f42
|
|
@ -1,28 +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;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
||||
{
|
||||
// This is not yet fleshed out - consider this part of the
|
||||
// work-in-progress definition of CandidateSet.
|
||||
internal readonly struct Candidate
|
||||
{
|
||||
public readonly MatcherEndpoint Endpoint;
|
||||
|
||||
public readonly string[] Parameters;
|
||||
|
||||
public Candidate(MatcherEndpoint endpoint)
|
||||
{
|
||||
Endpoint = endpoint;
|
||||
Parameters = Array.Empty<string>();
|
||||
}
|
||||
|
||||
public Candidate(MatcherEndpoint endpoint, string[] parameters)
|
||||
{
|
||||
Endpoint = endpoint;
|
||||
Parameters = parameters;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -30,7 +30,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
|||
_samples = SampleRequests(EndpointCount, SampleCount);
|
||||
|
||||
_baseline = (BarebonesMatcher)SetupMatcher(new BarebonesMatcherBuilder());
|
||||
_dfa = SetupMatcher(new DfaMatcherBuilder());
|
||||
_dfa = SetupMatcher(CreateDfaMatcherBuilder());
|
||||
_tree = SetupMatcher(new TreeRouterMatcherBuilder());
|
||||
|
||||
_feature = new EndpointFeature();
|
||||
|
|
|
|||
|
|
@ -22,9 +22,18 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
|||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging();
|
||||
services.AddOptions();
|
||||
services.AddRouting();
|
||||
services.AddDispatcher();
|
||||
return services.BuildServiceProvider();
|
||||
}
|
||||
|
||||
private protected DfaMatcherBuilder CreateDfaMatcherBuilder()
|
||||
{
|
||||
var services = CreateServices();
|
||||
return ActivatorUtilities.CreateInstance<DfaMatcherBuilder>(services);
|
||||
}
|
||||
|
||||
private protected static MatcherEndpoint CreateEndpoint(string template, string httpMethod = null)
|
||||
{
|
||||
var metadata = new List<object>();
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
|||
SetupRequests();
|
||||
|
||||
_baseline = (BarebonesMatcher)SetupMatcher(new BarebonesMatcherBuilder());
|
||||
_dfa = SetupMatcher(new DfaMatcherBuilder());
|
||||
_dfa = SetupMatcher(CreateDfaMatcherBuilder());
|
||||
_tree = SetupMatcher(new TreeRouterMatcherBuilder());
|
||||
|
||||
_feature = new EndpointFeature();
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
|||
_samples = SampleRequests(EndpointCount, SampleCount);
|
||||
|
||||
_baseline = (BarebonesMatcher)SetupMatcher(new BarebonesMatcherBuilder());
|
||||
_dfa = (DfaMatcher)SetupMatcher(new DfaMatcherBuilder());
|
||||
_dfa = (DfaMatcher)SetupMatcher(CreateDfaMatcherBuilder());
|
||||
}
|
||||
|
||||
[Benchmark(Baseline = true, OperationsPerInvoke = SampleCount)]
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
|||
SetupRequests();
|
||||
|
||||
_baseline = (BarebonesMatcher)SetupMatcher(new BarebonesMatcherBuilder());
|
||||
_dfa = (DfaMatcher)SetupMatcher(new DfaMatcherBuilder());
|
||||
_dfa = (DfaMatcher)SetupMatcher(CreateDfaMatcherBuilder());
|
||||
}
|
||||
|
||||
[Benchmark(Baseline = true, OperationsPerInvoke = EndpointCount)]
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
|||
Requests[0].Request.Path = "/plaintext";
|
||||
|
||||
_baseline = (TrivialMatcher)SetupMatcher(new TrivialMatcherBuilder());
|
||||
_dfa = (DfaMatcher)SetupMatcher(new DfaMatcherBuilder());
|
||||
_dfa = (DfaMatcher)SetupMatcher(CreateDfaMatcherBuilder());
|
||||
}
|
||||
|
||||
private Matcher SetupMatcher(MatcherBuilder builder)
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
|||
SetupRequests();
|
||||
|
||||
_baseline = (TrivialMatcher)SetupMatcher(new TrivialMatcherBuilder());
|
||||
_dfa = (DfaMatcher)SetupMatcher(new DfaMatcherBuilder());
|
||||
_dfa = (DfaMatcher)SetupMatcher(CreateDfaMatcherBuilder());
|
||||
|
||||
_feature = new EndpointFeature();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
|||
Requests[0].Request.Path = "/plaintext";
|
||||
|
||||
_baseline = (BarebonesMatcher)SetupMatcher(new BarebonesMatcherBuilder());
|
||||
_dfa = SetupMatcher(new DfaMatcherBuilder());
|
||||
_dfa = SetupMatcher(CreateDfaMatcherBuilder());
|
||||
_route = SetupMatcher(new RouteMatcherBuilder());
|
||||
_tree = SetupMatcher(new TreeRouterMatcherBuilder());
|
||||
|
||||
|
|
|
|||
|
|
@ -18,23 +18,12 @@
|
|||
for perf comparisons.
|
||||
-->
|
||||
<ItemGroup>
|
||||
<Compile Include="..\..\test\Microsoft.AspNetCore.Routing.Tests\Matchers\CandidateSet.cs" Link="Matchers\CandidateSet.cs" />
|
||||
<Compile Include="..\..\test\Microsoft.AspNetCore.Routing.Tests\Matchers\MatcherBuilder.cs">
|
||||
<Link>Matchers\MatcherBuilder.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="..\..\test\Microsoft.AspNetCore.Routing.Tests\Matchers\BarebonesMatcher.cs">
|
||||
<Link>Matchers\BarebonesMatcher.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="..\..\test\Microsoft.AspNetCore.Routing.Tests\Matchers\BarebonesMatcherBuilder.cs">
|
||||
<Link>Matchers\BarebonesMatcherBuilder.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="..\..\test\Microsoft.AspNetCore.Routing.Tests\Matchers\DfaMatcher.cs">
|
||||
<Link>Matchers\DfaMatcher.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="..\..\test\Microsoft.AspNetCore.Routing.Tests\Matchers\DfaMatcherBuilder.cs">
|
||||
<Link>Matchers\DfaMatcherBuilder.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="..\..\test\Microsoft.AspNetCore.Routing.Tests\Matchers\MatcherBuilderEntry.cs" Link="Matchers\MatcherBuilderEntry.cs" />
|
||||
<Compile Include="..\..\test\Microsoft.AspNetCore.Routing.Tests\Matchers\RouteMatcher.cs">
|
||||
<Link>Matchers\RouteMatcher.cs</Link>
|
||||
</Compile>
|
||||
|
|
|
|||
|
|
@ -3,4 +3,5 @@
|
|||
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
|
||||
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
|
||||
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")]
|
||||
|
|
|
|||
|
|
@ -24,6 +24,57 @@ namespace Microsoft.AspNetCore.Routing
|
|||
internal PropertyStorage _propertyStorage;
|
||||
private int _count;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="RouteValueDictionary"/> from the provided array.
|
||||
/// The new instance will take ownership of the array, and may mutate it.
|
||||
/// </summary>
|
||||
/// <param name="items">The items array.</param>
|
||||
/// <returns>A new <see cref="RouteValueDictionary"/>.</returns>
|
||||
public static RouteValueDictionary FromArray(KeyValuePair<string, object>[] items)
|
||||
{
|
||||
if (items == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(items));
|
||||
}
|
||||
|
||||
// We need to compress the array by removing non-contiguous items. We
|
||||
// typically have a very small number of items to process. We don't need
|
||||
// to preserve order.
|
||||
var start = 0;
|
||||
var end = items.Length - 1;
|
||||
|
||||
// We walk forwards from the beginning of the array and fill in 'null' slots.
|
||||
// We walk backwards from the end of the array end move items in non-null' slots
|
||||
// into whatever start is pointing to. O(n)
|
||||
while (start <= end)
|
||||
{
|
||||
if (items[start].Key != null)
|
||||
{
|
||||
start++;
|
||||
}
|
||||
else if (items[end].Key != null)
|
||||
{
|
||||
// Swap this item into start and advance
|
||||
items[start] = items[end];
|
||||
items[end] = default;
|
||||
start++;
|
||||
end--;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Both null, we need to hold on 'start' since we
|
||||
// still need to fill it with something.
|
||||
end--;
|
||||
}
|
||||
}
|
||||
|
||||
return new RouteValueDictionary()
|
||||
{
|
||||
_arrayStorage = items,
|
||||
_count = start,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an empty <see cref="RouteValueDictionary"/>.
|
||||
/// </summary>
|
||||
|
|
@ -134,7 +185,6 @@ namespace Microsoft.AspNetCore.Routing
|
|||
}
|
||||
else
|
||||
{
|
||||
|
||||
_arrayStorage[index] = new KeyValuePair<string, object>(key, value);
|
||||
}
|
||||
}
|
||||
|
|
@ -325,7 +375,7 @@ namespace Microsoft.AspNetCore.Routing
|
|||
EnsureCapacity(Count);
|
||||
|
||||
var index = FindInArray(item.Key);
|
||||
var array = _arrayStorage;
|
||||
var array = _arrayStorage;
|
||||
if (index >= 0 && EqualityComparer<object>.Default.Equals(array[index].Value, item.Value))
|
||||
{
|
||||
Array.Copy(array, index + 1, array, index, _count - index);
|
||||
|
|
@ -426,7 +476,7 @@ namespace Microsoft.AspNetCore.Routing
|
|||
var property = storage.Properties[i];
|
||||
array[i] = new KeyValuePair<string, object>(property.Name, property.GetValue(storage.Value));
|
||||
}
|
||||
|
||||
|
||||
_arrayStorage = array;
|
||||
_propertyStorage = null;
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,108 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.AspNetCore.Routing.Patterns;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
||||
{
|
||||
internal readonly struct Candidate
|
||||
{
|
||||
public readonly MatcherEndpoint Endpoint;
|
||||
|
||||
// Used to optimize out operations that modify route values.
|
||||
public readonly CandidateFlags Flags;
|
||||
|
||||
// Data for creating the RouteValueDictionary. We assign each key its own slot
|
||||
// and we fill the values array with all of the default values.
|
||||
//
|
||||
// Then when we process parameters, we don't need to operate on the RouteValueDictionary
|
||||
// we can just operate on an array, which is much much faster.
|
||||
public readonly KeyValuePair<string, object>[] Slots;
|
||||
|
||||
// List of parameters to capture. Segment is the segment index, index is the
|
||||
// index into the values array.
|
||||
public readonly (string parameterName, int segmentIndex, int slotIndex)[] Captures;
|
||||
|
||||
// Catchall parameter to capture (limit one per template).
|
||||
public readonly (string parameterName, int segmentIndex, int slotIndex) CatchAll;
|
||||
|
||||
// Complex segments are processed in a separate pass because they require a
|
||||
// RouteValueDictionary.
|
||||
public readonly (RoutePatternPathSegment pathSegment, int segmentIndex)[] ComplexSegments;
|
||||
|
||||
public readonly MatchProcessor[] MatchProcessors;
|
||||
|
||||
// Used in tests.
|
||||
public Candidate(MatcherEndpoint endpoint)
|
||||
{
|
||||
Endpoint = endpoint;
|
||||
|
||||
Slots = Array.Empty<KeyValuePair<string, object>>();
|
||||
Captures = Array.Empty<(string parameterName, int segmentIndex, int slotIndex)>();
|
||||
CatchAll = default;
|
||||
ComplexSegments = Array.Empty<(RoutePatternPathSegment pathSegment, int segmentIndex)>();
|
||||
MatchProcessors = Array.Empty<MatchProcessor>();
|
||||
|
||||
Flags = CandidateFlags.None;
|
||||
}
|
||||
|
||||
public Candidate(
|
||||
MatcherEndpoint endpoint,
|
||||
KeyValuePair<string, object>[] slots,
|
||||
(string parameterName, int segmentIndex, int slotIndex)[] captures,
|
||||
(string parameterName, int segmentIndex, int slotIndex) catchAll,
|
||||
(RoutePatternPathSegment pathSegment, int segmentIndex)[] complexSegments,
|
||||
MatchProcessor[] matchProcessors)
|
||||
{
|
||||
Endpoint = endpoint;
|
||||
Slots = slots;
|
||||
Captures = captures;
|
||||
CatchAll = catchAll;
|
||||
ComplexSegments = complexSegments;
|
||||
MatchProcessors = matchProcessors;
|
||||
|
||||
Flags = CandidateFlags.None;
|
||||
for (var i = 0; i < slots.Length; i++)
|
||||
{
|
||||
if (slots[i].Key != null)
|
||||
{
|
||||
Flags |= CandidateFlags.HasDefaults;
|
||||
}
|
||||
}
|
||||
|
||||
if (captures.Length > 0)
|
||||
{
|
||||
Flags |= CandidateFlags.HasCaptures;
|
||||
}
|
||||
|
||||
if (catchAll.parameterName != null)
|
||||
{
|
||||
Flags |= CandidateFlags.HasCatchAll;
|
||||
}
|
||||
|
||||
if (complexSegments.Length > 0)
|
||||
{
|
||||
Flags |= CandidateFlags.HasComplexSegments;
|
||||
}
|
||||
|
||||
if (matchProcessors.Length > 0)
|
||||
{
|
||||
Flags |= CandidateFlags.HasMatchProcessors;
|
||||
}
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum CandidateFlags
|
||||
{
|
||||
None = 0,
|
||||
HasDefaults = 1,
|
||||
HasCaptures = 2,
|
||||
HasCatchAll = 4,
|
||||
HasSlots = HasDefaults | HasCaptures | HasCatchAll,
|
||||
HasComplexSegments = 8,
|
||||
HasMatchProcessors = 16,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,8 +5,6 @@ using System;
|
|||
|
||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
||||
{
|
||||
// This is not yet fleshed out - this is a work in progress to
|
||||
// unblock the benchmarks.
|
||||
internal class CandidateSet
|
||||
{
|
||||
public static readonly CandidateSet Empty = new CandidateSet(Array.Empty<Candidate>(), Array.Empty<int>());
|
||||
|
|
@ -56,4 +54,4 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
|||
return groups;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,290 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing.EndpointConstraints;
|
||||
using Microsoft.AspNetCore.Routing.Patterns;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
||||
{
|
||||
internal sealed class DfaMatcher : Matcher
|
||||
{
|
||||
private readonly EndpointSelector _endpointSelector;
|
||||
private readonly DfaState[] _states;
|
||||
|
||||
public DfaMatcher(EndpointSelector endpointSelector, DfaState[] states)
|
||||
{
|
||||
_endpointSelector = endpointSelector;
|
||||
_states = states;
|
||||
}
|
||||
|
||||
public sealed override Task MatchAsync(HttpContext httpContext, IEndpointFeature feature)
|
||||
{
|
||||
if (httpContext == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(httpContext));
|
||||
}
|
||||
|
||||
if (feature == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(feature));
|
||||
}
|
||||
|
||||
// The sequence of actions we take is optimized to avoid doing expensive work
|
||||
// like creating substrings, creating route value dictionaries, and calling
|
||||
// into policies like versioning.
|
||||
var path = httpContext.Request.Path.Value;
|
||||
|
||||
// First tokenize the path into series of segments.
|
||||
Span<PathSegment> buffer = stackalloc PathSegment[FastPathTokenizer.DefaultSegmentCount];
|
||||
var count = FastPathTokenizer.Tokenize(path, buffer);
|
||||
var segments = buffer.Slice(0, count);
|
||||
|
||||
// SelectCandidates will process the DFA and return a candidate set. This does
|
||||
// some preliminary matching of the URL (mostly the literal segments).
|
||||
var candidates = SelectCandidates(path, segments);
|
||||
if (candidates.GroupCount == 0)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// At this point we have a candidate set, defined as a list of groups
|
||||
// of candidates. Each member of a given group has the same priority
|
||||
// (priority is defined by order, precedence and other factors like http method
|
||||
// or version).
|
||||
//
|
||||
// We don't yet know that any candidate can be considered a match, because
|
||||
// we haven't processed things like route constraints and complex segments.
|
||||
//
|
||||
// Now we'll go group by group to capture route values, process constraints,
|
||||
// and process complex segments.
|
||||
//
|
||||
// Perf: using groups.Length - 1 here to elide the bounds check. We're relying
|
||||
// on assumptions of how Groups works.
|
||||
var candidatesArray = candidates.Candidates;
|
||||
var groups = candidates.Groups;
|
||||
|
||||
for (var i = 0; i < groups.Length - 1; i++)
|
||||
{
|
||||
var start = groups[i];
|
||||
var length = groups[i + 1] - groups[i];
|
||||
var group = candidatesArray.AsSpan(start, length);
|
||||
|
||||
// Yes, these allocate. We should revise how this interaction works exactly
|
||||
// once the extensibility is more locked down.
|
||||
//
|
||||
// Would could produce a fast path for a small number of members in
|
||||
// a group.
|
||||
var members = new BitArray(group.Length);
|
||||
var groupValues = new RouteValueDictionary[group.Length];
|
||||
|
||||
if (FilterGroup(
|
||||
httpContext,
|
||||
path,
|
||||
segments,
|
||||
group,
|
||||
members,
|
||||
groupValues))
|
||||
{
|
||||
// We must have some matches because FilterGroup returned true.
|
||||
|
||||
// So: this code is SUPER SUPER temporary. We don't intent to keep
|
||||
// EndpointSelector around for very long.
|
||||
var candidatesForEndpointSelector = new List<Endpoint>();
|
||||
for (var j = 0; j < group.Length; j++)
|
||||
{
|
||||
if (members.Get(j))
|
||||
{
|
||||
candidatesForEndpointSelector.Add(group[j].Endpoint);
|
||||
}
|
||||
}
|
||||
|
||||
var result = _endpointSelector.SelectBestCandidate(httpContext, candidatesForEndpointSelector);
|
||||
if (result != null)
|
||||
{
|
||||
// Find the route values, based on which endpoint was selected. We have
|
||||
// to do this because the endpoint selector returns an endpoint
|
||||
// instead of mutating the feature.
|
||||
for (var j = 0; j < group.Length; j++)
|
||||
{
|
||||
if (ReferenceEquals(result, group[j].Endpoint))
|
||||
{
|
||||
feature.Endpoint = result;
|
||||
feature.Invoker = ((MatcherEndpoint)result).Invoker;
|
||||
feature.Values = groupValues[j];
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// End super temporary code
|
||||
}
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
internal CandidateSet SelectCandidates(string path, ReadOnlySpan<PathSegment> segments)
|
||||
{
|
||||
var states = _states;
|
||||
|
||||
var destination = 0;
|
||||
for (var i = 0; i < segments.Length; i++)
|
||||
{
|
||||
destination = states[destination].Transitions.GetDestination(path, segments[i]);
|
||||
}
|
||||
|
||||
return states[destination].Candidates;
|
||||
}
|
||||
|
||||
private bool FilterGroup(
|
||||
HttpContext httpContext,
|
||||
string path,
|
||||
ReadOnlySpan<PathSegment> segments,
|
||||
ReadOnlySpan<Candidate> group,
|
||||
BitArray members,
|
||||
RouteValueDictionary[] groupValues)
|
||||
{
|
||||
var hasMatch = false;
|
||||
for (var i = 0; i < group.Length; i++)
|
||||
{
|
||||
// PERF: specifically not copying group[i] into a local. It's a relatively
|
||||
// fat struct and we don't want to eagerly copy it.
|
||||
var flags = group[i].Flags;
|
||||
|
||||
// First process all of the parameters and defaults.
|
||||
RouteValueDictionary values;
|
||||
if ((flags & Candidate.CandidateFlags.HasSlots) == 0)
|
||||
{
|
||||
values = new RouteValueDictionary();
|
||||
}
|
||||
else
|
||||
{
|
||||
// The Slots array has the default values of the route values in it.
|
||||
//
|
||||
// We want to create a new array for the route values based on Slots
|
||||
// as a prototype.
|
||||
var prototype = group[i].Slots;
|
||||
var slots = new KeyValuePair<string, object>[prototype.Length];
|
||||
|
||||
if ((flags & Candidate.CandidateFlags.HasDefaults) != 0)
|
||||
{
|
||||
Array.Copy(prototype, 0, slots, 0, prototype.Length);
|
||||
}
|
||||
|
||||
if ((flags & Candidate.CandidateFlags.HasCaptures) != 0)
|
||||
{
|
||||
ProcessCaptures(slots, group[i].Captures, path, segments);
|
||||
}
|
||||
|
||||
if ((flags & Candidate.CandidateFlags.HasCatchAll) != 0)
|
||||
{
|
||||
ProcessCatchAll(slots, group[i].CatchAll, path, segments);
|
||||
}
|
||||
|
||||
values = RouteValueDictionary.FromArray(slots);
|
||||
}
|
||||
|
||||
groupValues[i] = values;
|
||||
|
||||
// Now that we have the route values, we need to process complex segments.
|
||||
// Complex segments go through an old API that requires a fully-materialized
|
||||
// route value dictionary.
|
||||
var isMatch = true;
|
||||
if ((flags & Candidate.CandidateFlags.HasComplexSegments) != 0)
|
||||
{
|
||||
isMatch &= ProcessComplexSegments(group[i].ComplexSegments, path, segments, values);
|
||||
}
|
||||
|
||||
if ((flags & Candidate.CandidateFlags.HasMatchProcessors) != 0)
|
||||
{
|
||||
isMatch &= ProcessMatchProcessors(group[i].MatchProcessors, httpContext, values);
|
||||
}
|
||||
|
||||
members.Set(i, isMatch);
|
||||
hasMatch |= isMatch;
|
||||
}
|
||||
|
||||
return hasMatch;
|
||||
}
|
||||
|
||||
private void ProcessCaptures(
|
||||
KeyValuePair<string, object>[] slots,
|
||||
(string parameterName, int segmentIndex, int slotIndex)[] captures,
|
||||
string path,
|
||||
ReadOnlySpan<PathSegment> segments)
|
||||
{
|
||||
for (var i = 0; i < captures.Length; i++)
|
||||
{
|
||||
var parameterName = captures[i].parameterName;
|
||||
if (segments.Length > captures[i].segmentIndex)
|
||||
{
|
||||
var segment = segments[captures[i].segmentIndex];
|
||||
if (parameterName != null && segment.Length > 0)
|
||||
{
|
||||
slots[captures[i].slotIndex] = new KeyValuePair<string, object>(
|
||||
parameterName,
|
||||
path.Substring(segment.Start, segment.Length));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessCatchAll(
|
||||
KeyValuePair<string, object>[] slots,
|
||||
(string parameterName, int segmentIndex, int slotIndex) catchAll,
|
||||
string path,
|
||||
ReadOnlySpan<PathSegment> segments)
|
||||
{
|
||||
if (segments.Length > catchAll.segmentIndex)
|
||||
{
|
||||
var segment = segments[catchAll.segmentIndex];
|
||||
slots[catchAll.slotIndex] = new KeyValuePair<string, object>(
|
||||
catchAll.parameterName,
|
||||
path.Substring(segment.Start));
|
||||
}
|
||||
}
|
||||
|
||||
private bool ProcessComplexSegments(
|
||||
(RoutePatternPathSegment pathSegment, int segmentIndex)[] complexSegments,
|
||||
string path,
|
||||
ReadOnlySpan<PathSegment> segments,
|
||||
RouteValueDictionary values)
|
||||
{
|
||||
for (var i = 0; i < complexSegments.Length; i++)
|
||||
{
|
||||
var segment = segments[complexSegments[i].segmentIndex];
|
||||
var text = path.Substring(segment.Start, segment.Length);
|
||||
if (!RoutePatternMatcher.MatchComplexSegment(complexSegments[i].pathSegment, text, values))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool ProcessMatchProcessors(
|
||||
MatchProcessor[] matchProcessors,
|
||||
HttpContext httpContext,
|
||||
RouteValueDictionary values)
|
||||
{
|
||||
for (var i = 0; i < matchProcessors.Length; i++)
|
||||
{
|
||||
var matchProcessor = matchProcessors[i];
|
||||
if (!matchProcessor.ProcessInbound(httpContext, values))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,435 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Routing.EndpointConstraints;
|
||||
using Microsoft.AspNetCore.Routing.Patterns;
|
||||
using Microsoft.AspNetCore.Routing.Template;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
||||
{
|
||||
internal class DfaMatcherBuilder : MatcherBuilder
|
||||
{
|
||||
private readonly List<MatcherBuilderEntry> _entries = new List<MatcherBuilderEntry>();
|
||||
private readonly IInlineConstraintResolver _constraintResolver = new DefaultInlineConstraintResolver(Options.Create(new RouteOptions()));
|
||||
|
||||
private readonly MatchProcessorFactory _matchProcessorFactory;
|
||||
private readonly EndpointSelector _endpointSelector;
|
||||
|
||||
public DfaMatcherBuilder(
|
||||
MatchProcessorFactory matchProcessorFactory,
|
||||
EndpointSelector endpointSelector)
|
||||
{
|
||||
_matchProcessorFactory = matchProcessorFactory ?? throw new ArgumentNullException(nameof(matchProcessorFactory));
|
||||
_endpointSelector = endpointSelector ?? throw new ArgumentNullException(nameof(endpointSelector));
|
||||
}
|
||||
|
||||
public override void AddEndpoint(MatcherEndpoint endpoint)
|
||||
{
|
||||
_entries.Add(new MatcherBuilderEntry(endpoint));
|
||||
}
|
||||
|
||||
public DfaNode BuildDfaTree()
|
||||
{
|
||||
// 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
|
||||
// traverse. This means that we need to order the entries first, or else we will
|
||||
// miss possible edges in the DFA.
|
||||
_entries.Sort();
|
||||
|
||||
// Since we're doing a BFS we will process each 'level' of the tree in stages
|
||||
// this list will hold the set of items we need to process at the current
|
||||
// stage.
|
||||
var work = new List<(MatcherBuilderEntry entry, List<DfaNode> parents)>();
|
||||
|
||||
var root = new DfaNode() { Depth = 0, Label = "/" };
|
||||
|
||||
// To prepare for this we need to compute the max depth, as well as
|
||||
// a seed list of items to process (entry, root).
|
||||
var maxDepth = 0;
|
||||
for (var i = 0; i < _entries.Count; i++)
|
||||
{
|
||||
var entry = _entries[i];
|
||||
maxDepth = Math.Max(maxDepth, entry.Pattern.Segments.Count);
|
||||
|
||||
work.Add((entry, new List<DfaNode>() { root, }));
|
||||
}
|
||||
|
||||
// Now we process the entries a level at a time.
|
||||
for (var depth = 0; depth <= maxDepth; depth++)
|
||||
{
|
||||
// As we process items, collect the next set of items.
|
||||
var nextWork = new List<(MatcherBuilderEntry entry, List<DfaNode> parents)>();
|
||||
|
||||
for (var i = 0; i < work.Count; i++)
|
||||
{
|
||||
var (entry, parents) = work[i];
|
||||
|
||||
if (!HasAdditionalRequiredSegments(entry, depth))
|
||||
{
|
||||
for (var j = 0; j < parents.Count; j++)
|
||||
{
|
||||
var parent = parents[j];
|
||||
parent.Matches.Add(entry);
|
||||
}
|
||||
}
|
||||
|
||||
// Find the parents of this edge at the current depth
|
||||
var nextParents = new List<DfaNode>();
|
||||
var segment = GetCurrentSegment(entry, depth);
|
||||
if (segment == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for (var j = 0; j < parents.Count; j++)
|
||||
{
|
||||
var parent = parents[j];
|
||||
if (segment.IsSimple && segment.Parts[0].IsLiteral)
|
||||
{
|
||||
var literal = segment.Parts[0].Text;
|
||||
if (!parent.Literals.TryGetValue(literal, out var next))
|
||||
{
|
||||
next = new DfaNode()
|
||||
{
|
||||
Depth = parent.Depth + 1,
|
||||
Label = parent.Label + literal + "/",
|
||||
};
|
||||
parent.Literals.Add(literal, next);
|
||||
}
|
||||
|
||||
nextParents.Add(next);
|
||||
}
|
||||
else if (segment.IsSimple && segment.Parts[0].IsCatchAll)
|
||||
{
|
||||
// A catch all should traverse all literal nodes as well as parameter nodes
|
||||
// we don't need to create the parameter node here because of ordering
|
||||
// all catchalls will be processed after all parameters.
|
||||
nextParents.AddRange(parent.Literals.Values);
|
||||
if (parent.Parameters != null)
|
||||
{
|
||||
nextParents.Add(parent.Parameters);
|
||||
}
|
||||
|
||||
// We also create a 'catchall' here. We don't do further traversals
|
||||
// on the catchall node because only catchalls can end up here. The
|
||||
// catchall node allows us to capture an unlimited amount of segments
|
||||
// and also to match a zero-length segment, which a parameter node
|
||||
// doesn't allow.
|
||||
if (parent.CatchAll == null)
|
||||
{
|
||||
parent.CatchAll = new DfaNode()
|
||||
{
|
||||
Depth = parent.Depth + 1,
|
||||
Label = parent.Label + "{*...}/",
|
||||
};
|
||||
|
||||
// The catchall node just loops.
|
||||
parent.CatchAll.Parameters = parent.CatchAll;
|
||||
parent.CatchAll.CatchAll = parent.CatchAll;
|
||||
}
|
||||
|
||||
parent.CatchAll.Matches.Add(entry);
|
||||
}
|
||||
else if (segment.IsSimple && segment.Parts[0].IsParameter)
|
||||
{
|
||||
if (parent.Parameters == null)
|
||||
{
|
||||
parent.Parameters = new DfaNode()
|
||||
{
|
||||
Depth = parent.Depth + 1,
|
||||
Label = parent.Label + "{...}/",
|
||||
};
|
||||
}
|
||||
|
||||
// A parameter should traverse all literal nodes as well as the parameter node
|
||||
nextParents.AddRange(parent.Literals.Values);
|
||||
nextParents.Add(parent.Parameters);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Complex segment - we treat these are parameters here and do the
|
||||
// expensive processing later. We don't want to spend time processing
|
||||
// complex segments unless they are the best match, and treating them
|
||||
// like parameters in the DFA allows us to do just that.
|
||||
if (parent.Parameters == null)
|
||||
{
|
||||
parent.Parameters = new DfaNode()
|
||||
{
|
||||
Depth = parent.Depth + 1,
|
||||
Label = parent.Label + "{...}/",
|
||||
};
|
||||
}
|
||||
|
||||
nextParents.AddRange(parent.Literals.Values);
|
||||
nextParents.Add(parent.Parameters);
|
||||
}
|
||||
}
|
||||
|
||||
if (nextParents.Count > 0)
|
||||
{
|
||||
nextWork.Add((entry, nextParents));
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare the process the next stage.
|
||||
work = nextWork;
|
||||
}
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
private TemplateSegment GetCurrentSegment(MatcherBuilderEntry entry, int depth)
|
||||
{
|
||||
if (depth < entry.Pattern.Segments.Count)
|
||||
{
|
||||
return entry.Pattern.Segments[depth];
|
||||
}
|
||||
|
||||
if (entry.Pattern.Segments.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var lastSegment = entry.Pattern.Segments[entry.Pattern.Segments.Count - 1];
|
||||
if (lastSegment.IsSimple && lastSegment.Parts[0].IsCatchAll)
|
||||
{
|
||||
return lastSegment;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public override Matcher Build()
|
||||
{
|
||||
var root = BuildDfaTree();
|
||||
|
||||
var states = new List<DfaState>();
|
||||
var tables = new List<JumpTableBuilder>();
|
||||
AddNode(root, states, tables);
|
||||
|
||||
var exit = states.Count;
|
||||
states.Add(new DfaState(CandidateSet.Empty, null));
|
||||
tables.Add(new JumpTableBuilder() { DefaultDestination = exit, ExitDestination = exit, });
|
||||
|
||||
for (var i = 0; i < tables.Count; i++)
|
||||
{
|
||||
if (tables[i].DefaultDestination == JumpTableBuilder.InvalidDestination)
|
||||
{
|
||||
tables[i].DefaultDestination = exit;
|
||||
}
|
||||
|
||||
if (tables[i].ExitDestination == JumpTableBuilder.InvalidDestination)
|
||||
{
|
||||
tables[i].ExitDestination = exit;
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < states.Count; i++)
|
||||
{
|
||||
states[i] = new DfaState(states[i].Candidates, tables[i].Build());
|
||||
}
|
||||
|
||||
return new DfaMatcher(_endpointSelector, states.ToArray());
|
||||
}
|
||||
|
||||
private int AddNode(DfaNode node, List<DfaState> states, List<JumpTableBuilder> tables)
|
||||
{
|
||||
node.Matches.Sort();
|
||||
|
||||
var stateIndex = states.Count;
|
||||
var candidates = new CandidateSet(
|
||||
node.Matches.Select(CreateCandidate).ToArray(),
|
||||
CandidateSet.MakeGroups(GetGroupLengths(node)));
|
||||
states.Add(new DfaState(candidates, null));
|
||||
|
||||
var table = new JumpTableBuilder();
|
||||
tables.Add(table);
|
||||
|
||||
foreach (var kvp in node.Literals)
|
||||
{
|
||||
if (kvp.Key == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var transition = Transition(kvp.Value);
|
||||
table.AddEntry(kvp.Key, transition);
|
||||
}
|
||||
|
||||
if (node.Parameters != null &&
|
||||
node.CatchAll != null &&
|
||||
ReferenceEquals(node.Parameters, node.CatchAll))
|
||||
{
|
||||
// This node has a single transition to but it should accept zero-width segments
|
||||
// this can happen when a node only has catchall parameters.
|
||||
table.DefaultDestination = Transition(node.Parameters);
|
||||
table.ExitDestination = table.DefaultDestination;
|
||||
}
|
||||
else if (node.Parameters != null && node.CatchAll != null)
|
||||
{
|
||||
// This node has a separate transition for zero-width segments
|
||||
// this can happen when a node has both parameters and catchall parameters.
|
||||
table.DefaultDestination = Transition(node.Parameters);
|
||||
table.ExitDestination = Transition(node.CatchAll);
|
||||
}
|
||||
else if (node.Parameters != null)
|
||||
{
|
||||
// This node has paramters but no catchall.
|
||||
table.DefaultDestination = Transition(node.Parameters);
|
||||
}
|
||||
else if (node.CatchAll != null)
|
||||
{
|
||||
// This node has a catchall but no parameters
|
||||
table.DefaultDestination = Transition(node.CatchAll);
|
||||
table.ExitDestination = table.DefaultDestination;
|
||||
}
|
||||
|
||||
return stateIndex;
|
||||
|
||||
int Transition(DfaNode next)
|
||||
{
|
||||
// Break cycles
|
||||
return ReferenceEquals(node, next) ? stateIndex : AddNode(next, states, tables);
|
||||
}
|
||||
}
|
||||
|
||||
// internal for tests
|
||||
internal Candidate CreateCandidate(MatcherBuilderEntry entry)
|
||||
{
|
||||
var assignments = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||
var slots = new List<KeyValuePair<string, object>>();
|
||||
var captures = new List<(string parameterName, int segmentIndex, int slotIndex)>();
|
||||
(string parameterName, int segmentIndex, int slotIndex) catchAll = default;
|
||||
|
||||
foreach (var kvp in entry.Endpoint.Defaults)
|
||||
{
|
||||
assignments.Add(kvp.Key, assignments.Count);
|
||||
slots.Add(kvp);
|
||||
}
|
||||
|
||||
for (var i = 0; i < entry.Pattern.Segments.Count; i++)
|
||||
{
|
||||
var segment = entry.Pattern.Segments[i];
|
||||
if (!segment.IsSimple)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var part = segment.Parts[0];
|
||||
if (!part.IsParameter)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!assignments.TryGetValue(part.Name, out var slotIndex))
|
||||
{
|
||||
slotIndex = assignments.Count;
|
||||
assignments.Add(part.Name, slotIndex);
|
||||
|
||||
var hasDefaultValue = part.DefaultValue != null || part.IsCatchAll;
|
||||
slots.Add(hasDefaultValue ? new KeyValuePair<string, object>(part.Name, part.DefaultValue) : default);
|
||||
}
|
||||
|
||||
if (part.IsCatchAll)
|
||||
{
|
||||
catchAll = (part.Name, i, slotIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
captures.Add((part.Name, i, slotIndex));
|
||||
}
|
||||
}
|
||||
|
||||
var complexSegments = new List<(RoutePatternPathSegment pathSegment, int segmentIndex)>();
|
||||
for (var i = 0; i < entry.Pattern.Segments.Count; i++)
|
||||
{
|
||||
var segment = entry.Pattern.Segments[i];
|
||||
if (segment.IsSimple)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
complexSegments.Add((segment.ToRoutePatternPathSegment(), i));
|
||||
}
|
||||
|
||||
var matchProcessors = new List<MatchProcessor>();
|
||||
for (var i = 0; i < entry.Endpoint.MatchProcessorReferences.Count; i++)
|
||||
{
|
||||
var reference = entry.Endpoint.MatchProcessorReferences[i];
|
||||
var processor = _matchProcessorFactory.Create(reference);
|
||||
matchProcessors.Add(processor);
|
||||
}
|
||||
|
||||
return new Candidate(
|
||||
entry.Endpoint,
|
||||
slots.ToArray(),
|
||||
captures.ToArray(),
|
||||
catchAll,
|
||||
complexSegments.ToArray(),
|
||||
matchProcessors.ToArray());
|
||||
}
|
||||
|
||||
private int[] GetGroupLengths(DfaNode node)
|
||||
{
|
||||
if (node.Matches.Count == 0)
|
||||
{
|
||||
return Array.Empty<int>();
|
||||
}
|
||||
|
||||
var groups = new List<int>();
|
||||
|
||||
var length = 1;
|
||||
var exemplar = node.Matches[0];
|
||||
|
||||
for (var i = 1; i < node.Matches.Count; i++)
|
||||
{
|
||||
if (!exemplar.PriorityEquals(node.Matches[i]))
|
||||
{
|
||||
groups.Add(length);
|
||||
length = 0;
|
||||
|
||||
exemplar = node.Matches[i];
|
||||
}
|
||||
|
||||
length++;
|
||||
}
|
||||
|
||||
groups.Add(length);
|
||||
|
||||
return groups.ToArray();
|
||||
}
|
||||
|
||||
private static bool HasAdditionalRequiredSegments(MatcherBuilderEntry entry, int depth)
|
||||
{
|
||||
for (var i = depth; i < entry.Pattern.Segments.Count; i++)
|
||||
{
|
||||
var segment = entry.Pattern.Segments[i];
|
||||
if (!segment.IsSimple)
|
||||
{
|
||||
// Complex segments always require more processing
|
||||
return true;
|
||||
}
|
||||
|
||||
var part = segment.Parts[0];
|
||||
if (part.IsLiteral)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!part.IsOptional &&
|
||||
!part.IsCatchAll &&
|
||||
part.DefaultValue == null &&
|
||||
!entry.Endpoint.Defaults.ContainsKey(part.Name))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
||||
{
|
||||
// Intermediate data structure used to build the DFA. Not used at runtime.
|
||||
[DebuggerDisplay("{DebuggerToString(),nq}")]
|
||||
internal class DfaNode
|
||||
{
|
||||
public DfaNode()
|
||||
{
|
||||
Literals = new Dictionary<string, DfaNode>(StringComparer.OrdinalIgnoreCase);
|
||||
Matches = new List<MatcherBuilderEntry>();
|
||||
}
|
||||
|
||||
// The depth of the node. The depth indicates the number of segments
|
||||
// that must be processed to arrive at this node.
|
||||
public int Depth { get; set; }
|
||||
|
||||
// Just for diagnostics and debugging
|
||||
public string Label { get; set; }
|
||||
|
||||
public List<MatcherBuilderEntry> Matches { get; }
|
||||
|
||||
public Dictionary<string, DfaNode> Literals { get; }
|
||||
|
||||
public DfaNode Parameters { get; set; }
|
||||
|
||||
public DfaNode CatchAll { get; set; }
|
||||
|
||||
private string DebuggerToString()
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
builder.Append(Label);
|
||||
builder.Append(" d:");
|
||||
builder.Append(Depth);
|
||||
builder.Append(" m:");
|
||||
builder.Append(Matches.Count);
|
||||
builder.Append(" c: ");
|
||||
builder.Append(string.Join(", ", Literals.Select(kvp => $"{kvp.Key}->({FormatNode(kvp.Value)})")));
|
||||
return builder.ToString();
|
||||
|
||||
// DfaNodes can be self-referential, don't traverse cycles.
|
||||
string FormatNode(DfaNode other)
|
||||
{
|
||||
return ReferenceEquals(this, other) ? "this" : other.DebuggerToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
// 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.Diagnostics;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
||||
{
|
||||
[DebuggerDisplay("{DebuggerToString(),nq}")]
|
||||
internal readonly struct DfaState
|
||||
{
|
||||
public readonly CandidateSet Candidates;
|
||||
public readonly JumpTable Transitions;
|
||||
|
||||
public DfaState(CandidateSet candidates, JumpTable transitions)
|
||||
{
|
||||
Candidates = candidates;
|
||||
Transitions = transitions;
|
||||
}
|
||||
|
||||
public string DebuggerToString()
|
||||
{
|
||||
return $"m: {Candidates.Candidates?.Length ?? 0}, j: ({Transitions?.DebuggerToString()})";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -14,9 +14,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
|||
{
|
||||
Endpoint = endpoint;
|
||||
|
||||
HttpMethod = endpoint.Metadata
|
||||
.OfType<HttpMethodEndpointConstraint>()
|
||||
.FirstOrDefault()?.HttpMethods.Single();
|
||||
HttpMethod = endpoint.Metadata.OfType<HttpMethodEndpointConstraint>().FirstOrDefault()?.HttpMethods.Single();
|
||||
Precedence = RoutePrecedence.ComputeInbound(endpoint.ParsedTemplate);
|
||||
}
|
||||
|
||||
|
|
@ -44,15 +42,12 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
|||
return comparison;
|
||||
}
|
||||
|
||||
// Treat the presence of an HttpMethod as a boolean for the purposes of
|
||||
// comparison. We want HttpMethod != null to mean *more specific*.
|
||||
comparison = (HttpMethod == null).CompareTo(other.HttpMethod == null);
|
||||
if (comparison != 0)
|
||||
{
|
||||
return comparison;
|
||||
}
|
||||
|
||||
return Pattern.TemplateText.CompareTo(other.Pattern.TemplateText);
|
||||
}
|
||||
|
||||
public bool PriorityEquals(MatcherBuilderEntry other)
|
||||
{
|
||||
return Order == other.Order && Precedence == other.Precedence;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -161,7 +161,7 @@ namespace Microsoft.AspNetCore.Routing
|
|||
}
|
||||
if (!pathSegment.IsSimple)
|
||||
{
|
||||
if (!MatchComplexSegment(pathSegment, requestSegment.ToString(), Defaults, values))
|
||||
if (!MatchComplexSegment(pathSegment, requestSegment.ToString(), values))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
|
@ -287,10 +287,9 @@ namespace Microsoft.AspNetCore.Routing
|
|||
return false;
|
||||
}
|
||||
|
||||
private bool MatchComplexSegment(
|
||||
internal static bool MatchComplexSegment(
|
||||
RoutePatternPathSegment routeSegment,
|
||||
string requestSegment,
|
||||
IReadOnlyDictionary<string, object> defaults,
|
||||
RouteValueDictionary values)
|
||||
{
|
||||
var indexOfLastSegment = routeSegment.Parts.Count - 1;
|
||||
|
|
@ -307,7 +306,7 @@ namespace Microsoft.AspNetCore.Routing
|
|||
if (routeSegment.Parts[indexOfLastSegment] is RoutePatternParameterPart parameter && parameter.IsOptional &&
|
||||
routeSegment.Parts[indexOfLastSegment - 1].IsSeparator)
|
||||
{
|
||||
if (MatchComplexSegmentCore(routeSegment, requestSegment, Defaults, values, indexOfLastSegment))
|
||||
if (MatchComplexSegmentCore(routeSegment, requestSegment, values, indexOfLastSegment))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
|
@ -322,21 +321,19 @@ namespace Microsoft.AspNetCore.Routing
|
|||
return MatchComplexSegmentCore(
|
||||
routeSegment,
|
||||
requestSegment,
|
||||
Defaults,
|
||||
values,
|
||||
indexOfLastSegment - 2);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return MatchComplexSegmentCore(routeSegment, requestSegment, Defaults, values, indexOfLastSegment);
|
||||
return MatchComplexSegmentCore(routeSegment, requestSegment, values, indexOfLastSegment);
|
||||
}
|
||||
}
|
||||
|
||||
private bool MatchComplexSegmentCore(
|
||||
private static bool MatchComplexSegmentCore(
|
||||
RoutePatternPathSegment routeSegment,
|
||||
string requestSegment,
|
||||
IReadOnlyDictionary<string, object> defaults,
|
||||
RouteValueDictionary values,
|
||||
int indexOfLastSegmentUsed)
|
||||
{
|
||||
|
|
@ -499,7 +496,7 @@ namespace Microsoft.AspNetCore.Routing
|
|||
{
|
||||
foreach (var item in outValues)
|
||||
{
|
||||
values.Add(item.Key, item.Value);
|
||||
values[item.Key] = item.Value;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -5,3 +5,4 @@ using System.Runtime.CompilerServices;
|
|||
|
||||
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Routing.Performance, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
|
||||
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Routing.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
|
||||
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")]
|
||||
|
|
|
|||
|
|
@ -1489,6 +1489,76 @@ namespace Microsoft.AspNetCore.Routing.Tests
|
|||
Assert.Equal("value3", storage[1].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromArray_TakesOwnershipOfArray()
|
||||
{
|
||||
// Arrange
|
||||
var array = new KeyValuePair<string, object>[]
|
||||
{
|
||||
new KeyValuePair<string, object>("a", 0),
|
||||
new KeyValuePair<string, object>("b", 1),
|
||||
new KeyValuePair<string, object>("c", 2),
|
||||
};
|
||||
|
||||
var dictionary = RouteValueDictionary.FromArray(array);
|
||||
|
||||
// Act - modifying the array should modify the dictionary
|
||||
array[0] = new KeyValuePair<string, object>("aa", 10);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(3, dictionary.Count);
|
||||
Assert.Equal(10, dictionary["aa"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromArray_EmptyArray()
|
||||
{
|
||||
// Arrange
|
||||
var array = Array.Empty<KeyValuePair<string, object>>();
|
||||
|
||||
// Act
|
||||
var dictionary = RouteValueDictionary.FromArray(array);
|
||||
|
||||
// Assert
|
||||
Assert.Empty(dictionary);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromArray_RemovesGapsInArray()
|
||||
{
|
||||
// Arrange
|
||||
var array = new KeyValuePair<string, object>[]
|
||||
{
|
||||
new KeyValuePair<string, object>(null, null),
|
||||
new KeyValuePair<string, object>("a", 0),
|
||||
new KeyValuePair<string, object>(null, null),
|
||||
new KeyValuePair<string, object>(null, null),
|
||||
new KeyValuePair<string, object>("b", 1),
|
||||
new KeyValuePair<string, object>("c", 2),
|
||||
new KeyValuePair<string, object>("d", 3),
|
||||
new KeyValuePair<string, object>(null, null),
|
||||
};
|
||||
|
||||
// Act - calling From should modify the array
|
||||
var dictionary = RouteValueDictionary.FromArray(array);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(4, dictionary.Count);
|
||||
Assert.Equal(
|
||||
new KeyValuePair<string, object>[]
|
||||
{
|
||||
new KeyValuePair<string, object>("d", 3),
|
||||
new KeyValuePair<string, object>("a", 0),
|
||||
new KeyValuePair<string, object>("c", 2),
|
||||
new KeyValuePair<string, object>("b", 1),
|
||||
new KeyValuePair<string, object>(null, null),
|
||||
new KeyValuePair<string, object>(null, null),
|
||||
new KeyValuePair<string, object>(null, null),
|
||||
new KeyValuePair<string, object>(null, null),
|
||||
},
|
||||
array);
|
||||
}
|
||||
|
||||
private class RegularType
|
||||
{
|
||||
public bool IsAwesome { get; set; }
|
||||
|
|
|
|||
|
|
@ -1,28 +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;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
||||
{
|
||||
// This is not yet fleshed out - consider this part of the
|
||||
// work-in-progress definition of CandidateSet.
|
||||
internal readonly struct Candidate
|
||||
{
|
||||
public readonly MatcherEndpoint Endpoint;
|
||||
|
||||
public readonly string[] Parameters;
|
||||
|
||||
public Candidate(MatcherEndpoint endpoint)
|
||||
{
|
||||
Endpoint = endpoint;
|
||||
Parameters = Array.Empty<string>();
|
||||
}
|
||||
|
||||
public Candidate(MatcherEndpoint endpoint, string[] parameters)
|
||||
{
|
||||
Endpoint = endpoint;
|
||||
Parameters = parameters;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,134 +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;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing.EndpointConstraints;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
||||
{
|
||||
internal class DfaMatcher : Matcher
|
||||
{
|
||||
private readonly State[] _states;
|
||||
|
||||
public DfaMatcher(State[] states)
|
||||
{
|
||||
_states = states;
|
||||
}
|
||||
|
||||
public override Task MatchAsync(HttpContext httpContext, IEndpointFeature feature)
|
||||
{
|
||||
if (httpContext == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(httpContext));
|
||||
}
|
||||
|
||||
if (feature == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(feature));
|
||||
}
|
||||
|
||||
var path = httpContext.Request.Path.Value;
|
||||
Span<PathSegment> segments = stackalloc PathSegment[FastPathTokenizer.DefaultSegmentCount];
|
||||
var count = FastPathTokenizer.Tokenize(path, segments);
|
||||
|
||||
var candidates = SelectCandidates(path, segments.Slice(0, count));
|
||||
|
||||
var matches = new List<(Endpoint, RouteValueDictionary)>();
|
||||
|
||||
// This code ignores groups for right now.
|
||||
for (var i = 0; i < candidates.Candidates.Length; i++)
|
||||
{
|
||||
var isMatch = true;
|
||||
|
||||
var candidate = candidates.Candidates[i];
|
||||
var values = new RouteValueDictionary();
|
||||
var parameters = candidate.Parameters;
|
||||
if (parameters != null)
|
||||
{
|
||||
for (var j = 0; j < parameters.Length; j++)
|
||||
{
|
||||
var parameter = parameters[j];
|
||||
if (parameter != null && segments[j].Length == 0)
|
||||
{
|
||||
isMatch = false;
|
||||
break;
|
||||
}
|
||||
else if (parameter != null)
|
||||
{
|
||||
var value = path.Substring(segments[j].Start, segments[j].Length);
|
||||
values.Add(parameter, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This is some super super temporary code so we can pass the benchmarks
|
||||
// that do HTTP method matching.
|
||||
var httpMethodConstraint = candidate.Endpoint.Metadata.GetMetadata<HttpMethodEndpointConstraint>();
|
||||
if (httpMethodConstraint != null && !MatchHttpMethod(httpContext.Request.Method, httpMethodConstraint))
|
||||
{
|
||||
isMatch = false;
|
||||
}
|
||||
|
||||
if (isMatch)
|
||||
{
|
||||
matches.Add((candidate.Endpoint, values));
|
||||
}
|
||||
}
|
||||
|
||||
feature.Endpoint = matches.Count == 0 ? null : matches[0].Item1;
|
||||
feature.Values = matches.Count == 0 ? null : matches[0].Item2;
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// This is some super super temporary code so we can pass the benchmarks
|
||||
// that do HTTP method matching.
|
||||
private bool MatchHttpMethod(string httpMethod, HttpMethodEndpointConstraint constraint)
|
||||
{
|
||||
foreach (var supportedHttpMethod in constraint.HttpMethods)
|
||||
{
|
||||
if (string.Equals(supportedHttpMethod, httpMethod, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public CandidateSet SelectCandidates(string path, ReadOnlySpan<PathSegment> segments)
|
||||
{
|
||||
var states = _states;
|
||||
var current = 0;
|
||||
|
||||
for (var i = 0; i < segments.Length; i++)
|
||||
{
|
||||
current = states[current].Transitions.GetDestination(path, segments[i]);
|
||||
}
|
||||
|
||||
return states[current].Candidates;
|
||||
}
|
||||
|
||||
[DebuggerDisplay("{DebuggerToString(),nq}")]
|
||||
public readonly struct State
|
||||
{
|
||||
public readonly CandidateSet Candidates;
|
||||
public readonly JumpTable Transitions;
|
||||
|
||||
public State(CandidateSet candidates, JumpTable transitions)
|
||||
{
|
||||
Candidates = candidates;
|
||||
Transitions = transitions;
|
||||
}
|
||||
|
||||
public string DebuggerToString()
|
||||
{
|
||||
return $"m: {Candidates.Candidates?.Length ?? 0}, j: ({Transitions?.DebuggerToString()})";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,226 +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;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Routing.Template;
|
||||
using static Microsoft.AspNetCore.Routing.Matchers.DfaMatcher;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
||||
{
|
||||
internal class DfaMatcherBuilder : MatcherBuilder
|
||||
{
|
||||
private List<MatcherBuilderEntry> _entries = new List<MatcherBuilderEntry>();
|
||||
|
||||
public override void AddEndpoint(MatcherEndpoint endpoint)
|
||||
{
|
||||
var parsed = TemplateParser.Parse(endpoint.Template);
|
||||
_entries.Add(new MatcherBuilderEntry(endpoint));
|
||||
}
|
||||
|
||||
public override Matcher Build()
|
||||
{
|
||||
_entries.Sort();
|
||||
|
||||
var root = new Node() { Depth = -1 };
|
||||
|
||||
// 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 traverse.
|
||||
var maxDepth = 0;
|
||||
for (var i = 0; i < _entries.Count; i++)
|
||||
{
|
||||
maxDepth = Math.Max(maxDepth, _entries[i].Pattern.Segments.Count);
|
||||
}
|
||||
|
||||
for (var depth = 0; depth <= maxDepth; depth++)
|
||||
{
|
||||
for (var i = 0; i < _entries.Count; i++)
|
||||
{
|
||||
var entry = _entries[i];
|
||||
if (entry.Pattern.Segments.Count < depth)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find the parents of this edge at the current depth
|
||||
var parents = new List<Node>() { root };
|
||||
for (var j = 0; j < depth; j++)
|
||||
{
|
||||
var next = new List<Node>();
|
||||
for (var k = 0; k < parents.Count; k++)
|
||||
{
|
||||
next.Add(Traverse(parents[k], entry.Pattern.Segments[j]));
|
||||
}
|
||||
|
||||
parents = next;
|
||||
}
|
||||
|
||||
if (entry.Pattern.Segments.Count == depth)
|
||||
{
|
||||
for (var j = 0; j < parents.Count; j++)
|
||||
{
|
||||
var parent = parents[j];
|
||||
parent.Matches.Add(entry);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
var segment = entry.Pattern.Segments[depth];
|
||||
for (var j = 0; j < parents.Count; j++)
|
||||
{
|
||||
var parent = parents[j];
|
||||
if (segment.IsSimple && segment.Parts[0].IsLiteral)
|
||||
{
|
||||
if (!parent.Literals.TryGetValue(segment.Parts[0].Text, out var next))
|
||||
{
|
||||
next = new Node() { Depth = depth, };
|
||||
parent.Literals.Add(segment.Parts[0].Text, next);
|
||||
}
|
||||
}
|
||||
else if (segment.IsSimple && segment.Parts[0].IsParameter)
|
||||
{
|
||||
if (!parent.Literals.TryGetValue("*", out var next))
|
||||
{
|
||||
next = new Node() { Depth = depth, };
|
||||
parent.Literals.Add("*", next);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException("We only support simple segments.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var states = new List<State>();
|
||||
var tables = new List<JumpTableBuilder>();
|
||||
AddNode(root, states, tables);
|
||||
|
||||
var exit = states.Count;
|
||||
states.Add(new State(CandidateSet.Empty, null));
|
||||
tables.Add(new JumpTableBuilder() { DefaultDestination = exit, ExitDestination = exit, });
|
||||
|
||||
for (var i = 0; i < tables.Count; i++)
|
||||
{
|
||||
if (tables[i].DefaultDestination == JumpTableBuilder.InvalidDestination)
|
||||
{
|
||||
tables[i].DefaultDestination = exit;
|
||||
}
|
||||
|
||||
if (tables[i].ExitDestination == JumpTableBuilder.InvalidDestination)
|
||||
{
|
||||
tables[i].ExitDestination = exit;
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < states.Count; i++)
|
||||
{
|
||||
states[i] = new State(states[i].Candidates, tables[i].Build());
|
||||
}
|
||||
|
||||
return new DfaMatcher(states.ToArray());
|
||||
}
|
||||
|
||||
private Node Traverse(Node node, TemplateSegment segment)
|
||||
{
|
||||
if (!segment.IsSimple)
|
||||
{
|
||||
throw new InvalidOperationException("We only support simple segments.");
|
||||
}
|
||||
|
||||
if (segment.Parts[0].IsLiteral)
|
||||
{
|
||||
return node.Literals[segment.Parts[0].Text];
|
||||
}
|
||||
|
||||
return node.Literals["*"];
|
||||
}
|
||||
|
||||
private static int AddNode(Node node, List<State> states, List<JumpTableBuilder> tables)
|
||||
{
|
||||
node.Matches.Sort();
|
||||
|
||||
var index = states.Count;
|
||||
|
||||
// This is just temporary. This code ignores groups for now, and creates
|
||||
// a single group with all matches.
|
||||
var candidates = new CandidateSet(
|
||||
node.Matches.Select(CreateCandidate).ToArray(),
|
||||
CandidateSet.MakeGroups(new int[] { node.Matches.Count, }));
|
||||
|
||||
// JumpTable temporarily null. Will be patched later.
|
||||
states.Add(new State(candidates, null));
|
||||
|
||||
var table = new JumpTableBuilder();
|
||||
tables.Add(table);
|
||||
|
||||
foreach (var kvp in node.Literals)
|
||||
{
|
||||
if (kvp.Key == "*")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var transition = AddNode(kvp.Value, states, tables);
|
||||
table.AddEntry(kvp.Key, transition);
|
||||
}
|
||||
|
||||
var defaultIndex = -1;
|
||||
if (node.Literals.TryGetValue("*", out var exit))
|
||||
{
|
||||
defaultIndex = AddNode(exit, states, tables);
|
||||
}
|
||||
|
||||
table.DefaultDestination = defaultIndex;
|
||||
return index;
|
||||
}
|
||||
|
||||
private static Candidate CreateCandidate(MatcherBuilderEntry entry)
|
||||
{
|
||||
var parameters = entry.Pattern.Segments
|
||||
.Select(s => s.IsSimple && s.Parts[0].IsParameter ? s.Parts[0].Name : null)
|
||||
.ToArray();
|
||||
return new Candidate(entry.Endpoint, parameters);
|
||||
}
|
||||
|
||||
private static Node DeepCopy(Node node)
|
||||
{
|
||||
var copy = new Node() { Depth = node.Depth, };
|
||||
copy.Matches.AddRange(node.Matches);
|
||||
|
||||
foreach (var kvp in node.Literals)
|
||||
{
|
||||
copy.Literals.Add(kvp.Key, DeepCopy(kvp.Value));
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
[DebuggerDisplay("{DebuggerToString(),nq}")]
|
||||
private class Node
|
||||
{
|
||||
public int Depth { get; set; }
|
||||
|
||||
public List<MatcherBuilderEntry> Matches { get; } = new List<MatcherBuilderEntry>();
|
||||
|
||||
public Dictionary<string, Node> Literals { get; } = new Dictionary<string, Node>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private string DebuggerToString()
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
builder.Append("d:");
|
||||
builder.Append(Depth);
|
||||
builder.Append(" m:");
|
||||
builder.Append(Matches.Count);
|
||||
builder.Append(" c: ");
|
||||
builder.Append(string.Join(", ", Literals.Select(kvp => $"{kvp.Key}->({kvp.Value.DebuggerToString()})")));
|
||||
return builder.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,626 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Routing.Constraints;
|
||||
using Microsoft.AspNetCore.Routing.EndpointConstraints;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
||||
{
|
||||
public class DfaMatcherBuilderTest
|
||||
{
|
||||
[Fact]
|
||||
public void BuildDfaTree_SingleEndpoint_Empty()
|
||||
{
|
||||
// Arrange
|
||||
var builder = CreateDfaMatcherBuilder();
|
||||
|
||||
var endpoint = CreateEndpoint("/");
|
||||
builder.AddEndpoint(endpoint);
|
||||
|
||||
// Act
|
||||
var root = builder.BuildDfaTree();
|
||||
|
||||
// Assert
|
||||
Assert.Same(endpoint, Assert.Single(root.Matches).Endpoint);
|
||||
Assert.Null(root.Parameters);
|
||||
Assert.Empty(root.Literals);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildDfaTree_SingleEndpoint_Literals()
|
||||
{
|
||||
// Arrange
|
||||
var builder = CreateDfaMatcherBuilder();
|
||||
|
||||
var endpoint = CreateEndpoint("a/b/c");
|
||||
builder.AddEndpoint(endpoint);
|
||||
|
||||
// Act
|
||||
var root = builder.BuildDfaTree();
|
||||
|
||||
// Assert
|
||||
Assert.Empty(root.Matches);
|
||||
Assert.Null(root.Parameters);
|
||||
|
||||
var next = Assert.Single(root.Literals);
|
||||
Assert.Equal("a", next.Key);
|
||||
|
||||
var a = next.Value;
|
||||
Assert.Empty(a.Matches);
|
||||
Assert.Null(a.Parameters);
|
||||
|
||||
next = Assert.Single(a.Literals);
|
||||
Assert.Equal("b", next.Key);
|
||||
|
||||
var b = next.Value;
|
||||
Assert.Empty(b.Matches);
|
||||
Assert.Null(b.Parameters);
|
||||
|
||||
next = Assert.Single(b.Literals);
|
||||
Assert.Equal("c", next.Key);
|
||||
|
||||
var c = next.Value;
|
||||
Assert.Same(endpoint, Assert.Single(c.Matches).Endpoint);
|
||||
Assert.Null(c.Parameters);
|
||||
Assert.Empty(c.Literals);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildDfaTree_SingleEndpoint_Parameters()
|
||||
{
|
||||
// Arrange
|
||||
var builder = CreateDfaMatcherBuilder();
|
||||
|
||||
var endpoint = CreateEndpoint("{a}/{b}/{c}");
|
||||
builder.AddEndpoint(endpoint);
|
||||
|
||||
// Act
|
||||
var root = builder.BuildDfaTree();
|
||||
|
||||
// Assert
|
||||
Assert.Empty(root.Matches);
|
||||
Assert.Empty(root.Literals);
|
||||
|
||||
var a = root.Parameters;
|
||||
Assert.Empty(a.Matches);
|
||||
Assert.Empty(a.Literals);
|
||||
|
||||
var b = a.Parameters;
|
||||
Assert.Empty(b.Matches);
|
||||
Assert.Empty(b.Literals);
|
||||
|
||||
var c = b.Parameters;
|
||||
Assert.Same(endpoint, Assert.Single(c.Matches).Endpoint);
|
||||
Assert.Null(c.Parameters);
|
||||
Assert.Empty(c.Literals);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildDfaTree_SingleEndpoint_CatchAll()
|
||||
{
|
||||
// Arrange
|
||||
var builder = CreateDfaMatcherBuilder();
|
||||
|
||||
var endpoint = CreateEndpoint("{a}/{*b}");
|
||||
builder.AddEndpoint(endpoint);
|
||||
|
||||
// Act
|
||||
var root = builder.BuildDfaTree();
|
||||
|
||||
// Assert
|
||||
Assert.Empty(root.Matches);
|
||||
Assert.Empty(root.Literals);
|
||||
|
||||
var a = root.Parameters;
|
||||
|
||||
// The catch all can match a path like '/a'
|
||||
Assert.Same(endpoint, Assert.Single(a.Matches).Endpoint);
|
||||
Assert.Empty(a.Literals);
|
||||
Assert.Null(a.Parameters);
|
||||
|
||||
// Catch-all nodes include an extra transition that loops to process
|
||||
// extra segments.
|
||||
var catchAll = a.CatchAll;
|
||||
Assert.Same(endpoint, Assert.Single(catchAll.Matches).Endpoint);
|
||||
Assert.Empty(catchAll.Literals);
|
||||
Assert.Same(catchAll, catchAll.Parameters);
|
||||
Assert.Same(catchAll, catchAll.CatchAll);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildDfaTree_SingleEndpoint_CatchAllAtRoot()
|
||||
{
|
||||
// Arrange
|
||||
var builder = CreateDfaMatcherBuilder();
|
||||
|
||||
var endpoint = CreateEndpoint("{*a}");
|
||||
builder.AddEndpoint(endpoint);
|
||||
|
||||
// Act
|
||||
var root = builder.BuildDfaTree();
|
||||
|
||||
// Assert
|
||||
Assert.Same(endpoint, Assert.Single(root.Matches).Endpoint);
|
||||
Assert.Empty(root.Literals);
|
||||
|
||||
// Catch-all nodes include an extra transition that loops to process
|
||||
// extra segments.
|
||||
var catchAll = root.CatchAll;
|
||||
Assert.Same(endpoint, Assert.Single(catchAll.Matches).Endpoint);
|
||||
Assert.Empty(catchAll.Literals);
|
||||
Assert.Same(catchAll, catchAll.Parameters);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildDfaTree_MultipleEndpoint_LiteralAndLiteral()
|
||||
{
|
||||
// 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.Empty(root.Matches);
|
||||
Assert.Null(root.Parameters);
|
||||
|
||||
var next = Assert.Single(root.Literals);
|
||||
Assert.Equal("a", next.Key);
|
||||
|
||||
var a = next.Value;
|
||||
Assert.Empty(a.Matches);
|
||||
|
||||
Assert.Equal(2, a.Literals.Count);
|
||||
|
||||
var b1 = a.Literals["b1"];
|
||||
Assert.Empty(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).Endpoint);
|
||||
Assert.Null(c1.Parameters);
|
||||
Assert.Empty(c1.Literals);
|
||||
|
||||
var b2 = a.Literals["b2"];
|
||||
Assert.Empty(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).Endpoint);
|
||||
Assert.Null(c2.Parameters);
|
||||
Assert.Empty(c2.Literals);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildDfaTree_MultipleEndpoint_LiteralAndParameter()
|
||||
{
|
||||
// Arrange
|
||||
var builder = CreateDfaMatcherBuilder();
|
||||
|
||||
var endpoint1 = CreateEndpoint("a/b/c");
|
||||
builder.AddEndpoint(endpoint1);
|
||||
|
||||
var endpoint2 = CreateEndpoint("a/{b}/c");
|
||||
builder.AddEndpoint(endpoint2);
|
||||
|
||||
// Act
|
||||
var root = builder.BuildDfaTree();
|
||||
|
||||
// Assert
|
||||
Assert.Empty(root.Matches);
|
||||
Assert.Null(root.Parameters);
|
||||
|
||||
var next = Assert.Single(root.Literals);
|
||||
Assert.Equal("a", next.Key);
|
||||
|
||||
var a = next.Value;
|
||||
Assert.Empty(a.Matches);
|
||||
|
||||
next = Assert.Single(a.Literals);
|
||||
Assert.Equal("b", next.Key);
|
||||
|
||||
var b = next.Value;
|
||||
Assert.Empty(b.Matches);
|
||||
Assert.Null(b.Parameters);
|
||||
|
||||
next = Assert.Single(b.Literals);
|
||||
Assert.Equal("c", next.Key);
|
||||
|
||||
var c1 = next.Value;
|
||||
Assert.Collection(
|
||||
c1.Matches,
|
||||
e => Assert.Same(endpoint1, e.Endpoint),
|
||||
e => Assert.Same(endpoint2, e.Endpoint));
|
||||
Assert.Null(c1.Parameters);
|
||||
Assert.Empty(c1.Literals);
|
||||
|
||||
var b2 = a.Parameters;
|
||||
Assert.Empty(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).Endpoint);
|
||||
Assert.Null(c2.Parameters);
|
||||
Assert.Empty(c2.Literals);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildDfaTree_MultipleEndpoint_ParameterAndParameter()
|
||||
{
|
||||
// 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.Empty(root.Matches);
|
||||
Assert.Null(root.Parameters);
|
||||
|
||||
var next = Assert.Single(root.Literals);
|
||||
Assert.Equal("a", next.Key);
|
||||
|
||||
var a = next.Value;
|
||||
Assert.Empty(a.Matches);
|
||||
Assert.Empty(a.Literals);
|
||||
|
||||
var b = a.Parameters;
|
||||
Assert.Empty(b.Matches);
|
||||
Assert.Null(b.Parameters);
|
||||
|
||||
next = Assert.Single(b.Literals);
|
||||
Assert.Equal("c", next.Key);
|
||||
|
||||
var c = next.Value;
|
||||
Assert.Collection(
|
||||
c.Matches,
|
||||
e => Assert.Same(endpoint1, e.Endpoint),
|
||||
e => Assert.Same(endpoint2, e.Endpoint));
|
||||
Assert.Null(c.Parameters);
|
||||
Assert.Empty(c.Literals);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildDfaTree_MultipleEndpoint_LiteralAndCatchAll()
|
||||
{
|
||||
// Arrange
|
||||
var builder = CreateDfaMatcherBuilder();
|
||||
|
||||
var endpoint1 = CreateEndpoint("a/b/c");
|
||||
builder.AddEndpoint(endpoint1);
|
||||
|
||||
var endpoint2 = CreateEndpoint("a/{*b}");
|
||||
builder.AddEndpoint(endpoint2);
|
||||
|
||||
// Act
|
||||
var root = builder.BuildDfaTree();
|
||||
|
||||
// Assert
|
||||
Assert.Empty(root.Matches);
|
||||
Assert.Null(root.Parameters);
|
||||
|
||||
var next = Assert.Single(root.Literals);
|
||||
Assert.Equal("a", next.Key);
|
||||
|
||||
var a = next.Value;
|
||||
Assert.Same(endpoint2, Assert.Single(a.Matches).Endpoint);
|
||||
|
||||
next = Assert.Single(a.Literals);
|
||||
Assert.Equal("b", next.Key);
|
||||
|
||||
var b1 = next.Value;
|
||||
Assert.Same(endpoint2, Assert.Single(a.Matches).Endpoint);
|
||||
Assert.Null(b1.Parameters);
|
||||
|
||||
next = Assert.Single(b1.Literals);
|
||||
Assert.Equal("c", next.Key);
|
||||
|
||||
var c1 = next.Value;
|
||||
Assert.Collection(
|
||||
c1.Matches,
|
||||
e => Assert.Same(endpoint1, e.Endpoint),
|
||||
e => Assert.Same(endpoint2, e.Endpoint));
|
||||
Assert.Null(c1.Parameters);
|
||||
Assert.Empty(c1.Literals);
|
||||
|
||||
var catchAll = a.CatchAll;
|
||||
Assert.Same(endpoint2, Assert.Single(catchAll.Matches).Endpoint);
|
||||
Assert.Same(catchAll, catchAll.Parameters);
|
||||
Assert.Same(catchAll, catchAll.CatchAll);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildDfaTree_MultipleEndpoint_ParameterAndCatchAll()
|
||||
{
|
||||
// Arrange
|
||||
var builder = CreateDfaMatcherBuilder();
|
||||
|
||||
var endpoint1 = CreateEndpoint("a/{b}/c");
|
||||
builder.AddEndpoint(endpoint1);
|
||||
|
||||
var endpoint2 = CreateEndpoint("a/{*b}");
|
||||
builder.AddEndpoint(endpoint2);
|
||||
|
||||
// Act
|
||||
var root = builder.BuildDfaTree();
|
||||
|
||||
// Assert
|
||||
Assert.Empty(root.Matches);
|
||||
Assert.Null(root.Parameters);
|
||||
|
||||
var next = Assert.Single(root.Literals);
|
||||
Assert.Equal("a", next.Key);
|
||||
|
||||
var a = next.Value;
|
||||
Assert.Same(endpoint2, Assert.Single(a.Matches).Endpoint);
|
||||
Assert.Empty(a.Literals);
|
||||
|
||||
var b1 = a.Parameters;
|
||||
Assert.Same(endpoint2, Assert.Single(a.Matches).Endpoint);
|
||||
Assert.Null(b1.Parameters);
|
||||
|
||||
next = Assert.Single(b1.Literals);
|
||||
Assert.Equal("c", next.Key);
|
||||
|
||||
var c1 = next.Value;
|
||||
Assert.Collection(
|
||||
c1.Matches,
|
||||
e => Assert.Same(endpoint1, e.Endpoint),
|
||||
e => Assert.Same(endpoint2, e.Endpoint));
|
||||
Assert.Null(c1.Parameters);
|
||||
Assert.Empty(c1.Literals);
|
||||
|
||||
var catchAll = a.CatchAll;
|
||||
Assert.Same(endpoint2, Assert.Single(catchAll.Matches).Endpoint);
|
||||
Assert.Same(catchAll, catchAll.Parameters);
|
||||
Assert.Same(catchAll, catchAll.CatchAll);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateCandidate_JustLiterals()
|
||||
{
|
||||
// Arrange
|
||||
var endpoint = CreateEndpoint("/a/b/c");
|
||||
|
||||
var builder = CreateDfaMatcherBuilder();
|
||||
|
||||
// Act
|
||||
var candidate = builder.CreateCandidate(new MatcherBuilderEntry(endpoint));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(Candidate.CandidateFlags.None, candidate.Flags);
|
||||
Assert.Empty(candidate.Slots);
|
||||
Assert.Empty(candidate.Captures);
|
||||
Assert.Equal(default, candidate.CatchAll);
|
||||
Assert.Empty(candidate.ComplexSegments);
|
||||
Assert.Empty(candidate.MatchProcessors);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateCandidate_Parameters()
|
||||
{
|
||||
// Arrange
|
||||
var endpoint = CreateEndpoint("/{a}/{b}/{c}");
|
||||
|
||||
var builder = CreateDfaMatcherBuilder();
|
||||
|
||||
// Act
|
||||
var candidate = builder.CreateCandidate(new MatcherBuilderEntry(endpoint));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(Candidate.CandidateFlags.HasCaptures, candidate.Flags);
|
||||
Assert.Equal(3, candidate.Slots.Length);
|
||||
Assert.Collection(
|
||||
candidate.Captures,
|
||||
c => Assert.Equal(("a", 0, 0), c),
|
||||
c => Assert.Equal(("b", 1, 1), c),
|
||||
c => Assert.Equal(("c", 2, 2), c));
|
||||
Assert.Equal(default, candidate.CatchAll);
|
||||
Assert.Empty(candidate.ComplexSegments);
|
||||
Assert.Empty(candidate.MatchProcessors);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateCandidate_Parameters_WithDefaults()
|
||||
{
|
||||
// Arrange
|
||||
var endpoint = CreateEndpoint("/{a=aa}/{b=bb}/{c=cc}");
|
||||
|
||||
var builder = CreateDfaMatcherBuilder();
|
||||
|
||||
// Act
|
||||
var candidate = builder.CreateCandidate(new MatcherBuilderEntry(endpoint));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(
|
||||
Candidate.CandidateFlags.HasDefaults | Candidate.CandidateFlags.HasCaptures,
|
||||
candidate.Flags);
|
||||
Assert.Collection(
|
||||
candidate.Slots,
|
||||
s => Assert.Equal(new KeyValuePair<string, object>("a", "aa"), s),
|
||||
s => Assert.Equal(new KeyValuePair<string, object>("b", "bb"), s),
|
||||
s => Assert.Equal(new KeyValuePair<string, object>("c", "cc"), s));
|
||||
Assert.Collection(
|
||||
candidate.Captures,
|
||||
c => Assert.Equal(("a", 0, 0), c),
|
||||
c => Assert.Equal(("b", 1, 1), c),
|
||||
c => Assert.Equal(("c", 2, 2), c));
|
||||
Assert.Equal(default, candidate.CatchAll);
|
||||
Assert.Empty(candidate.ComplexSegments);
|
||||
Assert.Empty(candidate.MatchProcessors);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateCandidate_Parameters_CatchAll()
|
||||
{
|
||||
// Arrange
|
||||
var endpoint = CreateEndpoint("/{a}/{b}/{*c=cc}");
|
||||
|
||||
var builder = CreateDfaMatcherBuilder();
|
||||
|
||||
// Act
|
||||
var candidate = builder.CreateCandidate(new MatcherBuilderEntry(endpoint));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(
|
||||
Candidate.CandidateFlags.HasDefaults |
|
||||
Candidate.CandidateFlags.HasCaptures |
|
||||
Candidate.CandidateFlags.HasCatchAll,
|
||||
candidate.Flags);
|
||||
Assert.Collection(
|
||||
candidate.Slots,
|
||||
s => Assert.Equal(new KeyValuePair<string, object>("c", "cc"), s),
|
||||
s => Assert.Equal(new KeyValuePair<string, object>(null, null), s),
|
||||
s => Assert.Equal(new KeyValuePair<string, object>(null, null), s));
|
||||
Assert.Collection(
|
||||
candidate.Captures,
|
||||
c => Assert.Equal(("a", 0, 1), c),
|
||||
c => Assert.Equal(("b", 1, 2), c));
|
||||
Assert.Equal(("c", 2, 0), candidate.CatchAll);
|
||||
Assert.Empty(candidate.ComplexSegments);
|
||||
Assert.Empty(candidate.MatchProcessors);
|
||||
}
|
||||
|
||||
// Defaults are processed first, which affects the slot ordering.
|
||||
[Fact]
|
||||
public void CreateCandidate_Parameters_OutOfLineDefaults()
|
||||
{
|
||||
// Arrange
|
||||
var endpoint = CreateEndpoint("/{a}/{b}/{c=cc}", new { a = "aa", d = "dd", });
|
||||
|
||||
var builder = CreateDfaMatcherBuilder();
|
||||
|
||||
// Act
|
||||
var candidate = builder.CreateCandidate(new MatcherBuilderEntry(endpoint));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(
|
||||
Candidate.CandidateFlags.HasDefaults | Candidate.CandidateFlags.HasCaptures,
|
||||
candidate.Flags);
|
||||
Assert.Collection(
|
||||
candidate.Slots,
|
||||
s => Assert.Equal(new KeyValuePair<string, object>("a", "aa"), s),
|
||||
s => Assert.Equal(new KeyValuePair<string, object>("d", "dd"), s),
|
||||
s => Assert.Equal(new KeyValuePair<string, object>("c", "cc"), s),
|
||||
s => Assert.Equal(new KeyValuePair<string, object>(null, null), s));
|
||||
Assert.Collection(
|
||||
candidate.Captures,
|
||||
c => Assert.Equal(("a", 0, 0), c),
|
||||
c => Assert.Equal(("b", 1, 3), c),
|
||||
c => Assert.Equal(("c", 2, 2), c));
|
||||
Assert.Equal(default, candidate.CatchAll);
|
||||
Assert.Empty(candidate.ComplexSegments);
|
||||
Assert.Empty(candidate.MatchProcessors);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateCandidate_Parameters_ComplexSegments()
|
||||
{
|
||||
// Arrange
|
||||
var endpoint = CreateEndpoint("/{a}-{b=bb}/{c}");
|
||||
|
||||
var builder = CreateDfaMatcherBuilder();
|
||||
|
||||
// Act
|
||||
var candidate = builder.CreateCandidate(new MatcherBuilderEntry(endpoint));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(
|
||||
Candidate.CandidateFlags.HasDefaults |
|
||||
Candidate.CandidateFlags.HasCaptures |
|
||||
Candidate.CandidateFlags.HasComplexSegments,
|
||||
candidate.Flags);
|
||||
Assert.Collection(
|
||||
candidate.Slots,
|
||||
s => Assert.Equal(new KeyValuePair<string, object>("b", "bb"), s),
|
||||
s => Assert.Equal(new KeyValuePair<string, object>(null, null), s));
|
||||
Assert.Collection(
|
||||
candidate.Captures,
|
||||
c => Assert.Equal(("c", 1, 1), c));
|
||||
Assert.Equal(default, candidate.CatchAll);
|
||||
Assert.Collection(
|
||||
candidate.ComplexSegments,
|
||||
s => Assert.Equal(0, s.segmentIndex));
|
||||
Assert.Empty(candidate.MatchProcessors);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateCandidate_MatchProcessors()
|
||||
{
|
||||
// Arrange
|
||||
var endpoint = CreateEndpoint("/a/b/c", matchProcessors: new MatchProcessorReference[]
|
||||
{
|
||||
new MatchProcessorReference("a", new IntRouteConstraint()),
|
||||
});
|
||||
|
||||
var builder = CreateDfaMatcherBuilder();
|
||||
|
||||
// Act
|
||||
var candidate = builder.CreateCandidate(new MatcherBuilderEntry(endpoint));
|
||||
|
||||
// Assert
|
||||
Assert.Equal( Candidate.CandidateFlags.HasMatchProcessors, candidate.Flags);
|
||||
Assert.Empty(candidate.Slots);
|
||||
Assert.Empty(candidate.Captures);
|
||||
Assert.Equal(default, candidate.CatchAll);
|
||||
Assert.Empty(candidate.ComplexSegments);
|
||||
Assert.Single(candidate.MatchProcessors);
|
||||
}
|
||||
|
||||
private static DfaMatcherBuilder CreateDfaMatcherBuilder()
|
||||
{
|
||||
var dataSource = new CompositeEndpointDataSource(Array.Empty<EndpointDataSource>());
|
||||
return new DfaMatcherBuilder(
|
||||
Mock.Of<MatchProcessorFactory>(),
|
||||
new EndpointSelector(
|
||||
dataSource,
|
||||
new EndpointConstraintCache(dataSource, Array.Empty<IEndpointConstraintProvider>()),
|
||||
NullLoggerFactory.Instance));
|
||||
}
|
||||
|
||||
private MatcherEndpoint CreateEndpoint(
|
||||
string template,
|
||||
object defaults = null,
|
||||
IEnumerable<MatchProcessorReference> matchProcessors = null)
|
||||
{
|
||||
matchProcessors = matchProcessors ?? Array.Empty<MatchProcessorReference>();
|
||||
|
||||
return new MatcherEndpoint(
|
||||
MatcherEndpoint.EmptyInvoker,
|
||||
template,
|
||||
new RouteValueDictionary(defaults),
|
||||
new RouteValueDictionary(),
|
||||
matchProcessors.ToList(),
|
||||
0,
|
||||
new EndpointMetadataCollection(Array.Empty<object>()),
|
||||
"test");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +1,39 @@
|
|||
// 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.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
||||
{
|
||||
public class DfaMatcherConformanceTest : MatcherConformanceTest
|
||||
public class DfaMatcherConformanceTest : FullFeaturedMatcherConformanceTest
|
||||
{
|
||||
// See the comments in the base class. DfaMatcher fixes a long-standing bug
|
||||
// with catchall parameters and empty segments.
|
||||
public override async Task Quirks_CatchAllParameter(string template, string path, string[] keys, string[] values)
|
||||
{
|
||||
// Arrange
|
||||
var (matcher, endpoint) = CreateMatcher(template);
|
||||
var (httpContext, feature) = CreateContext(path);
|
||||
|
||||
// Act
|
||||
await matcher.MatchAsync(httpContext, feature);
|
||||
|
||||
// Assert
|
||||
DispatcherAssert.AssertMatch(feature, endpoint, keys, values);
|
||||
}
|
||||
|
||||
internal override Matcher CreateMatcher(params MatcherEndpoint[] endpoints)
|
||||
{
|
||||
var builder = new DfaMatcherBuilder();
|
||||
var services = new ServiceCollection()
|
||||
.AddLogging()
|
||||
.AddOptions()
|
||||
.AddRouting()
|
||||
.AddDispatcher()
|
||||
.AddTransient<DfaMatcherBuilder>()
|
||||
.BuildServiceProvider();
|
||||
|
||||
var builder = services.GetRequiredService<DfaMatcherBuilder>();
|
||||
for (int i = 0; i < endpoints.Length; i++)
|
||||
{
|
||||
builder.AddEndpoint(endpoints[i]);
|
||||
|
|
|
|||
|
|
@ -171,10 +171,14 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
|||
DispatcherAssert.AssertMatch(feature, endpoint, keys, values);
|
||||
}
|
||||
|
||||
// Historically catchall segments don't match an empty segment, but only if it's
|
||||
// the first one. So `/a/b//` would match, but `/a//` would not. This is pretty
|
||||
// wierd and inconsistent with the intent of using a catch all. The DfaMatcher
|
||||
// fixes this issue.
|
||||
[Theory]
|
||||
[InlineData("/{a}/{*b=b}", "/a///")]
|
||||
[InlineData("/{a}/{*b=b}", "/a//c/")]
|
||||
public virtual async Task NotMatch_CatchAllParameter(string template, string path)
|
||||
[InlineData("/{a}/{*b=b}", "/a///", new[] { "a", "b", }, new[] { "a", "//" })]
|
||||
[InlineData("/{a}/{*b=b}", "/a//c/", new[] { "a", "b", }, new[] { "a", "/c/" })]
|
||||
public virtual async Task Quirks_CatchAllParameter(string template, string path, string[] keys, string[] values)
|
||||
{
|
||||
// Arrange
|
||||
var (matcher, endpoint) = CreateMatcher(template);
|
||||
|
|
@ -185,6 +189,11 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
|||
|
||||
// Assert
|
||||
DispatcherAssert.AssertNotMatch(feature);
|
||||
|
||||
// Need to access these to prevent a warning from the xUnit analyzer.
|
||||
// Some of these tests will match (and process the values) and some will not.
|
||||
GC.KeepAlive(keys);
|
||||
GC.KeepAlive(values);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ using System.Linq;
|
|||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Routing.EndpointConstraints;
|
||||
using Microsoft.AspNetCore.Routing.Internal;
|
||||
using Microsoft.AspNetCore.Routing.Template;
|
||||
using Microsoft.AspNetCore.Routing.Tree;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.ObjectPool;
|
||||
|
|
|
|||
Loading…
Reference in New Issue