Fix remaining feature gaps in DfaMatcher (#621)

* Fix remaining feature gaps in DfaMatcher

* addressed minor feedback

* missed one
This commit is contained in:
Ryan Nowak 2018-07-17 19:22:46 -07:00 committed by GitHub
parent 1196349bf4
commit 400d243f42
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 1736 additions and 468 deletions

View File

@ -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;
}
}
}

View File

@ -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();

View File

@ -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>();

View File

@ -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();

View File

@ -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)]

View File

@ -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)]

View File

@ -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)

View File

@ -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();
}

View File

@ -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());

View File

@ -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>

View File

@ -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")]

View File

@ -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;

View File

@ -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,
}
}
}

View File

@ -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;
}
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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();
}
}
}
}

View File

@ -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()})";
}
}
}

View File

@ -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;
}
}
}
}

View File

@ -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;

View File

@ -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")]

View File

@ -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; }

View File

@ -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;
}
}
}

View File

@ -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()})";
}
}
}
}

View File

@ -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();
}
}
}
}

View File

@ -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");
}
}
}

View File

@ -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]);

View File

@ -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]

View File

@ -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;