Implement EndpointSelector and MatcherPolicy (#646)

Implement EndpointSelector and MatcherPolicy

This change makes the EndpointSelector API more concrete, and is the
beggining of removing EndpointConstraint by making it obsolete.

To that end, I'm introducing MatcherPolicy, which is a
feature-collection API for registering policies that interact with the
DfaMatcher. The two policies that we'll need to start are:
- ability to order endpoints
- ability to append 'policy' nodes to the graph

These two concepts together replace EndpointConstraint. Extending our
graph representation is a really efficient way to processing most common
scenarios.

---

In general this helps with common cases where 4 or so endpoints match
the URL, but with different HTTP methods supported on each. Today we
have to process route values and call into some 'policy' to make a
decision about which one is the winner. This change pushes this
knowledge down into the graph so that it's roughly as cheap as a
dictionary lookup, and can be done allocation-free.

The big savings here is ability to remove more candidates *before*
collecting route data.

---

Along with this change, I also built 'rejection' into the DFA node
model, you can see an example with the HTTP Method handling that I
implemented. I implemented a policy that can treat failure to resolve an
HTTP method as a 405 response by returning a failure endpoint. This is
at the heart of much of the feedback we've gotten in this area around
versioning and http method handling. We also have a version of this
feature in MVC for [Consumes].
This commit is contained in:
Ryan Nowak 2018-07-24 17:31:51 -07:00 committed by GitHub
parent f37ca0d2e9
commit 147c9527f3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
49 changed files with 3192 additions and 945 deletions

View File

@ -7,7 +7,7 @@ using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Routing.Matchers
{
public class MatcherSelectCandidatesSingleEntryBenchmark : MatcherBenchmarkBase
public class MatcheFindCandidateSetSingleEntryBenchmark : MatcherBenchmarkBase
{
private TrivialMatcher _baseline;
private DfaMatcher _dfa;
@ -40,9 +40,9 @@ namespace Microsoft.AspNetCore.Routing.Matchers
var path = httpContext.Request.Path.Value;
var segments = new ReadOnlySpan<PathSegment>(Array.Empty<PathSegment>());
var candidates = _baseline.SelectCandidates(path, segments);
var candidates = _baseline.FindCandidateSet(path, segments);
var endpoint = candidates.Candidates[0].Endpoint;
var endpoint = candidates[0].Endpoint;
Validate(Requests[0], Endpoints[0], endpoint);
}
@ -54,9 +54,9 @@ namespace Microsoft.AspNetCore.Routing.Matchers
Span<PathSegment> segments = stackalloc PathSegment[FastPathTokenizer.DefaultSegmentCount];
var count = FastPathTokenizer.Tokenize(path, segments);
var candidates = _dfa.SelectCandidates(path, segments.Slice(0, count));
var candidates = _dfa.FindCandidateSet(httpContext, path, segments.Slice(0, count));
var endpoint = candidates.Candidates[0].Endpoint;
var endpoint = candidates[0].Endpoint;
Validate(Requests[0], Endpoints[0], endpoint);
}
}

View File

@ -8,6 +8,7 @@ using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing.EndpointConstraints;
using Microsoft.AspNetCore.Routing.Metadata;
using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.Extensions.DependencyInjection;

View File

@ -7,7 +7,7 @@ using BenchmarkDotNet.Attributes;
namespace Microsoft.AspNetCore.Routing.Matchers
{
// Generated from https://github.com/Azure/azure-rest-api-specs
public partial class MatcherSelectCandidatesAzureBenchmark : MatcherBenchmarkBase
public partial class MatcherFindCandidateSetAzureBenchmark : MatcherBenchmarkBase
{
private const int SampleCount = 100;
@ -42,9 +42,9 @@ namespace Microsoft.AspNetCore.Routing.Matchers
var path = httpContext.Request.Path.Value;
var segments = new ReadOnlySpan<PathSegment>(Array.Empty<PathSegment>());
var candidates = _baseline.Matchers[sample].SelectCandidates(path, segments);
var candidates = _baseline.Matchers[sample].FindCandidateSet(path, segments);
var endpoint = candidates.Candidates[0].Endpoint;
var endpoint = candidates[0].Endpoint;
Validate(httpContext, Endpoints[sample], endpoint);
}
}
@ -61,9 +61,9 @@ namespace Microsoft.AspNetCore.Routing.Matchers
Span<PathSegment> segments = stackalloc PathSegment[FastPathTokenizer.DefaultSegmentCount];
var count = FastPathTokenizer.Tokenize(path, segments);
var candidates = _dfa.SelectCandidates(path, segments.Slice(0, count));
var candidates = _dfa.FindCandidateSet(httpContext, path, segments.Slice(0, count));
var endpoint = candidates.Candidates[0].Endpoint;
var endpoint = candidates[0].Endpoint;
Validate(httpContext, Endpoints[sample], endpoint);
}
}

View File

@ -7,7 +7,7 @@ using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Routing.Matchers
{
// This code was generated by the Swaggatherer
public partial class MatcherSelectCandidatesAzureBenchmark : MatcherBenchmarkBase
public partial class MatcherFindCandidateSetAzureBenchmark : MatcherBenchmarkBase
{
private const int EndpointCount = 3517;

View File

@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
{
// Generated from https://github.com/APIs-guru/openapi-directory
// Use https://editor2.swagger.io/ to convert from yaml to json-
public partial class MatcherSelectCandidatesGithubBenchmark : MatcherBenchmarkBase
public partial class MatcherFindCandidateSetGithubBenchmark : MatcherBenchmarkBase
{
private BarebonesMatcher _baseline;
private DfaMatcher _dfa;
@ -35,9 +35,9 @@ namespace Microsoft.AspNetCore.Routing.Matchers
var path = httpContext.Request.Path.Value;
var segments = new ReadOnlySpan<PathSegment>(Array.Empty<PathSegment>());
var candidates = _baseline.Matchers[i].SelectCandidates(path, segments);
var candidates = _baseline.Matchers[i].FindCandidateSet(path, segments);
var endpoint = candidates.Candidates[0].Endpoint;
var endpoint = candidates[0].Endpoint;
Validate(httpContext, Endpoints[i], endpoint);
}
}
@ -53,9 +53,9 @@ namespace Microsoft.AspNetCore.Routing.Matchers
Span<PathSegment> segments = stackalloc PathSegment[FastPathTokenizer.DefaultSegmentCount];
var count = FastPathTokenizer.Tokenize(path, segments);
var candidates = _dfa.SelectCandidates(path, segments.Slice(0, count));
var candidates = _dfa.FindCandidateSet(httpContext, path, segments.Slice(0, count));
var endpoint = candidates.Candidates[0].Endpoint;
var endpoint = candidates[0].Endpoint;
Validate(httpContext, Endpoints[i], endpoint);
}
}

View File

@ -7,7 +7,7 @@ using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Routing.Matchers
{
// This code was generated by the Swaggatherer
public partial class MatcherSelectCandidatesGithubBenchmark : MatcherBenchmarkBase
public partial class MatcherFindCandidateSetGithubBenchmark : MatcherBenchmarkBase
{
private const int EndpointCount = 155;

View File

@ -7,7 +7,7 @@ using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Routing.Matchers
{
public class MatcherSelectCandidatesSmallEntryCountBenchmark : MatcherBenchmarkBase
public class MatcherFindCandidateSetSmallEntryCountBenchmark : MatcherBenchmarkBase
{
private TrivialMatcher _baseline;
private DfaMatcher _dfa;
@ -75,9 +75,9 @@ namespace Microsoft.AspNetCore.Routing.Matchers
var segments = new ReadOnlySpan<PathSegment>(Array.Empty<PathSegment>());
var candidates = _baseline.SelectCandidates(path, segments);
var candidates = _baseline.FindCandidateSet(path, segments);
var endpoint = candidates.Candidates[0].Endpoint;
var endpoint = candidates[0].Endpoint;
Validate(Requests[0], Endpoints[9], endpoint);
}
@ -90,9 +90,9 @@ namespace Microsoft.AspNetCore.Routing.Matchers
Span<PathSegment> segments = stackalloc PathSegment[FastPathTokenizer.DefaultSegmentCount];
var count = FastPathTokenizer.Tokenize(path, segments);
var candidates = _dfa.SelectCandidates(path, segments.Slice(0, count));
var candidates = _dfa.FindCandidateSet(httpContext, path, segments.Slice(0, count));
var endpoint = candidates.Candidates[0].Endpoint;
var endpoint = candidates[0].Endpoint;
Validate(Requests[0], Endpoints[9], endpoint);
}
}

View File

@ -13,17 +13,13 @@ namespace Microsoft.AspNetCore.Routing.Matchers
internal sealed class TrivialMatcher : Matcher
{
private readonly MatcherEndpoint _endpoint;
private readonly CandidateSet _candidates;
private readonly Candidate[] _candidates;
public TrivialMatcher(MatcherEndpoint endpoint)
{
_endpoint = endpoint;
_candidates = new CandidateSet(
new Candidate[] { new Candidate(endpoint), },
// Single candidate group that contains one entry.
CandidateSet.MakeGroups(new[] { 1 }));
_candidates = new Candidate[] { new Candidate(endpoint), };
}
public sealed override Task MatchAsync(HttpContext httpContext, IEndpointFeature feature)
@ -49,14 +45,14 @@ namespace Microsoft.AspNetCore.Routing.Matchers
}
// This is here so this can be tested alongside DFA matcher.
internal CandidateSet SelectCandidates(string path, ReadOnlySpan<PathSegment> segments)
internal Candidate[] FindCandidateSet(string path, ReadOnlySpan<PathSegment> segments)
{
if (string.Equals(_endpoint.RoutePattern.RawText, path, StringComparison.OrdinalIgnoreCase))
{
return _candidates;
}
return CandidateSet.Empty;
return Array.Empty<Candidate>();
}
}
}

View File

@ -73,11 +73,13 @@ namespace Microsoft.Extensions.DependencyInjection
services.TryAddSingleton<IEndpointFinder<string>, NameBasedEndpointFinder>();
services.TryAddSingleton<IEndpointFinder<RouteValuesBasedEndpointFinderContext>, RouteValuesBasedEndpointFinder>();
services.TryAddSingleton<LinkGenerator, DefaultLinkGenerator>();
//
// Endpoint Selection
//
services.TryAddSingleton<EndpointSelector>();
services.TryAddSingleton<EndpointSelector, EndpointConstraintEndpointSelector>();
services.TryAddSingleton<EndpointConstraintCache>();
services.TryAddEnumerable(ServiceDescriptor.Singleton<MatcherPolicy, HttpMethodEndpointSelectorPolicy>());
// Will be cached by the EndpointSelector
services.TryAddEnumerable(

View File

@ -1,20 +1,17 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing.Matchers;
using Microsoft.Extensions.Internal;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing.Matchers;
using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Routing.EndpointConstraints
{
internal class EndpointSelector
internal class EndpointConstraintEndpointSelector : EndpointSelector
{
private static readonly IReadOnlyList<Endpoint> EmptyEndpoints = Array.Empty<Endpoint>();
@ -22,7 +19,7 @@ namespace Microsoft.AspNetCore.Routing.EndpointConstraints
private readonly EndpointConstraintCache _endpointConstraintCache;
private readonly ILogger _logger;
public EndpointSelector(
public EndpointConstraintEndpointSelector(
CompositeEndpointDataSource dataSource,
EndpointConstraintCache endpointConstraintCache,
ILoggerFactory loggerFactory)
@ -32,11 +29,19 @@ namespace Microsoft.AspNetCore.Routing.EndpointConstraints
_endpointConstraintCache = endpointConstraintCache;
}
public Endpoint SelectBestCandidate(HttpContext context, IReadOnlyList<Endpoint> candidates)
public override Task SelectAsync(
HttpContext httpContext,
IEndpointFeature feature,
CandidateSet candidates)
{
if (context == null)
if (httpContext == null)
{
throw new ArgumentNullException(nameof(context));
throw new ArgumentNullException(nameof(httpContext));
}
if (feature == null)
{
throw new ArgumentNullException(nameof(feature));
}
if (candidates == null)
@ -44,25 +49,30 @@ namespace Microsoft.AspNetCore.Routing.EndpointConstraints
throw new ArgumentNullException(nameof(candidates));
}
var finalMatches = EvaluateEndpointConstraints(context, candidates);
var finalMatches = EvaluateEndpointConstraints(httpContext, candidates);
if (finalMatches == null || finalMatches.Count == 0)
{
return null;
return Task.CompletedTask;
}
else if (finalMatches.Count == 1)
{
var selectedEndpoint = finalMatches[0];
var endpoint = finalMatches[0].Endpoint;
var values = finalMatches[0].Values;
return selectedEndpoint;
feature.Endpoint = endpoint;
feature.Invoker = (endpoint as MatcherEndpoint)?.Invoker;
feature.Values = values;
return Task.CompletedTask;
}
else
{
var endpointNames = string.Join(
Environment.NewLine,
finalMatches.Select(a => a.DisplayName));
finalMatches.Select(a => a.Endpoint.DisplayName));
Log.MatchAmbiguous(_logger, context, finalMatches);
Log.MatchAmbiguous(_logger, httpContext, finalMatches);
var message = Resources.FormatAmbiguousEndpoints(
Environment.NewLine,
@ -72,31 +82,61 @@ namespace Microsoft.AspNetCore.Routing.EndpointConstraints
}
}
private IReadOnlyList<Endpoint> EvaluateEndpointConstraints(
private IReadOnlyList<EndpointSelectorCandidate> EvaluateEndpointConstraints(
HttpContext context,
IReadOnlyList<Endpoint> endpoints)
CandidateSet candidateSet)
{
var candidates = new List<EndpointSelectorCandidate>();
// Perf: Avoid allocations
for (var i = 0; i < endpoints.Count; i++)
for (var i = 0; i < candidateSet.Count; i++)
{
var endpoint = endpoints[i];
var constraints = _endpointConstraintCache.GetEndpointConstraints(context, endpoint);
candidates.Add(new EndpointSelectorCandidate(endpoint, constraints));
ref var candidate = ref candidateSet[i];
if (candidate.IsValidCandidate)
{
var endpoint = candidate.Endpoint;
var constraints = _endpointConstraintCache.GetEndpointConstraints(context, endpoint);
candidates.Add(new EndpointSelectorCandidate(
endpoint,
candidate.Score,
candidate.Values,
constraints));
}
}
var matches = EvaluateEndpointConstraintsCore(context, candidates, startingOrder: null);
List<Endpoint> results = null;
List<EndpointSelectorCandidate> results = null;
if (matches != null)
{
results = new List<Endpoint>(matches.Count);
// Perf: Avoid allocations
for (var i = 0; i < matches.Count; i++)
results = new List<EndpointSelectorCandidate>(matches.Count);
// We need to disambiguate based on 'score' - take the first value of 'score'
// and then we only copy matches while they have the same score. This accounts
// for a difference in behavior between new routing and old.
switch (matches.Count)
{
var candidate = matches[i];
results.Add(candidate.Endpoint);
case 0:
break;
case 1:
results.Add(matches[0]);
break;
default:
var score = matches[0].Score;
for (var i = 0; i < matches.Count; i++)
{
if (matches[i].Score != score)
{
break;
}
results.Add(matches[i]);
}
break;
}
}
@ -213,11 +253,11 @@ namespace Microsoft.AspNetCore.Routing.EndpointConstraints
new EventId(1, "MatchAmbiguous"),
"Request matched multiple endpoints for request path '{Path}'. Matching endpoints: {AmbiguousEndpoints}");
public static void MatchAmbiguous(ILogger logger, HttpContext httpContext, IEnumerable<Endpoint> endpoints)
public static void MatchAmbiguous(ILogger logger, HttpContext httpContext, IEnumerable<EndpointSelectorCandidate> endpoints)
{
if (logger.IsEnabled(LogLevel.Error))
{
_matchAmbiguous(logger, httpContext.Request.Path, endpoints.Select(e => e.DisplayName), null);
_matchAmbiguous(logger, httpContext.Request.Path, endpoints.Select(e => e.Endpoint.DisplayName), null);
}
}
}

View File

@ -4,10 +4,11 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using Microsoft.AspNetCore.Routing.Metadata;
namespace Microsoft.AspNetCore.Routing.EndpointConstraints
{
public class HttpMethodEndpointConstraint : IEndpointConstraint
public class HttpMethodEndpointConstraint : IEndpointConstraint, IHttpMethodMetadata
{
public static readonly int HttpMethodConstraintOrder = 100;
@ -40,6 +41,8 @@ namespace Microsoft.AspNetCore.Routing.EndpointConstraints
public int Order => HttpMethodConstraintOrder;
IReadOnlyList<string> IHttpMethodMetadata.HttpMethods => _httpMethods;
public virtual bool Accept(EndpointConstraintContext context)
{
if (context == null)

View File

@ -1,10 +1,9 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Http;
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Routing.EndpointConstraints
{
@ -28,9 +27,13 @@ namespace Microsoft.AspNetCore.Routing.EndpointConstraints
{
}
public struct EndpointSelectorCandidate
public readonly struct EndpointSelectorCandidate
{
public EndpointSelectorCandidate(Endpoint endpoint, IReadOnlyList<IEndpointConstraint> constraints)
public EndpointSelectorCandidate(
Endpoint endpoint,
int score,
RouteValueDictionary values,
IReadOnlyList<IEndpointConstraint> constraints)
{
if (endpoint == null)
{
@ -38,11 +41,17 @@ namespace Microsoft.AspNetCore.Routing.EndpointConstraints
}
Endpoint = endpoint;
Score = score;
Values = values;
Constraints = constraints;
}
public Endpoint Endpoint { get; }
public int Score { get; }
public RouteValueDictionary Values { get; }
public IReadOnlyList<IEndpointConstraint> Constraints { get; }
}

View File

@ -34,6 +34,19 @@ namespace Microsoft.AspNetCore.Routing.Matchers
public readonly MatchProcessor[] MatchProcessors;
// Score is a sequential integer value that in determines the priority of an Endpoint.
// Scores are computed within the context of candidate set, and are meaningless when
// applied to endpoints not in the set.
//
// The score concept boils down the system of comparisons done when ordering Endpoints
// to a single value that can be compared easily. This can be defeated by having
// int32.MaxValue + 1 endpoints in a single set, but you would have other problems by
// that point.
//
// Score is not part of the Endpoint itself, because it's contextual based on where
// the endpoint appears. An Endpoint is often be a member of multiple candiate sets.
public readonly int Score;
// Used in tests.
public Candidate(MatcherEndpoint endpoint)
{
@ -44,12 +57,14 @@ namespace Microsoft.AspNetCore.Routing.Matchers
CatchAll = default;
ComplexSegments = Array.Empty<(RoutePatternPathSegment pathSegment, int segmentIndex)>();
MatchProcessors = Array.Empty<MatchProcessor>();
Score = 0;
Flags = CandidateFlags.None;
}
public Candidate(
MatcherEndpoint endpoint,
int score,
KeyValuePair<string, object>[] slots,
(string parameterName, int segmentIndex, int slotIndex)[] captures,
(string parameterName, int segmentIndex, int slotIndex) catchAll,
@ -57,6 +72,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
MatchProcessor[] matchProcessors)
{
Endpoint = endpoint;
Score = score;
Slots = slots;
Captures = captures;
CatchAll = catchAll;

View File

@ -2,56 +2,155 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Runtime.CompilerServices;
namespace Microsoft.AspNetCore.Routing.Matchers
{
internal class CandidateSet
public sealed class CandidateSet
{
public static readonly CandidateSet Empty = new CandidateSet(Array.Empty<Candidate>(), Array.Empty<int>());
// We inline storage for 4 candidates here to avoid allocations in common
// cases. There's no real reason why 4 is important, it just seemed like
// a plausible number.
private CandidateState _state0;
private CandidateState _state1;
private CandidateState _state2;
private CandidateState _state3;
// The array of candidates.
public readonly Candidate[] Candidates;
private CandidateState[] _additionalCandidates;
// The number of groups.
public readonly int GroupCount;
// The array of groups. Groups define a contiguous sets of indices into
// the candidates array.
//
// The groups array always contains N+1 entries where N is the number of groups.
// The extra array entry is there to make indexing easier, so we can lookup the 'end'
// of the last group without branching.
//
// Example:
// Group0: Candidates[0], Candidates[1]
// Group1: Candidates[2], Candidates[3], Candidates[4]
//
// The groups array would look like: { 0, 2, 5, }
public readonly int[] Groups;
public CandidateSet(Candidate[] candidates, int[] groups)
// Provided to make testing possible/easy for someone implementing
// an EndpointSelector.
public CandidateSet(MatcherEndpoint[] endpoints, int[] scores)
{
Candidates = candidates;
Groups = groups;
Count = endpoints.Length;
GroupCount = groups.Length == 0 ? 0 : groups.Length - 1;
switch (endpoints.Length)
{
case 0:
return;
case 1:
_state0 = new CandidateState(endpoints[0], score: scores[0]);
break;
case 2:
_state0 = new CandidateState(endpoints[0], score: scores[0]);
_state1 = new CandidateState(endpoints[1], score: scores[1]);
break;
case 3:
_state0 = new CandidateState(endpoints[0], score: scores[0]);
_state1 = new CandidateState(endpoints[1], score: scores[1]);
_state2 = new CandidateState(endpoints[2], score: scores[2]);
break;
case 4:
_state0 = new CandidateState(endpoints[0], score: scores[0]);
_state1 = new CandidateState(endpoints[1], score: scores[1]);
_state2 = new CandidateState(endpoints[2], score: scores[2]);
_state3 = new CandidateState(endpoints[3], score: scores[3]);
break;
default:
_state0 = new CandidateState(endpoints[0], score: scores[0]);
_state1 = new CandidateState(endpoints[1], score: scores[1]);
_state2 = new CandidateState(endpoints[2], score: scores[2]);
_state3 = new CandidateState(endpoints[3], score: scores[3]);
_additionalCandidates = new CandidateState[endpoints.Length - 4];
for (var i = 4; i < endpoints.Length; i++)
{
_additionalCandidates[i - 4] = new CandidateState(endpoints[i], score: scores[i]);
}
break;
}
}
// See description on Groups.
public static int[] MakeGroups(int[] lengths)
internal CandidateSet(Candidate[] candidates)
{
var groups = new int[lengths.Length + 1];
Count = candidates.Length;
var sum = 0;
for (var i = 0; i < lengths.Length; i++)
switch (candidates.Length)
{
groups[i] = sum;
sum += lengths[i];
case 0:
return;
case 1:
_state0 = new CandidateState(candidates[0].Endpoint, candidates[0].Score);
break;
case 2:
_state0 = new CandidateState(candidates[0].Endpoint, candidates[0].Score);
_state1 = new CandidateState(candidates[1].Endpoint, candidates[1].Score);
break;
case 3:
_state0 = new CandidateState(candidates[0].Endpoint, candidates[0].Score);
_state1 = new CandidateState(candidates[1].Endpoint, candidates[1].Score);
_state2 = new CandidateState(candidates[2].Endpoint, candidates[2].Score);
break;
case 4:
_state0 = new CandidateState(candidates[0].Endpoint, candidates[0].Score);
_state1 = new CandidateState(candidates[1].Endpoint, candidates[1].Score);
_state2 = new CandidateState(candidates[2].Endpoint, candidates[2].Score);
_state3 = new CandidateState(candidates[3].Endpoint, candidates[3].Score);
break;
default:
_state0 = new CandidateState(candidates[0].Endpoint, candidates[0].Score);
_state1 = new CandidateState(candidates[1].Endpoint, candidates[1].Score);
_state2 = new CandidateState(candidates[2].Endpoint, candidates[2].Score);
_state3 = new CandidateState(candidates[3].Endpoint, candidates[3].Score);
_additionalCandidates = new CandidateState[candidates.Length - 4];
for (var i = 4; i < candidates.Length; i++)
{
_additionalCandidates[i - 4] = new CandidateState(candidates[i].Endpoint, candidates[i].Score);
}
break;
}
}
groups[lengths.Length] = sum;
public int Count { get; }
return groups;
// Note that this is a ref-return because of both mutability and performance.
// We don't want to copy these fat structs if it can be avoided.
public ref CandidateState this[int index]
{
// PERF: Force inlining
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
// Friendliness for inlining
if ((uint)index >= Count)
{
ThrowIndexArgumentOutOfRangeException();
}
switch (index)
{
case 0:
return ref _state0;
case 1:
return ref _state1;
case 2:
return ref _state2;
case 3:
return ref _state3;
default:
return ref _additionalCandidates[index - 4];
}
}
}
private static void ThrowIndexArgumentOutOfRangeException()
{
throw new ArgumentOutOfRangeException("index");
}
}
}
}

View File

@ -0,0 +1,31 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.Routing.Matchers
{
public struct CandidateState
{
// Provided for testability
public CandidateState(MatcherEndpoint endpoint)
: this(endpoint, score: 0)
{
}
public CandidateState(MatcherEndpoint endpoint, int score)
{
Endpoint = endpoint;
Score = score;
IsValidCandidate = true;
Values = null;
}
public MatcherEndpoint Endpoint { get; }
public int Score { get; }
public bool IsValidCandidate { get; set; }
public RouteValueDictionary Values { get; set; }
}
}

View File

@ -0,0 +1,86 @@
// 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 System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Routing.Matchers
{
internal class DefaultEndpointSelector : EndpointSelector
{
public override Task SelectAsync(
HttpContext httpContext,
IEndpointFeature feature,
CandidateSet candidates)
{
MatcherEndpoint endpoint = null;
RouteValueDictionary values = null;
int? foundScore = null;
for (var i = 0; i < candidates.Count; i++)
{
ref var state = ref candidates[i];
var isValid = state.IsValidCandidate;
if (isValid && foundScore == null)
{
// This is the first match we've seen - speculatively assign it.
endpoint = state.Endpoint;
values = state.Values;
foundScore = state.Score;
}
else if (isValid && foundScore < state.Score)
{
// This candidate is lower priority than the one we've seen
// so far, we can stop.
//
// Don't worry about the 'null < state.Score' case, it returns false.
break;
}
else if (isValid && foundScore == state.Score)
{
// This is the second match we've found of the same score, so there
// must be an ambiguity.
//
// Don't worry about the 'null == state.Score' case, it returns false.
ReportAmbiguity(candidates);
// Unreachable, ReportAmbiguity always throws.
throw new NotSupportedException();
}
}
if (endpoint != null)
{
feature.Endpoint = endpoint;
feature.Invoker = endpoint.Invoker;
feature.Values = values;
}
return Task.CompletedTask;
}
private static void ReportAmbiguity(CandidateSet candidates)
{
// If we get here it's the result of an ambiguity - we're OK with this
// being a littler slower and more allocatey.
var matches = new List<MatcherEndpoint>();
for (var i = 0; i < candidates.Count; i++)
{
ref var state = ref candidates[i];
if (state.IsValidCandidate)
{
matches.Add(state.Endpoint);
}
}
var message = Resources.FormatAmbiguousEndpoints(
Environment.NewLine,
string.Join(Environment.NewLine, matches.Select(e => e.DisplayName)));
throw new AmbiguousMatchException(message);
}
}
}

View File

@ -2,25 +2,21 @@
// 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 EndpointSelector _selector;
private readonly DfaState[] _states;
public DfaMatcher(EndpointSelector endpointSelector, DfaState[] states)
public DfaMatcher(EndpointSelector selector, DfaState[] states)
{
_endpointSelector = endpointSelector;
_selector = selector;
_states = states;
}
@ -46,117 +42,40 @@ namespace Microsoft.AspNetCore.Routing.Matchers
var count = FastPathTokenizer.Tokenize(path, buffer);
var segments = buffer.Slice(0, count);
// SelectCandidates will process the DFA and return a candidate set. This does
// FindCandidateSet 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)
var candidates = FindCandidateSet(httpContext, path, segments);
if (candidates.Length == 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).
// At this point we have a candidate set, defined as a list of endpoints in
// priority order.
//
// 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,
// Now we'll iterate each endpoint to capture route values, process constraints,
// and process complex segments.
// `candidates` has all of our internal state that we use to process the
// set of endpoints before we call the EndpointSelector.
//
// 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;
// `candidateSet` is the mutable state that we pass to the EndpointSelector.
var candidateSet = new CandidateSet(candidates);
for (var i = 0; i < groups.Length - 1; i++)
for (var i = 0; i < candidates.Length; 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.
// PERF: using ref here to avoid copying around big structs.
//
// 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];
// Reminder!
// candidate: readonly data about the endpoint and how to match
// state: mutable storarge for our processing
ref var candidate = ref candidates[i];
ref var state = ref candidateSet[i];
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;
var flags = candidate.Flags;
// First process all of the parameters and defaults.
RouteValueDictionary values;
@ -170,7 +89,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
//
// We want to create a new array for the route values based on Slots
// as a prototype.
var prototype = group[i].Slots;
var prototype = candidate.Slots;
var slots = new KeyValuePair<string, object>[prototype.Length];
if ((flags & Candidate.CandidateFlags.HasDefaults) != 0)
@ -180,38 +99,62 @@ namespace Microsoft.AspNetCore.Routing.Matchers
if ((flags & Candidate.CandidateFlags.HasCaptures) != 0)
{
ProcessCaptures(slots, group[i].Captures, path, segments);
ProcessCaptures(slots, candidate.Captures, path, segments);
}
if ((flags & Candidate.CandidateFlags.HasCatchAll) != 0)
{
ProcessCatchAll(slots, group[i].CatchAll, path, segments);
{
ProcessCatchAll(slots, candidate.CatchAll, path, segments);
}
values = RouteValueDictionary.FromArray(slots);
}
groupValues[i] = values;
state.Values = 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);
isMatch &= ProcessComplexSegments(candidate.ComplexSegments, path, segments, values);
}
if ((flags & Candidate.CandidateFlags.HasMatchProcessors) != 0)
{
isMatch &= ProcessMatchProcessors(group[i].MatchProcessors, httpContext, values);
isMatch &= ProcessMatchProcessors(candidate.MatchProcessors, httpContext, values);
}
members.Set(i, isMatch);
hasMatch |= isMatch;
state.IsValidCandidate = isMatch;
}
return hasMatch;
return _selector.SelectAsync(httpContext, feature, candidateSet);
}
internal Candidate[] FindCandidateSet(
HttpContext httpContext,
string path,
ReadOnlySpan<PathSegment> segments)
{
var states = _states;
// Process each path segment
var destination = 0;
for (var i = 0; i < segments.Length; i++)
{
destination = states[destination].PathTransitions.GetDestination(path, segments[i]);
}
// Process an arbitrary number of policy-based decisions
var policyTransitions = states[destination].PolicyTransitions;
while (policyTransitions != null)
{
destination = policyTransitions.GetDestination(httpContext);
policyTransitions = states[destination].PolicyTransitions;
}
return states[destination].Candidates;
}
private void ProcessCaptures(

View File

@ -4,32 +4,37 @@
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 List<MatcherEndpoint> _endpoints = new List<MatcherEndpoint>();
private readonly MatchProcessorFactory _matchProcessorFactory;
private readonly EndpointSelector _endpointSelector;
private readonly EndpointSelector _selector;
private readonly MatcherPolicy[] _policies;
private readonly INodeBuilderPolicy[] _nodeBuilders;
private readonly MatcherEndpointComparer _comparer;
public DfaMatcherBuilder(
MatchProcessorFactory matchProcessorFactory,
EndpointSelector endpointSelector)
EndpointSelector selector,
IEnumerable<MatcherPolicy> policies)
{
_matchProcessorFactory = matchProcessorFactory ?? throw new ArgumentNullException(nameof(matchProcessorFactory));
_endpointSelector = endpointSelector ?? throw new ArgumentNullException(nameof(endpointSelector));
_matchProcessorFactory = matchProcessorFactory;
_selector = selector;
_policies = policies.OrderBy(p => p.Order).ToArray();
// Taking care to use _policies, which has been sorted.
_nodeBuilders = _policies.OfType<INodeBuilderPolicy>().ToArray();
_comparer = new MatcherEndpointComparer(_policies.OfType<IEndpointComparerPolicy>().ToArray());
}
public override void AddEndpoint(MatcherEndpoint endpoint)
{
_entries.Add(new MatcherBuilderEntry(endpoint));
_endpoints.Add(endpoint);
}
public DfaNode BuildDfaTree()
@ -38,48 +43,48 @@ namespace Microsoft.AspNetCore.Routing.Matchers
// 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();
_endpoints.Sort(_comparer);
// 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 work = new List<(MatcherEndpoint endpoint, 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++)
for (var i = 0; i < _endpoints.Count; i++)
{
var entry = _entries[i];
maxDepth = Math.Max(maxDepth, entry.RoutePattern.PathSegments.Count);
var endpoint = _endpoints[i];
maxDepth = Math.Max(maxDepth, endpoint.RoutePattern.PathSegments.Count);
work.Add((entry, new List<DfaNode>() { root, }));
work.Add((endpoint, 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)>();
var nextWork = new List<(MatcherEndpoint endpoint, List<DfaNode> parents)>();
for (var i = 0; i < work.Count; i++)
{
var (entry, parents) = work[i];
var (endpoint, parents) = work[i];
if (!HasAdditionalRequiredSegments(entry, depth))
if (!HasAdditionalRequiredSegments(endpoint, depth))
{
for (var j = 0; j < parents.Count; j++)
{
var parent = parents[j];
parent.Matches.Add(entry);
parent.Matches.Add(endpoint);
}
}
// Find the parents of this edge at the current depth
var nextParents = new List<DfaNode>();
var segment = GetCurrentSegment(entry, depth);
var segment = GetCurrentSegment(endpoint, depth);
if (segment == null)
{
continue;
@ -133,7 +138,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
parent.CatchAll.CatchAll = parent.CatchAll;
}
parent.CatchAll.Matches.Add(entry);
parent.CatchAll.Matches.Add(endpoint);
}
else if (segment.IsSimple && part.IsParameter)
{
@ -172,7 +177,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
if (nextParents.Count > 0)
{
nextWork.Add((entry, nextParents));
nextWork.Add((endpoint, nextParents));
}
}
@ -180,22 +185,26 @@ namespace Microsoft.AspNetCore.Routing.Matchers
work = nextWork;
}
// Build the trees of policy nodes (like HTTP methods). Post-order traversal
// means that we won't have infinite recursion.
root.Visit(ApplyPolicies);
return root;
}
private RoutePatternPathSegment GetCurrentSegment(MatcherBuilderEntry entry, int depth)
private RoutePatternPathSegment GetCurrentSegment(MatcherEndpoint endpoint, int depth)
{
if (depth < entry.RoutePattern.PathSegments.Count)
if (depth < endpoint.RoutePattern.PathSegments.Count)
{
return entry.RoutePattern.PathSegments[depth];
return endpoint.RoutePattern.PathSegments[depth];
}
if (entry.RoutePattern.PathSegments.Count == 0)
if (endpoint.RoutePattern.PathSegments.Count == 0)
{
return null;
}
var lastSegment = entry.RoutePattern.PathSegments[entry.RoutePattern.PathSegments.Count - 1];
var lastSegment = endpoint.RoutePattern.PathSegments[endpoint.RoutePattern.PathSegments.Count - 1];
if (lastSegment.IsSimple && lastSegment.Parts[0] is RoutePatternParameterPart parameterPart && parameterPart.IsCatchAll)
{
return lastSegment;
@ -209,46 +218,56 @@ namespace Microsoft.AspNetCore.Routing.Matchers
var root = BuildDfaTree();
var states = new List<DfaState>();
var tables = new List<JumpTableBuilder>();
AddNode(root, states, tables);
var tableBuilders = new List<(JumpTableBuilder pathBuilder, PolicyJumpTableBuilder policyBuilder)>();
AddNode(root, states, tableBuilders);
var exit = states.Count;
states.Add(new DfaState(CandidateSet.Empty, null));
tables.Add(new JumpTableBuilder() { DefaultDestination = exit, ExitDestination = exit, });
states.Add(new DfaState(Array.Empty<Candidate>(), null, null));
tableBuilders.Add((new JumpTableBuilder() { DefaultDestination = exit, ExitDestination = exit, }, null));
for (var i = 0; i < tables.Count; i++)
for (var i = 0; i < tableBuilders.Count; i++)
{
if (tables[i].DefaultDestination == JumpTableBuilder.InvalidDestination)
if (tableBuilders[i].pathBuilder?.DefaultDestination == JumpTableBuilder.InvalidDestination)
{
tables[i].DefaultDestination = exit;
tableBuilders[i].pathBuilder.DefaultDestination = exit;
}
if (tables[i].ExitDestination == JumpTableBuilder.InvalidDestination)
if (tableBuilders[i].pathBuilder?.ExitDestination == JumpTableBuilder.InvalidDestination)
{
tables[i].ExitDestination = exit;
tableBuilders[i].pathBuilder.ExitDestination = exit;
}
if (tableBuilders[i].policyBuilder?.ExitDestination == JumpTableBuilder.InvalidDestination)
{
tableBuilders[i].policyBuilder.ExitDestination = exit;
}
}
for (var i = 0; i < states.Count; i++)
{
states[i] = new DfaState(states[i].Candidates, tables[i].Build());
states[i] = new DfaState(
states[i].Candidates,
tableBuilders[i].pathBuilder?.Build(),
tableBuilders[i].policyBuilder?.Build());
}
return new DfaMatcher(_endpointSelector, states.ToArray());
return new DfaMatcher(_selector, states.ToArray());
}
private int AddNode(DfaNode node, List<DfaState> states, List<JumpTableBuilder> tables)
private int AddNode(
DfaNode node,
List<DfaState> states,
List<(JumpTableBuilder pathBuilder, PolicyJumpTableBuilder policyBuilder)> tableBuilders)
{
node.Matches.Sort();
node.Matches.Sort(_comparer);
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);
var candidates = CreateCandidates(node.Matches);
states.Add(new DfaState(candidates, null, null));
var pathBuilder = new JumpTableBuilder();
tableBuilders.Add((pathBuilder, null));
foreach (var kvp in node.Literals)
{
@ -258,7 +277,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
}
var transition = Transition(kvp.Value);
table.AddEntry(kvp.Key, transition);
pathBuilder.AddEntry(kvp.Key, transition);
}
if (node.Parameters != null &&
@ -267,26 +286,37 @@ namespace Microsoft.AspNetCore.Routing.Matchers
{
// 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;
pathBuilder.DefaultDestination = Transition(node.Parameters);
pathBuilder.ExitDestination = pathBuilder.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);
pathBuilder.DefaultDestination = Transition(node.Parameters);
pathBuilder.ExitDestination = Transition(node.CatchAll);
}
else if (node.Parameters != null)
{
// This node has paramters but no catchall.
table.DefaultDestination = Transition(node.Parameters);
pathBuilder.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;
pathBuilder.DefaultDestination = Transition(node.CatchAll);
pathBuilder.ExitDestination = pathBuilder.DefaultDestination;
}
if (node.PolicyEdges.Count > 0)
{
var policyBuilder = new PolicyJumpTableBuilder(node.NodeBuilder);
tableBuilders[stateIndex] = (pathBuilder, policyBuilder);
foreach (var kvp in node.PolicyEdges)
{
policyBuilder.AddEntry(kvp.Key, Transition(kvp.Value));
}
}
return stateIndex;
@ -294,27 +324,58 @@ namespace Microsoft.AspNetCore.Routing.Matchers
int Transition(DfaNode next)
{
// Break cycles
return ReferenceEquals(node, next) ? stateIndex : AddNode(next, states, tables);
return ReferenceEquals(node, next) ? stateIndex : AddNode(next, states, tableBuilders);
}
}
// Builds an array of candidates for a node, assigns a 'score' for each
// endpoint.
internal Candidate[] CreateCandidates(IReadOnlyList<MatcherEndpoint> endpoints)
{
if (endpoints.Count == 0)
{
return Array.Empty<Candidate>();
}
var candiates = new Candidate[endpoints.Count];
var score = 0;
var examplar = endpoints[0];
candiates[0] = CreateCandidate(examplar, score);
for (var i = 1; i < endpoints.Count; i++)
{
var endpoint = endpoints[i];
if (!_comparer.Equals(examplar, endpoint))
{
// This endpoint doesn't have the same priority.
examplar = endpoint;
score++;
}
candiates[i] = CreateCandidate(endpoint, score);
}
return candiates;
}
// internal for tests
internal Candidate CreateCandidate(MatcherBuilderEntry entry)
internal Candidate CreateCandidate(MatcherEndpoint endpoint, int score)
{
var assignments = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
var slots = new List<KeyValuePair<string, object>>();
var captures = new List<(string parameterName, int segmentIndex, int slotIndex)>();
(string parameterName, int segmentIndex, int slotIndex) catchAll = default;
foreach (var kvp in entry.Endpoint.RoutePattern.Defaults)
foreach (var kvp in endpoint.RoutePattern.Defaults)
{
assignments.Add(kvp.Key, assignments.Count);
slots.Add(kvp);
}
for (var i = 0; i < entry.Endpoint.RoutePattern.PathSegments.Count; i++)
for (var i = 0; i < endpoint.RoutePattern.PathSegments.Count; i++)
{
var segment = entry.Endpoint.RoutePattern.PathSegments[i];
var segment = endpoint.RoutePattern.PathSegments[i];
if (!segment.IsSimple)
{
continue;
@ -346,9 +407,9 @@ namespace Microsoft.AspNetCore.Routing.Matchers
}
var complexSegments = new List<(RoutePatternPathSegment pathSegment, int segmentIndex)>();
for (var i = 0; i < entry.RoutePattern.PathSegments.Count; i++)
for (var i = 0; i < endpoint.RoutePattern.PathSegments.Count; i++)
{
var segment = entry.RoutePattern.PathSegments[i];
var segment = endpoint.RoutePattern.PathSegments[i];
if (segment.IsSimple)
{
continue;
@ -358,9 +419,9 @@ namespace Microsoft.AspNetCore.Routing.Matchers
}
var matchProcessors = new List<MatchProcessor>();
foreach (var kvp in entry.Endpoint.RoutePattern.Constraints)
foreach (var kvp in endpoint.RoutePattern.Constraints)
{
var parameter = entry.Endpoint.RoutePattern.GetParameter(kvp.Key); // may be null, that's ok
var parameter = endpoint.RoutePattern.GetParameter(kvp.Key); // may be null, that's ok
var constraintReferences = kvp.Value;
for (var i = 0; i < constraintReferences.Count; i++)
{
@ -371,7 +432,8 @@ namespace Microsoft.AspNetCore.Routing.Matchers
}
return new Candidate(
entry.Endpoint,
endpoint,
score,
slots.ToArray(),
captures.ToArray(),
catchAll,
@ -393,7 +455,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
for (var i = 1; i < node.Matches.Count; i++)
{
if (!exemplar.PriorityEquals(node.Matches[i]))
if (!_comparer.Equals(exemplar, node.Matches[i]))
{
groups.Add(length);
length = 0;
@ -409,11 +471,11 @@ namespace Microsoft.AspNetCore.Routing.Matchers
return groups.ToArray();
}
private static bool HasAdditionalRequiredSegments(MatcherBuilderEntry entry, int depth)
private static bool HasAdditionalRequiredSegments(MatcherEndpoint endpoint, int depth)
{
for (var i = depth; i < entry.RoutePattern.PathSegments.Count; i++)
for (var i = depth; i < endpoint.RoutePattern.PathSegments.Count; i++)
{
var segment = entry.RoutePattern.PathSegments[i];
var segment = endpoint.RoutePattern.PathSegments[i];
if (!segment.IsSimple)
{
// Complex segments always require more processing
@ -437,5 +499,59 @@ namespace Microsoft.AspNetCore.Routing.Matchers
return false;
}
private void ApplyPolicies(DfaNode node)
{
if (node.Matches.Count == 0)
{
return;
}
// Start with the current node as the root.
var work = new List<DfaNode>() { node, };
for (var i = 0; i < _nodeBuilders.Length; i++)
{
var nodeBuilder = _nodeBuilders[i];
// Build a list of each
var nextWork = new List<DfaNode>();
for (var j = 0; j < work.Count; j++)
{
var parent = work[j];
if (!nodeBuilder.AppliesToNode(parent.Matches))
{
// This node-builder doesn't care about this node, so add it to the list
// to be processed by the next node-builder.
nextWork.Add(parent);
continue;
}
// This node-builder does apply to this node, so we need to create new nodes for each edge,
// and then attach them to the parent.
var edges = nodeBuilder.GetEdges(parent.Matches);
for (var k = 0; k < edges.Count; k++)
{
var edge = edges[k];
var next = new DfaNode();
// TODO: https://github.com/aspnet/Routing/issues/648
next.Matches.AddRange(edge.Endpoints.Cast<MatcherEndpoint>().ToArray());
nextWork.Add(next);
parent.PolicyEdges.Add(edge.State, next);
}
// Associate the node-builder so we can build a jump table later.
parent.NodeBuilder = nodeBuilder;
// The parent no longer has matches, it's not considered a terminal node.
parent.Matches.Clear();
}
work = nextWork;
}
}
}
}

View File

@ -16,7 +16,8 @@ namespace Microsoft.AspNetCore.Routing.Matchers
public DfaNode()
{
Literals = new Dictionary<string, DfaNode>(StringComparer.OrdinalIgnoreCase);
Matches = new List<MatcherBuilderEntry>();
Matches = new List<MatcherEndpoint>();
PolicyEdges = new Dictionary<object, DfaNode>();
}
// The depth of the node. The depth indicates the number of segments
@ -25,8 +26,8 @@ namespace Microsoft.AspNetCore.Routing.Matchers
// Just for diagnostics and debugging
public string Label { get; set; }
public List<MatcherBuilderEntry> Matches { get; }
public List<MatcherEndpoint> Matches { get; }
public Dictionary<string, DfaNode> Literals { get; }
@ -34,6 +35,37 @@ namespace Microsoft.AspNetCore.Routing.Matchers
public DfaNode CatchAll { get; set; }
public INodeBuilderPolicy NodeBuilder { get; set; }
public Dictionary<object, DfaNode> PolicyEdges { get; }
public void Visit(Action<DfaNode> visitor)
{
foreach (var kvp in Literals)
{
kvp.Value.Visit(visitor);
}
// Break cycles
if (Parameters != null && !ReferenceEquals(this, Parameters))
{
Parameters.Visit(visitor);
}
// Break cycles
if (CatchAll != null && !ReferenceEquals(this, CatchAll))
{
CatchAll.Visit(visitor);
}
foreach (var kvp in PolicyEdges)
{
kvp.Value.Visit(visitor);
}
visitor(this);
}
private string DebuggerToString()
{
var builder = new StringBuilder();

View File

@ -8,18 +8,23 @@ namespace Microsoft.AspNetCore.Routing.Matchers
[DebuggerDisplay("{DebuggerToString(),nq}")]
internal readonly struct DfaState
{
public readonly CandidateSet Candidates;
public readonly JumpTable Transitions;
public readonly Candidate[] Candidates;
public readonly JumpTable PathTransitions;
public readonly PolicyJumpTable PolicyTransitions;
public DfaState(CandidateSet candidates, JumpTable transitions)
public DfaState(Candidate[] candidates, JumpTable pathTransitions, PolicyJumpTable policyTransitions)
{
Candidates = candidates;
Transitions = transitions;
PathTransitions = pathTransitions;
PolicyTransitions = policyTransitions;
}
public string DebuggerToString()
{
return $"m: {Candidates.Candidates?.Length ?? 0}, j: ({Transitions?.DebuggerToString()})";
return
$"matches: {Candidates?.Length ?? 0}, " +
$"path: ({PathTransitions?.DebuggerToString()}), " +
$"policy: ({PolicyTransitions?.DebuggerToString()})";
}
}
}

View File

@ -0,0 +1,59 @@
// 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;
namespace Microsoft.AspNetCore.Routing.Matchers
{
public abstract class EndpointMetadataComparer<TMetadata> : IComparer<Endpoint> where TMetadata : class
{
public static readonly EndpointMetadataComparer<TMetadata> Default = new DefaultComparer<TMetadata>();
public int Compare(Endpoint x, Endpoint y)
{
if (x == null)
{
throw new ArgumentNullException(nameof(x));
}
if (y == null)
{
throw new ArgumentNullException(nameof(y));
}
return CompareMetadata(GetMetadata(x), GetMetadata(y));
}
protected virtual TMetadata GetMetadata(Endpoint endpoint)
{
return endpoint.Metadata.GetMetadata<TMetadata>();
}
protected virtual int CompareMetadata(TMetadata x, TMetadata y)
{
// The default policy is that if x endpoint defines TMetadata, and
// y endpoint does not, then x is *more specific* than y. We return
// -1 for this case so that x will come first in the sort order.
if (x == null && y != null)
{
// y is more specific
return 1;
}
else if (x != null && y == null)
{
// x is more specific
return -1;
}
// both endpoints have this metadata, or both do not have it, they have
// the same specificity.
return 0;
}
private class DefaultComparer<T> : EndpointMetadataComparer<T> where T : class
{
}
}
}

View File

@ -0,0 +1,16 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Routing.Matchers
{
public abstract class EndpointSelector
{
public abstract Task SelectAsync(
HttpContext httpContext,
IEndpointFeature feature,
CandidateSet candidates);
}
}

View File

@ -0,0 +1,182 @@
// 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 System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing.Metadata;
using Microsoft.AspNetCore.Routing.Patterns;
namespace Microsoft.AspNetCore.Routing.Matchers
{
public sealed class HttpMethodEndpointSelectorPolicy : MatcherPolicy, IEndpointComparerPolicy, INodeBuilderPolicy
{
// Used in tests
internal const string Http405EndpointDisplayName = "405 HTTP Method Not Supported";
// Used in tests
internal const string AnyMethod = "*";
public IComparer<Endpoint> Comparer => new HttpMethodMetadataEndpointComparer();
// The order value is chosen to be less than 0, so that it comes before naively
// written policies.
public override int Order => -1000;
public bool AppliesToNode(IReadOnlyList<Endpoint> endpoints)
{
if (endpoints == null)
{
throw new ArgumentNullException(nameof(endpoints));
}
for (var i = 0; i < endpoints.Count; i++)
{
if (endpoints[i].Metadata.GetMetadata<IHttpMethodMetadata>()?.HttpMethods.Any() == true)
{
return true;
}
}
return false;
}
public IReadOnlyList<PolicyNodeEdge> GetEdges(IReadOnlyList<Endpoint> endpoints)
{
// The algorithm here is designed to be preserve the order of the endpoints
// while also being relatively simple. Preserving order is important.
var allHttpMethods = endpoints
.SelectMany(e => GetHttpMethods(e))
.Distinct()
.OrderBy(m => m); // Sort for testability
var dictionary = new Dictionary<string, List<Endpoint>>();
foreach (var httpMethod in allHttpMethods)
{
dictionary.Add(httpMethod, new List<Endpoint>());
}
dictionary.Add(AnyMethod, new List<Endpoint>());
for (var i = 0; i < endpoints.Count; i++)
{
var endpoint = endpoints[i];
var httpMethods = GetHttpMethods(endpoint);
if (httpMethods.Count == 0)
{
// This endpoint suports all HTTP methods.
foreach (var kvp in dictionary)
{
kvp.Value.Add(endpoint);
}
continue;
}
for (var j = 0; j < httpMethods.Count; j++)
{
dictionary[httpMethods[j]].Add(endpoint);
}
}
// Adds a very low priority endpoint that will reject the request with
// a 405 if nothing else can handle this verb. This is only done if
// no other actions exist that handle the 'all verbs'.
//
// The rationale for this is that we want to report a 405 if none of
// the supported methods match, but we don't want to report a 405 in a
// case where an application defines an endpoint that handles all verbs, but
// a constraint rejects the request, or a complex segment fails to parse. We
// consider a case like that a 'user input validation' failure rather than
// a semantic violation of HTTP.
//
// This will make 405 much more likely in API-focused applications, and somewhat
// unlikely in a traditional MVC application. That's good.
if (dictionary[AnyMethod].Count == 0)
{
dictionary[AnyMethod].Add(CreateRejectionEndpoint(allHttpMethods));
}
var edges = new List<PolicyNodeEdge>();
foreach (var kvp in dictionary)
{
edges.Add(new PolicyNodeEdge(kvp.Key, kvp.Value));
}
return edges;
IReadOnlyList<string> GetHttpMethods(Endpoint e)
{
return e.Metadata.GetMetadata<IHttpMethodMetadata>()?.HttpMethods ?? Array.Empty<string>();
}
}
public PolicyJumpTable BuildJumpTable(int exitDestination, IReadOnlyList<PolicyJumpTableEdge> edges)
{
var dictionary = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
for (var i = 0; i < edges.Count; i++)
{
// We create this data, so it's safe to cast it to a string.
dictionary.Add((string)edges[i].State, edges[i].Destination);
}
if (dictionary.TryGetValue(AnyMethod, out var matchesAnyVerb))
{
// If we have endpoints that match any HTTP method, use that as the exit.
exitDestination = matchesAnyVerb;
dictionary.Remove(AnyMethod);
}
return new DictionaryPolicyJumpTable(exitDestination, dictionary);
}
private Endpoint CreateRejectionEndpoint(IEnumerable<string> httpMethods)
{
var allow = string.Join(", ", httpMethods);
return new MatcherEndpoint(
(next) => (context) =>
{
context.Response.StatusCode = 405;
context.Response.Headers.Add("Allow", allow);
return Task.CompletedTask;
},
RoutePatternFactory.Parse("/"),
new RouteValueDictionary(),
0,
EndpointMetadataCollection.Empty,
Http405EndpointDisplayName);
}
private class DictionaryPolicyJumpTable : PolicyJumpTable
{
private readonly int _exitDestination;
private readonly Dictionary<string, int> _destinations;
public DictionaryPolicyJumpTable(int exitDestination, Dictionary<string, int> destinations)
{
_exitDestination = exitDestination;
_destinations = destinations;
}
public override int GetDestination(HttpContext httpContext)
{
var httpMethod = httpContext.Request.Method;
return _destinations.TryGetValue(httpMethod, out var destination) ? destination : _exitDestination;
}
}
private class HttpMethodMetadataEndpointComparer : EndpointMetadataComparer<IHttpMethodMetadata>
{
protected override int CompareMetadata(IHttpMethodMetadata x, IHttpMethodMetadata y)
{
// Ignore the metadata if it has an empty list of HTTP methods.
return base.CompareMetadata(
x?.HttpMethods.Count > 0 ? x : null,
y?.HttpMethods.Count > 0 ? y : null);
}
}
}
}

View File

@ -0,0 +1,12 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
namespace Microsoft.AspNetCore.Routing.Matchers
{
public interface IEndpointComparerPolicy
{
IComparer<Endpoint> Comparer { get; }
}
}

View File

@ -0,0 +1,16 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
namespace Microsoft.AspNetCore.Routing.Matchers
{
public interface INodeBuilderPolicy
{
bool AppliesToNode(IReadOnlyList<Endpoint> endpoints);
IReadOnlyList<PolicyNodeEdge> GetEdges(IReadOnlyList<Endpoint> endpoints);
PolicyJumpTable BuildJumpTable(int exitDestination, IReadOnlyList<PolicyJumpTableEdge> edges);
}
}

View File

@ -1,49 +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 Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.AspNetCore.Routing.Template;
namespace Microsoft.AspNetCore.Routing.Matchers
{
internal class MatcherBuilderEntry : IComparable<MatcherBuilderEntry>
{
public MatcherBuilderEntry(MatcherEndpoint endpoint)
{
Endpoint = endpoint;
Precedence = RoutePrecedence.ComputeInbound(endpoint.RoutePattern);
}
public MatcherEndpoint Endpoint { get; }
public int Order => Endpoint.Order;
public RoutePattern RoutePattern => Endpoint.RoutePattern;
public decimal Precedence { get; }
public int CompareTo(MatcherBuilderEntry other)
{
var comparison = Order.CompareTo(other.Order);
if (comparison != 0)
{
return comparison;
}
comparison = Precedence.CompareTo(other.Precedence);
if (comparison != 0)
{
return comparison;
}
return RoutePattern.RawText.CompareTo(other.RoutePattern.RawText);
}
public bool PriorityEquals(MatcherBuilderEntry other)
{
return Order == other.Order && Precedence == other.Precedence;
}
}
}

View File

@ -0,0 +1,103 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Diagnostics;
namespace Microsoft.AspNetCore.Routing.Matchers
{
// Use to sort and group MatcherEndpoints.
//
// NOTE:
// When ordering endpoints, we compare the route templates as an absolute last resort.
// This is used as a factor to ensure that we always have a predictable ordering
// for tests, errors, etc.
//
// When we group endpoints we don't consider the route template, because we're trying
// to group endpoints not separate them.
//
// TLDR:
// IComparer implementation considers the template string as a tiebreaker.
// IEqualityComparer implementation does not.
// This is cool and good.
internal class MatcherEndpointComparer : IComparer<MatcherEndpoint>, IEqualityComparer<MatcherEndpoint>
{
private readonly IComparer<MatcherEndpoint>[] _comparers;
public MatcherEndpointComparer(IEndpointComparerPolicy[] policies)
{
// Order, Precedence, (others)...
_comparers = new IComparer<MatcherEndpoint>[2 + policies.Length];
_comparers[0] = OrderComparer.Instance;
_comparers[1] = PrecedenceComparer.Instance;
for (var i = 0; i < policies.Length; i++)
{
_comparers[i + 2] = policies[i].Comparer;
}
}
public int Compare(MatcherEndpoint x, MatcherEndpoint y)
{
// We don't expose this publicly, and we should never call it on
// a null endpoint.
Debug.Assert(x != null);
Debug.Assert(y != null);
var compare = CompareCore(x, y);
// Since we're sorting, use the route template as a last resort.
return compare == 0 ? x.RoutePattern.RawText.CompareTo(y.RoutePattern.RawText) : compare;
}
public bool Equals(MatcherEndpoint x, MatcherEndpoint y)
{
// We don't expose this publicly, and we should never call it on
// a null endpoint.
Debug.Assert(x != null);
Debug.Assert(y != null);
return CompareCore(x, y) == 0;
}
public int GetHashCode(MatcherEndpoint obj)
{
// This should not be possible to call publicly.
Debug.Fail("We don't expect this to be called.");
throw new System.NotImplementedException();
}
private int CompareCore(MatcherEndpoint x, MatcherEndpoint y)
{
for (var i = 0; i < _comparers.Length; i++)
{
var compare = _comparers[i].Compare(x, y);
if (compare != 0)
{
return compare;
}
}
return 0;
}
private class OrderComparer : IComparer<MatcherEndpoint>
{
public static readonly IComparer<MatcherEndpoint> Instance = new OrderComparer();
public int Compare(MatcherEndpoint x, MatcherEndpoint y)
{
return x.Order.CompareTo(y.Order);
}
}
private class PrecedenceComparer : IComparer<MatcherEndpoint>
{
public static readonly IComparer<MatcherEndpoint> Instance = new PrecedenceComparer();
public int Compare(MatcherEndpoint x, MatcherEndpoint y)
{
return x.RoutePattern.InboundPrecedence.CompareTo(y.RoutePattern.InboundPrecedence);
}
}
}
}

View File

@ -0,0 +1,10 @@
// 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.
namespace Microsoft.AspNetCore.Routing
{
public abstract class MatcherPolicy
{
public abstract int Order { get; }
}
}

View File

@ -0,0 +1,17 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Routing.Matchers
{
public abstract class PolicyJumpTable
{
public abstract int GetDestination(HttpContext httpContext);
internal virtual string DebuggerToString()
{
return GetType().Name;
}
}
}

View File

@ -0,0 +1,32 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
namespace Microsoft.AspNetCore.Routing.Matchers
{
internal class PolicyJumpTableBuilder
{
private readonly INodeBuilderPolicy _nodeBuilder;
private readonly List<PolicyJumpTableEdge> _entries;
public PolicyJumpTableBuilder(INodeBuilderPolicy nodeBuilder)
{
_nodeBuilder = nodeBuilder;
_entries = new List<PolicyJumpTableEdge>();
}
// The destination state for a non-match.
public int ExitDestination { get; set; } = JumpTableBuilder.InvalidDestination;
public void AddEntry(object state, int destination)
{
_entries.Add(new PolicyJumpTableEdge(state, destination));
}
public PolicyJumpTable Build()
{
return _nodeBuilder.BuildJumpTable(ExitDestination, _entries);
}
}
}

View File

@ -0,0 +1,18 @@
// 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.
namespace Microsoft.AspNetCore.Routing.Matchers
{
public readonly struct PolicyJumpTableEdge
{
public PolicyJumpTableEdge(object state, int destination)
{
State = state ?? throw new System.ArgumentNullException(nameof(state));
Destination = destination;
}
public object State { get; }
public int Destination { get; }
}
}

View File

@ -0,0 +1,20 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
namespace Microsoft.AspNetCore.Routing.Matchers
{
public readonly struct PolicyNodeEdge
{
public PolicyNodeEdge(object state, IReadOnlyList<Endpoint> endpoints)
{
State = state ?? throw new System.ArgumentNullException(nameof(state));
Endpoints = endpoints ?? throw new System.ArgumentNullException(nameof(endpoints));
}
public IReadOnlyList<Endpoint> Endpoints { get; }
public object State { get; }
}
}

View File

@ -0,0 +1,24 @@
// 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;
namespace Microsoft.AspNetCore.Routing.Metadata
{
public sealed class HttpMethodMetadata : IHttpMethodMetadata
{
public HttpMethodMetadata(IEnumerable<string> httpMethods)
{
if (httpMethods == null)
{
throw new ArgumentNullException(nameof(httpMethods));
}
HttpMethods = httpMethods.ToArray();
}
public IReadOnlyList<string> HttpMethods { get; }
}
}

View File

@ -0,0 +1,12 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information
using System.Collections.Generic;
namespace Microsoft.AspNetCore.Routing.Metadata
{
public interface IHttpMethodMetadata
{
IReadOnlyList<string> HttpMethods { get; }
}
}

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Microsoft.AspNetCore.Routing.Template;
namespace Microsoft.AspNetCore.Routing.Patterns
{
@ -30,12 +31,19 @@ namespace Microsoft.AspNetCore.Routing.Patterns
Constraints = constraints;
Parameters = parameters;
PathSegments = pathSegments;
InboundPrecedence = RoutePrecedence.ComputeInbound(this);
OutboundPrecedence = RoutePrecedence.ComputeOutbound(this);
}
public IReadOnlyDictionary<string, object> Defaults { get; }
public IReadOnlyDictionary<string, IReadOnlyList<RoutePatternConstraintReference>> Constraints { get; }
public decimal InboundPrecedence { get; }
public decimal OutboundPrecedence { get; }
public string RawText { get; }
public IReadOnlyList<RoutePatternParameterPart> Parameters { get; }

View File

@ -0,0 +1,510 @@
// 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 System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing.Matchers;
using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Logging.Testing;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Routing.EndpointConstraints
{
public class EndpointConstraintEndpointSelectorTest
{
[Fact]
public async Task SelectBestCandidate_MultipleEndpoints_BestMatchSelected()
{
// Arrange
var defaultEndpoint = CreateEndpoint("No constraint endpoint");
var postEndpoint = CreateEndpoint(
"POST constraint endpoint",
new HttpMethodEndpointConstraint(new[] { "POST" }));
var endpoints = new[]
{
defaultEndpoint,
postEndpoint
};
var selector = CreateSelector(endpoints);
var httpContext = new DefaultHttpContext();
httpContext.Request.Method = "POST";
var feature = new EndpointFeature();
// Act
await selector.SelectAsync(httpContext, feature, CreateCandidateSet(endpoints));
// Assert
Assert.Same(postEndpoint, feature.Endpoint);
}
[Fact]
public async Task SelectBestCandidate_MultipleEndpoints_AmbiguousMatchExceptionThrown()
{
// Arrange
var expectedMessage =
"The request matched multiple endpoints. Matches: " + Environment.NewLine +
Environment.NewLine +
"Ambiguous1" + Environment.NewLine +
"Ambiguous2";
var defaultEndpoint1 = CreateEndpoint("Ambiguous1");
var defaultEndpoint2 = CreateEndpoint("Ambiguous2");
var endpoints = new[]
{
defaultEndpoint1,
defaultEndpoint2
};
var selector = CreateSelector(endpoints);
var httpContext = new DefaultHttpContext();
httpContext.Request.Method = "POST";
var feature = new EndpointFeature();
// Act
var ex = await Assert.ThrowsAnyAsync<AmbiguousMatchException>(() =>
{
return selector.SelectAsync(httpContext, feature, CreateCandidateSet(endpoints));
});
// Assert
Assert.Equal(expectedMessage, ex.Message);
}
[Fact]
public async Task SelectBestCandidate_AmbiguousEndpoints_LogIsCorrect()
{
// Arrange
var sink = new TestSink();
var loggerFactory = new TestLoggerFactory(sink, enabled: true);
var endpoints = new[]
{
CreateEndpoint("A1"),
CreateEndpoint("A2"),
};
var selector = CreateSelector(endpoints, loggerFactory);
var httpContext = CreateHttpContext("POST");
var feature = new EndpointFeature();
var names = string.Join(", ", endpoints.Select(action => action.DisplayName));
var expectedMessage =
$"Request matched multiple endpoints for request path '/test'. " +
$"Matching endpoints: {names}";
// Act
await Assert.ThrowsAsync<AmbiguousMatchException>(() =>
{
return selector.SelectAsync(httpContext, feature, CreateCandidateSet(endpoints));
});
// Assert
Assert.Empty(sink.Scopes);
var write = Assert.Single(sink.Writes);
Assert.Equal(expectedMessage, write.State?.ToString());
}
[Fact]
public async Task SelectBestCandidate_PrefersEndpointWithConstraints()
{
// Arrange
var endpointWithConstraint = CreateEndpoint(
"Has constraint",
new HttpMethodEndpointConstraint(new string[] { "POST" }));
var endpointWithoutConstraints = CreateEndpoint("No constraint");
var endpoints = new[] { endpointWithConstraint, endpointWithoutConstraints };
var selector = CreateSelector(endpoints);
var httpContext = CreateHttpContext("POST");
var feature = new EndpointFeature();
// Act
await selector.SelectAsync(httpContext, feature, CreateCandidateSet(endpoints));
// Assert
Assert.Same(endpointWithConstraint, endpointWithConstraint);
}
[Fact]
public async Task SelectBestCandidate_ConstraintsRejectAll()
{
// Arrange
var endpoint1 = CreateEndpoint(
"action1",
new BooleanConstraint() { Pass = false, });
var endpoint2 = CreateEndpoint(
"action2",
new BooleanConstraint() { Pass = false, });
var endpoints = new[] { endpoint1, endpoint2 };
var selector = CreateSelector(endpoints);
var httpContext = CreateHttpContext("POST");
var feature = new EndpointFeature();
// Act
await selector.SelectAsync(httpContext, feature, CreateCandidateSet(endpoints));
// Assert
Assert.Null(feature.Endpoint);
}
[Fact]
public async Task SelectBestCandidate_ConstraintsRejectAll_DifferentStages()
{
// Arrange
var endpoint1 = CreateEndpoint(
"action1",
new BooleanConstraint() { Pass = false, Order = 0 },
new BooleanConstraint() { Pass = true, Order = 1 });
var endpoint2 = CreateEndpoint(
"action2",
new BooleanConstraint() { Pass = true, Order = 0 },
new BooleanConstraint() { Pass = false, Order = 1 });
var endpoints = new[] { endpoint1, endpoint2 };
var selector = CreateSelector(endpoints);
var httpContext = CreateHttpContext("POST");
var feature = new EndpointFeature();
// Act
await selector.SelectAsync(httpContext, feature, CreateCandidateSet(endpoints));
// Assert
Assert.Null(feature.Endpoint);
}
[Fact]
public async Task SelectBestCandidate_EndpointConstraintFactory()
{
// Arrange
var endpointWithConstraints = CreateEndpoint(
"actionWithConstraints",
new ConstraintFactory()
{
Constraint = new BooleanConstraint() { Pass = true },
});
var actionWithoutConstraints = CreateEndpoint("actionWithoutConstraints");
var endpoints = new[] { endpointWithConstraints, actionWithoutConstraints };
var selector = CreateSelector(endpoints);
var httpContext = CreateHttpContext("POST");
var feature = new EndpointFeature();
// Act
await selector.SelectAsync(httpContext, feature, CreateCandidateSet(endpoints));
// Assert
Assert.Same(endpointWithConstraints, feature.Endpoint);
}
[Fact]
public async Task SelectBestCandidate_MultipleCallsNoConstraint_ReturnsEndpoint()
{
// Arrange
var noConstraint = CreateEndpoint("noConstraint");
var endpoints = new[] { noConstraint };
var selector = CreateSelector(endpoints);
var httpContext = CreateHttpContext("POST");
var feature = new EndpointFeature();
// Act
await selector.SelectAsync(httpContext, feature, CreateCandidateSet(endpoints));
var endpoint1 = feature.Endpoint;
await selector.SelectAsync(httpContext, feature, CreateCandidateSet(endpoints));
var endpoint2 = feature.Endpoint;
// Assert
Assert.Same(endpoint1, noConstraint);
Assert.Same(endpoint2, noConstraint);
}
[Fact]
public async Task SelectBestCandidate_MultipleCallsNonConstraintMetadata_ReturnsEndpoint()
{
// Arrange
var noConstraint = CreateEndpoint("noConstraint", new object());
var endpoints = new[] { noConstraint };
var selector = CreateSelector(endpoints);
var httpContext = CreateHttpContext("POST");
var feature = new EndpointFeature();
// Act
await selector.SelectAsync(httpContext, feature, CreateCandidateSet(endpoints));
var endpoint1 = feature.Endpoint;
await selector.SelectAsync(httpContext, feature, CreateCandidateSet(endpoints));
var endpoint2 = feature.Endpoint;
// Assert
Assert.Same(endpoint1, noConstraint);
Assert.Same(endpoint2, noConstraint);
}
[Fact]
public async Task SelectBestCandidate_EndpointConstraintFactory_ReturnsNull()
{
// Arrange
var nullConstraint = CreateEndpoint("nullConstraint", new ConstraintFactory());
var endpoints = new[] { nullConstraint };
var selector = CreateSelector(endpoints);
var httpContext = CreateHttpContext("POST");
var feature = new EndpointFeature();
// Act
await selector.SelectAsync(httpContext, feature, CreateCandidateSet(endpoints));
var endpoint1 = feature.Endpoint;
await selector.SelectAsync(httpContext, feature, CreateCandidateSet(endpoints));
var endpoint2 = feature.Endpoint;
// Assert
Assert.Same(endpoint1, nullConstraint);
Assert.Same(endpoint2, nullConstraint);
}
// There's a custom constraint provider registered that only understands BooleanConstraintMarker
[Fact]
public async Task SelectBestCandidate_CustomProvider()
{
// Arrange
var endpointWithConstraints = CreateEndpoint(
"actionWithConstraints",
new BooleanConstraintMarker() { Pass = true });
var endpointWithoutConstraints = CreateEndpoint("actionWithoutConstraints");
var endpoints = new[] { endpointWithConstraints, endpointWithoutConstraints, };
var selector = CreateSelector(endpoints);
var httpContext = CreateHttpContext("POST");
var feature = new EndpointFeature();
// Act
await selector.SelectAsync(httpContext, feature, CreateCandidateSet(endpoints));
// Assert
Assert.Same(endpointWithConstraints, feature.Endpoint);
}
// Due to ordering of stages, the first action will be better.
[Fact]
public async Task SelectBestCandidate_ConstraintsInOrder()
{
// Arrange
var best = CreateEndpoint("best", new BooleanConstraint() { Pass = true, Order = 0, });
var worst = CreateEndpoint("worst", new BooleanConstraint() { Pass = true, Order = 1, });
var endpoints = new[] { best, worst };
var selector = CreateSelector(endpoints);
var httpContext = CreateHttpContext("POST");
var feature = new EndpointFeature();
// Act
await selector.SelectAsync(httpContext, feature, CreateCandidateSet(endpoints));
// Assert
Assert.Same(best, feature.Endpoint);
}
// Due to ordering of stages, the first action will be better.
[Fact]
public async Task SelectBestCandidate_ConstraintsInOrder_MultipleStages()
{
// Arrange
var best = CreateEndpoint(
"best",
new BooleanConstraint() { Pass = true, Order = 0, },
new BooleanConstraint() { Pass = true, Order = 1, },
new BooleanConstraint() { Pass = true, Order = 2, });
var worst = CreateEndpoint(
"worst",
new BooleanConstraint() { Pass = true, Order = 0, },
new BooleanConstraint() { Pass = true, Order = 1, },
new BooleanConstraint() { Pass = true, Order = 3, });
var endpoints = new[] { best, worst };
var selector = CreateSelector(endpoints);
var httpContext = CreateHttpContext("POST");
var feature = new EndpointFeature();
// Act
await selector.SelectAsync(httpContext, feature, CreateCandidateSet(endpoints));
// Assert
Assert.Same(best, feature.Endpoint);
}
[Fact]
public async Task SelectBestCandidate_Fallback_ToEndpointWithoutConstraints()
{
// Arrange
var nomatch1 = CreateEndpoint(
"nomatch1",
new BooleanConstraint() { Pass = true, Order = 0, },
new BooleanConstraint() { Pass = true, Order = 1, },
new BooleanConstraint() { Pass = false, Order = 2, });
var nomatch2 = CreateEndpoint(
"nomatch2",
new BooleanConstraint() { Pass = true, Order = 0, },
new BooleanConstraint() { Pass = true, Order = 1, },
new BooleanConstraint() { Pass = false, Order = 3, });
var best = CreateEndpoint("best");
var endpoints = new[] { best, nomatch1, nomatch2 };
var selector = CreateSelector(endpoints);
var httpContext = CreateHttpContext("POST");
var feature = new EndpointFeature();
// Act
await selector.SelectAsync(httpContext, feature, CreateCandidateSet(endpoints));
// Assert
Assert.Same(best, feature.Endpoint);
}
private static MatcherEndpoint CreateEndpoint(string displayName, params object[] metadata)
{
return new MatcherEndpoint(
MatcherEndpoint.EmptyInvoker,
RoutePatternFactory.Parse("/"),
new RouteValueDictionary(),
0,
new EndpointMetadataCollection(metadata),
displayName);
}
private static CandidateSet CreateCandidateSet(MatcherEndpoint[] endpoints)
{
var scores = new int[endpoints.Length];
return new CandidateSet(endpoints, scores);
}
private static EndpointSelector CreateSelector(IReadOnlyList<Endpoint> actions, ILoggerFactory loggerFactory = null)
{
loggerFactory = loggerFactory ?? NullLoggerFactory.Instance;
var endpointDataSource = new CompositeEndpointDataSource(new[] { new DefaultEndpointDataSource(actions) });
var actionConstraintProviders = new IEndpointConstraintProvider[] {
new DefaultEndpointConstraintProvider(),
new BooleanConstraintProvider(),
};
return new EndpointConstraintEndpointSelector(
endpointDataSource,
GetEndpointConstraintCache(actionConstraintProviders),
loggerFactory);
}
private static HttpContext CreateHttpContext(string httpMethod)
{
var serviceProvider = new ServiceCollection().BuildServiceProvider();
var httpContext = new Mock<HttpContext>(MockBehavior.Strict);
var request = new Mock<HttpRequest>(MockBehavior.Strict);
request.SetupGet(r => r.Method).Returns(httpMethod);
request.SetupGet(r => r.Path).Returns(new PathString("/test"));
request.SetupGet(r => r.Headers).Returns(new HeaderDictionary());
httpContext.SetupGet(c => c.Request).Returns(request.Object);
httpContext.SetupGet(c => c.RequestServices).Returns(serviceProvider);
return httpContext.Object;
}
private static EndpointConstraintCache GetEndpointConstraintCache(IEndpointConstraintProvider[] actionConstraintProviders = null)
{
return new EndpointConstraintCache(
new CompositeEndpointDataSource(Array.Empty<EndpointDataSource>()),
actionConstraintProviders.AsEnumerable() ?? new List<IEndpointConstraintProvider>());
}
private class BooleanConstraint : IEndpointConstraint
{
public bool Pass { get; set; }
public int Order { get; set; }
public bool Accept(EndpointConstraintContext context)
{
return Pass;
}
}
private class ConstraintFactory : IEndpointConstraintFactory
{
public IEndpointConstraint Constraint { get; set; }
public bool IsReusable => true;
public IEndpointConstraint CreateInstance(IServiceProvider services)
{
return Constraint;
}
}
private class BooleanConstraintMarker : IEndpointConstraintMetadata
{
public bool Pass { get; set; }
}
private class BooleanConstraintProvider : IEndpointConstraintProvider
{
public int Order { get; set; }
public void OnProvidersExecuting(EndpointConstraintProviderContext context)
{
foreach (var item in context.Results)
{
if (item.Metadata is BooleanConstraintMarker marker)
{
Assert.Null(item.Constraint);
item.Constraint = new BooleanConstraint() { Pass = marker.Pass };
}
}
}
public void OnProvidersExecuted(EndpointConstraintProviderContext context)
{
}
}
}
}

View File

@ -1,505 +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.Linq;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing.Matchers;
using Microsoft.AspNetCore.Routing.TestObjects;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Logging.Testing;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Routing.EndpointConstraints
{
public class EndpointSelectorTests
{
[Fact]
public void SelectBestCandidate_MultipleEndpoints_BestMatchSelected()
{
// Arrange
var defaultEndpoint = new TestEndpoint(
EndpointMetadataCollection.Empty,
"No constraint endpoint");
var postEndpoint = new TestEndpoint(
new EndpointMetadataCollection(new object[] { new HttpMethodEndpointConstraint(new[] { "POST" }) }),
"POST constraint endpoint");
var endpoints = new Endpoint[]
{
defaultEndpoint,
postEndpoint
};
var endpointSelector = CreateSelector(endpoints);
var httpContext = new DefaultHttpContext();
httpContext.Request.Method = "POST";
// Act
var bestCandidateEndpoint = endpointSelector.SelectBestCandidate(httpContext, endpoints);
// Assert
Assert.NotNull(postEndpoint);
}
[Fact]
public void SelectBestCandidate_MultipleEndpoints_AmbiguousMatchExceptionThrown()
{
// Arrange
var expectedMessage =
"The request matched multiple endpoints. Matches: " + Environment.NewLine +
Environment.NewLine +
"Ambiguous1" + Environment.NewLine +
"Ambiguous2";
var defaultEndpoint1 = new TestEndpoint(
EndpointMetadataCollection.Empty,
"Ambiguous1");
var defaultEndpoint2 = new TestEndpoint(
EndpointMetadataCollection.Empty,
"Ambiguous2");
var endpoints = new Endpoint[]
{
defaultEndpoint1,
defaultEndpoint2
};
var endpointSelector = CreateSelector(endpoints);
var httpContext = new DefaultHttpContext();
httpContext.Request.Method = "POST";
// Act
var ex = Assert.ThrowsAny<AmbiguousMatchException>(() =>
{
endpointSelector.SelectBestCandidate(httpContext, endpoints);
});
// Assert
Assert.Equal(expectedMessage, ex.Message);
}
[Fact]
public void SelectBestCandidate_AmbiguousEndpoints_LogIsCorrect()
{
// Arrange
var sink = new TestSink();
var loggerFactory = new TestLoggerFactory(sink, enabled: true);
var actions = new Endpoint[]
{
new TestEndpoint(EndpointMetadataCollection.Empty, "A1"),
new TestEndpoint(EndpointMetadataCollection.Empty, "A2"),
};
var selector = CreateSelector(actions, loggerFactory);
var httpContext = CreateHttpContext("POST");
var actionNames = string.Join(", ", actions.Select(action => action.DisplayName));
var expectedMessage = $"Request matched multiple endpoints for request path '/test'. Matching endpoints: {actionNames}";
// Act
Assert.Throws<AmbiguousMatchException>(() => { selector.SelectBestCandidate(httpContext, actions); });
// Assert
Assert.Empty(sink.Scopes);
var write = Assert.Single(sink.Writes);
Assert.Equal(expectedMessage, write.State?.ToString());
}
[Fact]
public void SelectBestCandidate_PrefersEndpointWithConstraints()
{
// Arrange
var actionWithConstraints = new TestEndpoint(
new EndpointMetadataCollection(new[] { new HttpMethodEndpointConstraint(new string[] { "POST" }) }),
"Has constraint");
var actionWithoutConstraints = new TestEndpoint(
EndpointMetadataCollection.Empty,
"No constraint");
var actions = new Endpoint[] { actionWithConstraints, actionWithoutConstraints };
var selector = CreateSelector(actions);
var context = CreateHttpContext("POST");
// Act
var action = selector.SelectBestCandidate(context, actions);
// Assert
Assert.Same(action, actionWithConstraints);
}
[Fact]
public void SelectBestCandidate_ConstraintsRejectAll()
{
// Arrange
var action1 = new TestEndpoint(
new EndpointMetadataCollection(new[] { new BooleanConstraint() { Pass = false, } }),
"action1");
var action2 = new TestEndpoint(
new EndpointMetadataCollection(new[] { new BooleanConstraint() { Pass = false, } }),
"action2");
var actions = new Endpoint[] { action1, action2 };
var selector = CreateSelector(actions);
var context = CreateHttpContext("POST");
// Act
var action = selector.SelectBestCandidate(context, actions);
// Assert
Assert.Null(action);
}
[Fact]
public void SelectBestCandidate_ConstraintsRejectAll_DifferentStages()
{
// Arrange
var action1 = new TestEndpoint(new EndpointMetadataCollection(new[]
{
new BooleanConstraint() { Pass = false, Order = 0 },
new BooleanConstraint() { Pass = true, Order = 1 },
}),
"action1");
var action2 = new TestEndpoint(new EndpointMetadataCollection(new[]
{
new BooleanConstraint() { Pass = true, Order = 0 },
new BooleanConstraint() { Pass = false, Order = 1 },
}),
"action2");
var actions = new Endpoint[] { action1, action2 };
var selector = CreateSelector(actions);
var context = CreateHttpContext("POST");
// Act
var action = selector.SelectBestCandidate(context, actions);
// Assert
Assert.Null(action);
}
[Fact]
public void SelectBestCandidate_EndpointConstraintFactory()
{
// Arrange
var actionWithConstraints = new TestEndpoint(new EndpointMetadataCollection(new[]
{
new ConstraintFactory()
{
Constraint = new BooleanConstraint() { Pass = true },
},
}),
"actionWithConstraints");
var actionWithoutConstraints = new TestEndpoint(
EndpointMetadataCollection.Empty,
"actionWithoutConstraints");
var actions = new Endpoint[] { actionWithConstraints, actionWithoutConstraints };
var selector = CreateSelector(actions);
var context = CreateHttpContext("POST");
// Act
var action = selector.SelectBestCandidate(context, actions);
// Assert
Assert.Same(action, actionWithConstraints);
}
[Fact]
public void SelectBestCandidate_MultipleCallsNoConstraint_ReturnsEndpoint()
{
// Arrange
var noConstraint = new TestEndpoint(EndpointMetadataCollection.Empty, "noConstraint");
var actions = new Endpoint[] { noConstraint };
var selector = CreateSelector(actions);
var context = CreateHttpContext("POST");
// Act
var action1 = selector.SelectBestCandidate(context, actions);
var action2 = selector.SelectBestCandidate(context, actions);
// Assert
Assert.Same(action1, noConstraint);
Assert.Same(action2, noConstraint);
}
[Fact]
public void SelectBestCandidate_MultipleCallsNonConstraintMetadata_ReturnsEndpoint()
{
// Arrange
var noConstraint = new TestEndpoint(new EndpointMetadataCollection(new[]
{
new object(),
}),
"noConstraint");
var actions = new Endpoint[] { noConstraint };
var selector = CreateSelector(actions);
var context = CreateHttpContext("POST");
// Act
var action1 = selector.SelectBestCandidate(context, actions);
var action2 = selector.SelectBestCandidate(context, actions);
// Assert
Assert.Same(action1, noConstraint);
Assert.Same(action2, noConstraint);
}
[Fact]
public void SelectBestCandidate_EndpointConstraintFactory_ReturnsNull()
{
// Arrange
var nullConstraint = new TestEndpoint(new EndpointMetadataCollection(new[]
{
new ConstraintFactory(),
}),
"nullConstraint");
var actions = new Endpoint[] { nullConstraint };
var selector = CreateSelector(actions);
var context = CreateHttpContext("POST");
// Act
var action1 = selector.SelectBestCandidate(context, actions);
var action2 = selector.SelectBestCandidate(context, actions);
// Assert
Assert.Same(action1, nullConstraint);
Assert.Same(action2, nullConstraint);
}
// There's a custom constraint provider registered that only understands BooleanConstraintMarker
[Fact]
public void SelectBestCandidate_CustomProvider()
{
// Arrange
var actionWithConstraints = new TestEndpoint(new EndpointMetadataCollection(new[]
{
new BooleanConstraintMarker() { Pass = true },
}),
"actionWithConstraints");
var actionWithoutConstraints = new TestEndpoint(
EndpointMetadataCollection.Empty,
"actionWithoutConstraints");
var actions = new Endpoint[] { actionWithConstraints, actionWithoutConstraints, };
var selector = CreateSelector(actions);
var context = CreateHttpContext("POST");
// Act
var action = selector.SelectBestCandidate(context, actions);
// Assert
Assert.Same(action, actionWithConstraints);
}
// Due to ordering of stages, the first action will be better.
[Fact]
public void SelectBestCandidate_ConstraintsInOrder()
{
// Arrange
var best = new TestEndpoint(new EndpointMetadataCollection(new[]
{
new BooleanConstraint() { Pass = true, Order = 0, },
}),
"best");
var worst = new TestEndpoint(new EndpointMetadataCollection(new[]
{
new BooleanConstraint() { Pass = true, Order = 1, },
}),
"worst");
var actions = new Endpoint[] { best, worst };
var selector = CreateSelector(actions);
var context = CreateHttpContext("POST");
// Act
var action = selector.SelectBestCandidate(context, actions);
// Assert
Assert.Same(action, best);
}
// Due to ordering of stages, the first action will be better.
[Fact]
public void SelectBestCandidate_ConstraintsInOrder_MultipleStages()
{
// Arrange
var best = new TestEndpoint(new EndpointMetadataCollection(new[]
{
new BooleanConstraint() { Pass = true, Order = 0, },
new BooleanConstraint() { Pass = true, Order = 1, },
new BooleanConstraint() { Pass = true, Order = 2, },
}),
"best");
var worst = new TestEndpoint(new EndpointMetadataCollection(new[]
{
new BooleanConstraint() { Pass = true, Order = 0, },
new BooleanConstraint() { Pass = true, Order = 1, },
new BooleanConstraint() { Pass = true, Order = 3, },
}),
"worst");
var actions = new Endpoint[] { best, worst };
var selector = CreateSelector(actions);
var context = CreateHttpContext("POST");
// Act
var action = selector.SelectBestCandidate(context, actions);
// Assert
Assert.Same(action, best);
}
[Fact]
public void SelectBestCandidate_Fallback_ToEndpointWithoutConstraints()
{
// Arrange
var nomatch1 = new TestEndpoint(new EndpointMetadataCollection(new[]
{
new BooleanConstraint() { Pass = true, Order = 0, },
new BooleanConstraint() { Pass = true, Order = 1, },
new BooleanConstraint() { Pass = false, Order = 2, },
}),
"nomatch1");
var nomatch2 = new TestEndpoint(new EndpointMetadataCollection(new[]
{
new BooleanConstraint() { Pass = true, Order = 0, },
new BooleanConstraint() { Pass = true, Order = 1, },
new BooleanConstraint() { Pass = false, Order = 3, },
}),
"nomatch2");
var best = new TestEndpoint(EndpointMetadataCollection.Empty, "best");
var actions = new Endpoint[] { best, nomatch1, nomatch2 };
var selector = CreateSelector(actions);
var context = CreateHttpContext("POST");
// Act
var action = selector.SelectBestCandidate(context, actions);
// Assert
Assert.Same(action, best);
}
private static EndpointSelector CreateSelector(IReadOnlyList<Endpoint> actions, ILoggerFactory loggerFactory = null)
{
loggerFactory = loggerFactory ?? NullLoggerFactory.Instance;
var endpointDataSource = new CompositeEndpointDataSource(new[] { new DefaultEndpointDataSource(actions) });
var actionConstraintProviders = new IEndpointConstraintProvider[] {
new DefaultEndpointConstraintProvider(),
new BooleanConstraintProvider(),
};
return new EndpointSelector(
endpointDataSource,
GetEndpointConstraintCache(actionConstraintProviders),
loggerFactory);
}
private static HttpContext CreateHttpContext(string httpMethod)
{
var serviceProvider = new ServiceCollection().BuildServiceProvider();
var httpContext = new Mock<HttpContext>(MockBehavior.Strict);
var request = new Mock<HttpRequest>(MockBehavior.Strict);
request.SetupGet(r => r.Method).Returns(httpMethod);
request.SetupGet(r => r.Path).Returns(new PathString("/test"));
request.SetupGet(r => r.Headers).Returns(new HeaderDictionary());
httpContext.SetupGet(c => c.Request).Returns(request.Object);
httpContext.SetupGet(c => c.RequestServices).Returns(serviceProvider);
return httpContext.Object;
}
private static EndpointConstraintCache GetEndpointConstraintCache(IEndpointConstraintProvider[] actionConstraintProviders = null)
{
return new EndpointConstraintCache(
new CompositeEndpointDataSource(Array.Empty<EndpointDataSource>()),
actionConstraintProviders.AsEnumerable() ?? new List<IEndpointConstraintProvider>());
}
private class BooleanConstraint : IEndpointConstraint
{
public bool Pass { get; set; }
public int Order { get; set; }
public bool Accept(EndpointConstraintContext context)
{
return Pass;
}
}
private class ConstraintFactory : IEndpointConstraintFactory
{
public IEndpointConstraint Constraint { get; set; }
public bool IsReusable => true;
public IEndpointConstraint CreateInstance(IServiceProvider services)
{
return Constraint;
}
}
private class BooleanConstraintMarker : IEndpointConstraintMetadata
{
public bool Pass { get; set; }
}
private class BooleanConstraintProvider : IEndpointConstraintProvider
{
public int Order { get; set; }
public void OnProvidersExecuting(EndpointConstraintProviderContext context)
{
foreach (var item in context.Results)
{
if (item.Metadata is BooleanConstraintMarker marker)
{
Assert.Null(item.Constraint);
item.Constraint = new BooleanConstraint() { Pass = marker.Pass };
}
}
}
public void OnProvidersExecuted(EndpointConstraintProviderContext context)
{
}
}
}
}

View File

@ -63,6 +63,8 @@ namespace Microsoft.AspNetCore.Routing.Internal
var endpointSelectorCandidate = new EndpointSelectorCandidate(
new TestEndpoint(EndpointMetadataCollection.Empty, string.Empty),
0,
new RouteValueDictionary(),
new List<IEndpointConstraint> { constraint });
context.Candidates = new List<EndpointSelectorCandidate> { endpointSelectorCandidate };

View File

@ -49,18 +49,14 @@ namespace Microsoft.AspNetCore.Routing.Matchers
public readonly MatcherEndpoint Endpoint;
private readonly string[] _segments;
private readonly CandidateSet _candidates;
private readonly Candidate[] _candidates;
public InnerMatcher(string[] segments, MatcherEndpoint endpoint)
{
_segments = segments;
Endpoint = endpoint;
_candidates = new CandidateSet(
new Candidate[] { new Candidate(endpoint), },
// Single candidate group that contains one entry.
CandidateSet.MakeGroups(new[] { 1 }));
_candidates = new Candidate[] { new Candidate(endpoint), };
}
public bool TryMatch(string path)
@ -114,14 +110,14 @@ namespace Microsoft.AspNetCore.Routing.Matchers
return segment == _segments.Length;
}
internal CandidateSet SelectCandidates(string path, ReadOnlySpan<PathSegment> segments)
internal Candidate[] FindCandidateSet(string path, ReadOnlySpan<PathSegment> segments)
{
if (TryMatch(path))
{
return _candidates;
}
return CandidateSet.Empty;
return Array.Empty<Candidate>();
}
public override Task MatchAsync(HttpContext httpContext, IEndpointFeature feature)

View File

@ -0,0 +1,103 @@
// 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.Linq;
using Microsoft.AspNetCore.Routing.Patterns;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Routing.Matchers
{
public class CandidateSetTest
{
// We special case low numbers of candidates, so we want to verify that it works correctly for a variety
// of input sizes.
[Theory]
[InlineData(0)]
[InlineData(1)]
[InlineData(2)]
[InlineData(3)]
[InlineData(4)]
[InlineData(5)] // this is the break-point where we start to use a list.
[InlineData(6)]
public void Create_CreatesCandidateSet(int count)
{
// Arrange
var endpoints = new MatcherEndpoint[count];
for (var i = 0; i < endpoints.Length; i++)
{
endpoints[i] = CreateEndpoint($"/{i}");
}
var builder = CreateDfaMatcherBuilder();
var candidates = builder.CreateCandidates(endpoints);
// Act
var candidateSet = new CandidateSet(candidates);
// Assert
for (var i = 0; i < candidateSet.Count; i++)
{
ref var state = ref candidateSet[i];
Assert.True(state.IsValidCandidate);
Assert.Same(endpoints[i], state.Endpoint);
Assert.Equal(candidates[i].Score, state.Score);
Assert.Null(state.Values);
}
}
// We special case low numbers of candidates, so we want to verify that it works correctly for a variety
// of input sizes.
[Theory]
[InlineData(0)]
[InlineData(1)]
[InlineData(2)]
[InlineData(3)]
[InlineData(4)]
[InlineData(5)] // this is the break-point where we start to use a list.
[InlineData(6)]
public void Create_CreatesCandidateSet_TestConstructor(int count)
{
// Arrange
var endpoints = new MatcherEndpoint[count];
for (var i = 0; i < endpoints.Length; i++)
{
endpoints[i] = CreateEndpoint($"/{i}");
}
// Act
var candidateSet = new CandidateSet(endpoints, Enumerable.Range(0, count).ToArray());
// Assert
for (var i = 0; i < candidateSet.Count; i++)
{
ref var state = ref candidateSet[i];
Assert.True(state.IsValidCandidate);
Assert.Same(endpoints[i], state.Endpoint);
Assert.Equal(i, state.Score);
Assert.Null(state.Values);
}
}
private MatcherEndpoint CreateEndpoint(string template)
{
return new MatcherEndpoint(
MatcherEndpoint.EmptyInvoker,
RoutePatternFactory.Parse(template),
new RouteValueDictionary(),
0,
EndpointMetadataCollection.Empty,
"test");
}
private static DfaMatcherBuilder CreateDfaMatcherBuilder(params MatcherPolicy[] policies)
{
var dataSource = new CompositeEndpointDataSource(Array.Empty<EndpointDataSource>());
return new DfaMatcherBuilder(
Mock.Of<MatchProcessorFactory>(),
Mock.Of<EndpointSelector>(),
policies);
}
}
}

View File

@ -0,0 +1,203 @@
// 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.AspNetCore.Http;
using Microsoft.AspNetCore.Routing.Patterns;
using Xunit;
namespace Microsoft.AspNetCore.Routing.Matchers
{
public class DefaultEndpointSelectorTest
{
[Fact]
public async Task SelectAsync_NoCandidates_DoesNothing()
{
// Arrange
var endpoints = new MatcherEndpoint[] { };
var scores = new int[] { };
var candidateSet = CreateCandidateSet(endpoints, scores);
var (httpContext, feature) = CreateContext();
var selector = CreateSelector();
// Act
await selector.SelectAsync(httpContext, feature, candidateSet);
// Assert
Assert.Null(feature.Endpoint);
}
[Fact]
public async Task SelectAsync_NoValidCandidates_DoesNothing()
{
// Arrange
var endpoints = new MatcherEndpoint[] { CreateEndpoint("/test"), };
var scores = new int[] { 0, };
var candidateSet = CreateCandidateSet(endpoints, scores);
candidateSet[0].Values = new RouteValueDictionary();
candidateSet[0].IsValidCandidate = false;
var (httpContext, feature) = CreateContext();
var selector = CreateSelector();
// Act
await selector.SelectAsync(httpContext, feature, candidateSet);
// Assert
Assert.Null(feature.Endpoint);
Assert.Null(feature.Values);
}
[Fact]
public async Task SelectAsync_SingleCandidate_ChoosesCandidate()
{
// Arrange
var endpoints = new MatcherEndpoint[] { CreateEndpoint("/test"), };
var scores = new int[] { 0, };
var candidateSet = CreateCandidateSet(endpoints, scores);
candidateSet[0].Values = new RouteValueDictionary();
candidateSet[0].IsValidCandidate = true;
var (httpContext, feature) = CreateContext();
var selector = CreateSelector();
// Act
await selector.SelectAsync(httpContext, feature, candidateSet);
// Assert
Assert.Same(endpoints[0], feature.Endpoint);
Assert.Same(endpoints[0].Invoker, feature.Invoker);
Assert.NotNull(feature.Values);
}
[Fact]
public async Task SelectAsync_SingleValidCandidate_ChoosesCandidate()
{
// Arrange
var endpoints = new MatcherEndpoint[] { CreateEndpoint("/test1"), CreateEndpoint("/test2"), };
var scores = new int[] { 0, 0 };
var candidateSet = CreateCandidateSet(endpoints, scores);
candidateSet[0].IsValidCandidate = false;
candidateSet[1].IsValidCandidate = true;
var (httpContext, feature) = CreateContext();
var selector = CreateSelector();
// Act
await selector.SelectAsync(httpContext, feature, candidateSet);
// Assert
Assert.Same(endpoints[1], feature.Endpoint);
}
[Fact]
public async Task SelectAsync_SingleValidCandidateInGroup_ChoosesCandidate()
{
// Arrange
var endpoints = new MatcherEndpoint[] { CreateEndpoint("/test1"), CreateEndpoint("/test2"), CreateEndpoint("/test3"), };
var scores = new int[] { 0, 0, 1 };
var candidateSet = CreateCandidateSet(endpoints, scores);
candidateSet[0].IsValidCandidate = false;
candidateSet[1].IsValidCandidate = true;
candidateSet[2].IsValidCandidate = true;
var (httpContext, feature) = CreateContext();
var selector = CreateSelector();
// Act
await selector.SelectAsync(httpContext, feature, candidateSet);
// Assert
Assert.Same(endpoints[1], feature.Endpoint);
}
[Fact]
public async Task SelectAsync_ManyGroupsLastCandidate_ChoosesCandidate()
{
// Arrange
var endpoints = new MatcherEndpoint[]
{
CreateEndpoint("/test1"),
CreateEndpoint("/test2"),
CreateEndpoint("/test3"),
CreateEndpoint("/test4"),
CreateEndpoint("/test5"),
};
var scores = new int[] { 0, 1, 2, 3, 4 };
var candidateSet = CreateCandidateSet(endpoints, scores);
candidateSet[0].IsValidCandidate = false;
candidateSet[1].IsValidCandidate = false;
candidateSet[2].IsValidCandidate = false;
candidateSet[3].IsValidCandidate = false;
candidateSet[4].IsValidCandidate = true;
var (httpContext, feature) = CreateContext();
var selector = CreateSelector();
// Act
await selector.SelectAsync(httpContext, feature, candidateSet);
// Assert
Assert.Same(endpoints[4], feature.Endpoint);
}
[Fact]
public async Task SelectAsync_MultipleValidCandidatesInGroup_ReportsAmbiguity()
{
// Arrange
var endpoints = new MatcherEndpoint[] { CreateEndpoint("/test1"), CreateEndpoint("/test2"), CreateEndpoint("/test3"), };
var scores = new int[] { 0, 1, 1 };
var candidateSet = CreateCandidateSet(endpoints, scores);
candidateSet[0].IsValidCandidate = false;
candidateSet[1].IsValidCandidate = true;
candidateSet[2].IsValidCandidate = true;
var (httpContext, feature) = CreateContext();
var selector = CreateSelector();
// Act
var ex = await Assert.ThrowsAsync<AmbiguousMatchException>(() => selector.SelectAsync(httpContext, feature, candidateSet));
// Assert
Assert.Equal(
@"The request matched multiple endpoints. Matches:
test: /test2
test: /test3", ex.Message);
Assert.Null(feature.Endpoint);
}
private static (HttpContext httpContext, IEndpointFeature feature) CreateContext()
{
return (new DefaultHttpContext(), new EndpointFeature());
}
private static MatcherEndpoint CreateEndpoint(string template)
{
return new MatcherEndpoint(
MatcherEndpoint.EmptyInvoker,
RoutePatternFactory.Parse(template),
new RouteValueDictionary(),
0,
EndpointMetadataCollection.Empty,
$"test: {template}");
}
private static CandidateSet CreateCandidateSet(MatcherEndpoint[] endpoints, int[] scores)
{
return new CandidateSet(endpoints, scores);
}
private static DefaultEndpointSelector CreateSelector()
{
return new DefaultEndpointSelector();
}
}
}

View File

@ -5,9 +5,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Routing.Constraints;
using Microsoft.AspNetCore.Routing.EndpointConstraints;
using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Xunit;
@ -28,7 +26,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
var root = builder.BuildDfaTree();
// Assert
Assert.Same(endpoint, Assert.Single(root.Matches).Endpoint);
Assert.Same(endpoint, Assert.Single(root.Matches));
Assert.Null(root.Parameters);
Assert.Empty(root.Literals);
}
@ -67,7 +65,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
Assert.Equal("c", next.Key);
var c = next.Value;
Assert.Same(endpoint, Assert.Single(c.Matches).Endpoint);
Assert.Same(endpoint, Assert.Single(c.Matches));
Assert.Null(c.Parameters);
Assert.Empty(c.Literals);
}
@ -97,7 +95,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
Assert.Empty(b.Literals);
var c = b.Parameters;
Assert.Same(endpoint, Assert.Single(c.Matches).Endpoint);
Assert.Same(endpoint, Assert.Single(c.Matches));
Assert.Null(c.Parameters);
Assert.Empty(c.Literals);
}
@ -121,14 +119,14 @@ namespace Microsoft.AspNetCore.Routing.Matchers
var a = root.Parameters;
// The catch all can match a path like '/a'
Assert.Same(endpoint, Assert.Single(a.Matches).Endpoint);
Assert.Same(endpoint, Assert.Single(a.Matches));
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.Same(endpoint, Assert.Single(catchAll.Matches));
Assert.Empty(catchAll.Literals);
Assert.Same(catchAll, catchAll.Parameters);
Assert.Same(catchAll, catchAll.CatchAll);
@ -147,13 +145,13 @@ namespace Microsoft.AspNetCore.Routing.Matchers
var root = builder.BuildDfaTree();
// Assert
Assert.Same(endpoint, Assert.Single(root.Matches).Endpoint);
Assert.Same(endpoint, Assert.Single(root.Matches));
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.Same(endpoint, Assert.Single(catchAll.Matches));
Assert.Empty(catchAll.Literals);
Assert.Same(catchAll, catchAll.Parameters);
}
@ -193,7 +191,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
Assert.Equal("c", next.Key);
var c1 = next.Value;
Assert.Same(endpoint1, Assert.Single(c1.Matches).Endpoint);
Assert.Same(endpoint1, Assert.Single(c1.Matches));
Assert.Null(c1.Parameters);
Assert.Empty(c1.Literals);
@ -205,7 +203,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
Assert.Equal("c", next.Key);
var c2 = next.Value;
Assert.Same(endpoint2, Assert.Single(c2.Matches).Endpoint);
Assert.Same(endpoint2, Assert.Single(c2.Matches));
Assert.Null(c2.Parameters);
Assert.Empty(c2.Literals);
}
@ -248,8 +246,8 @@ namespace Microsoft.AspNetCore.Routing.Matchers
var c1 = next.Value;
Assert.Collection(
c1.Matches,
e => Assert.Same(endpoint1, e.Endpoint),
e => Assert.Same(endpoint2, e.Endpoint));
e => Assert.Same(endpoint1, e),
e => Assert.Same(endpoint2, e));
Assert.Null(c1.Parameters);
Assert.Empty(c1.Literals);
@ -261,7 +259,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
Assert.Equal("c", next.Key);
var c2 = next.Value;
Assert.Same(endpoint2, Assert.Single(c2.Matches).Endpoint);
Assert.Same(endpoint2, Assert.Single(c2.Matches));
Assert.Null(c2.Parameters);
Assert.Empty(c2.Literals);
}
@ -302,8 +300,8 @@ namespace Microsoft.AspNetCore.Routing.Matchers
var c = next.Value;
Assert.Collection(
c.Matches,
e => Assert.Same(endpoint1, e.Endpoint),
e => Assert.Same(endpoint2, e.Endpoint));
e => Assert.Same(endpoint1, e),
e => Assert.Same(endpoint2, e));
Assert.Null(c.Parameters);
Assert.Empty(c.Literals);
}
@ -331,13 +329,13 @@ namespace Microsoft.AspNetCore.Routing.Matchers
Assert.Equal("a", next.Key);
var a = next.Value;
Assert.Same(endpoint2, Assert.Single(a.Matches).Endpoint);
Assert.Same(endpoint2, Assert.Single(a.Matches));
next = Assert.Single(a.Literals);
Assert.Equal("b", next.Key);
var b1 = next.Value;
Assert.Same(endpoint2, Assert.Single(a.Matches).Endpoint);
Assert.Same(endpoint2, Assert.Single(a.Matches));
Assert.Null(b1.Parameters);
next = Assert.Single(b1.Literals);
@ -346,13 +344,13 @@ namespace Microsoft.AspNetCore.Routing.Matchers
var c1 = next.Value;
Assert.Collection(
c1.Matches,
e => Assert.Same(endpoint1, e.Endpoint),
e => Assert.Same(endpoint2, e.Endpoint));
e => Assert.Same(endpoint1, e),
e => Assert.Same(endpoint2, e));
Assert.Null(c1.Parameters);
Assert.Empty(c1.Literals);
var catchAll = a.CatchAll;
Assert.Same(endpoint2, Assert.Single(catchAll.Matches).Endpoint);
Assert.Same(endpoint2, Assert.Single(catchAll.Matches));
Assert.Same(catchAll, catchAll.Parameters);
Assert.Same(catchAll, catchAll.CatchAll);
}
@ -380,11 +378,11 @@ namespace Microsoft.AspNetCore.Routing.Matchers
Assert.Equal("a", next.Key);
var a = next.Value;
Assert.Same(endpoint2, Assert.Single(a.Matches).Endpoint);
Assert.Same(endpoint2, Assert.Single(a.Matches));
Assert.Empty(a.Literals);
var b1 = a.Parameters;
Assert.Same(endpoint2, Assert.Single(a.Matches).Endpoint);
Assert.Same(endpoint2, Assert.Single(a.Matches));
Assert.Null(b1.Parameters);
next = Assert.Single(b1.Literals);
@ -393,17 +391,239 @@ namespace Microsoft.AspNetCore.Routing.Matchers
var c1 = next.Value;
Assert.Collection(
c1.Matches,
e => Assert.Same(endpoint1, e.Endpoint),
e => Assert.Same(endpoint2, e.Endpoint));
e => Assert.Same(endpoint1, e),
e => Assert.Same(endpoint2, e));
Assert.Null(c1.Parameters);
Assert.Empty(c1.Literals);
var catchAll = a.CatchAll;
Assert.Same(endpoint2, Assert.Single(catchAll.Matches).Endpoint);
Assert.Same(endpoint2, Assert.Single(catchAll.Matches));
Assert.Same(catchAll, catchAll.Parameters);
Assert.Same(catchAll, catchAll.CatchAll);
}
[Fact]
public void BuildDfaTree_WithPolicies()
{
// Arrange
var builder = CreateDfaMatcherBuilder(new TestMetadata1MatcherPolicy(), new TestMetadata2MatcherPolicy());
var endpoint1 = CreateEndpoint("/a", metadata: new object[] { new TestMetadata1(0), new TestMetadata2(true), });
builder.AddEndpoint(endpoint1);
// 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.IsType<TestMetadata1MatcherPolicy>(a.NodeBuilder);
Assert.Collection(
a.PolicyEdges.OrderBy(e => e.Key),
e => Assert.Equal(0, e.Key));
var test1_0 = a.PolicyEdges[0];
Assert.Empty(a.Matches);
Assert.IsType<TestMetadata2MatcherPolicy>(test1_0.NodeBuilder);
Assert.Collection(
test1_0.PolicyEdges.OrderBy(e => e.Key),
e => Assert.Equal(true, e.Key));
var test2_true = test1_0.PolicyEdges[true];
Assert.Same(endpoint1, Assert.Single(test2_true.Matches));
Assert.Null(test2_true.NodeBuilder);
Assert.Empty(test2_true.PolicyEdges);
}
[Fact]
public void BuildDfaTree_WithPolicies_AndBranches()
{
// Arrange
var builder = CreateDfaMatcherBuilder(new TestMetadata1MatcherPolicy(), new TestMetadata2MatcherPolicy());
var endpoint1 = CreateEndpoint("/a", metadata: new object[] { new TestMetadata1(0), new TestMetadata2(true), });
builder.AddEndpoint(endpoint1);
var endpoint2 = CreateEndpoint("/a", metadata: new object[] { new TestMetadata1(1), new TestMetadata2(true), });
builder.AddEndpoint(endpoint2);
var endpoint3 = CreateEndpoint("/a", metadata: new object[] { new TestMetadata1(1), new TestMetadata2(false), });
builder.AddEndpoint(endpoint3);
// 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.IsType<TestMetadata1MatcherPolicy>(a.NodeBuilder);
Assert.Collection(
a.PolicyEdges.OrderBy(e => e.Key),
e => Assert.Equal(0, e.Key),
e => Assert.Equal(1, e.Key));
var test1_0 = a.PolicyEdges[0];
Assert.Empty(test1_0.Matches);
Assert.IsType<TestMetadata2MatcherPolicy>(test1_0.NodeBuilder);
Assert.Collection(
test1_0.PolicyEdges.OrderBy(e => e.Key),
e => Assert.Equal(true, e.Key));
var test2_true = test1_0.PolicyEdges[true];
Assert.Same(endpoint1, Assert.Single(test2_true.Matches));
Assert.Null(test2_true.NodeBuilder);
Assert.Empty(test2_true.PolicyEdges);
var test1_1 = a.PolicyEdges[1];
Assert.Empty(test1_1.Matches);
Assert.IsType<TestMetadata2MatcherPolicy>(test1_1.NodeBuilder);
Assert.Collection(
test1_1.PolicyEdges.OrderBy(e => e.Key),
e => Assert.Equal(false, e.Key),
e => Assert.Equal(true, e.Key));
test2_true = test1_1.PolicyEdges[true];
Assert.Same(endpoint2, Assert.Single(test2_true.Matches));
Assert.Null(test2_true.NodeBuilder);
Assert.Empty(test2_true.PolicyEdges);
var test2_false = test1_1.PolicyEdges[false];
Assert.Same(endpoint3, Assert.Single(test2_false.Matches));
Assert.Null(test2_false.NodeBuilder);
Assert.Empty(test2_false.PolicyEdges);
}
[Fact]
public void BuildDfaTree_WithPolicies_AndBranches_FirstPolicySkipped()
{
// Arrange
var builder = CreateDfaMatcherBuilder(new TestMetadata1MatcherPolicy(), new TestMetadata2MatcherPolicy());
var endpoint1 = CreateEndpoint("/a", metadata: new object[] { new TestMetadata2(true), });
builder.AddEndpoint(endpoint1);
var endpoint2 = CreateEndpoint("/a", metadata: new object[] { new TestMetadata2(true), });
builder.AddEndpoint(endpoint2);
var endpoint3 = CreateEndpoint("/a", metadata: new object[] { new TestMetadata2(false), });
builder.AddEndpoint(endpoint3);
// 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.IsType<TestMetadata2MatcherPolicy>(a.NodeBuilder);
Assert.Collection(
a.PolicyEdges.OrderBy(e => e.Key),
e => Assert.Equal(false, e.Key),
e => Assert.Equal(true, e.Key));
var test2_true = a.PolicyEdges[true];
Assert.Equal(new[] { endpoint1, endpoint2, }, test2_true.Matches);
Assert.Null(test2_true.NodeBuilder);
Assert.Empty(test2_true.PolicyEdges);
var test2_false = a.PolicyEdges[false];
Assert.Equal(new[] { endpoint3, }, test2_false.Matches);
Assert.Null(test2_false.NodeBuilder);
Assert.Empty(test2_false.PolicyEdges);
}
[Fact]
public void BuildDfaTree_WithPolicies_AndBranches_SecondSkipped()
{
// Arrange
var builder = CreateDfaMatcherBuilder(new TestMetadata1MatcherPolicy(), new TestMetadata2MatcherPolicy());
var endpoint1 = CreateEndpoint("/a", metadata: new object[] { new TestMetadata1(0), });
builder.AddEndpoint(endpoint1);
var endpoint2 = CreateEndpoint("/a", metadata: new object[] { new TestMetadata1(1), });
builder.AddEndpoint(endpoint2);
var endpoint3 = CreateEndpoint("/a", metadata: new object[] { new TestMetadata1(1), });
builder.AddEndpoint(endpoint3);
// 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.IsType<TestMetadata1MatcherPolicy>(a.NodeBuilder);
Assert.Collection(
a.PolicyEdges.OrderBy(e => e.Key),
e => Assert.Equal(0, e.Key),
e => Assert.Equal(1, e.Key));
var test1_0 = a.PolicyEdges[0];
Assert.Equal(new[] { endpoint1, }, test1_0.Matches);
Assert.Null(test1_0.NodeBuilder);
Assert.Empty(test1_0.PolicyEdges);
var test1_1 = a.PolicyEdges[1];
Assert.Equal(new[] { endpoint2, endpoint3, }, test1_1.Matches);
Assert.Null(test1_1.NodeBuilder);
Assert.Empty(test1_1.PolicyEdges);
}
[Fact]
public void BuildDfaTree_WithPolicies_AndBranches_BothPoliciesSkipped()
{
// Arrange
var builder = CreateDfaMatcherBuilder(new TestMetadata1MatcherPolicy(), new TestMetadata2MatcherPolicy());
var endpoint1 = CreateEndpoint("/a", metadata: new object[] { });
builder.AddEndpoint(endpoint1);
var endpoint2 = CreateEndpoint("/a", metadata: new object[] { });
builder.AddEndpoint(endpoint2);
var endpoint3 = CreateEndpoint("/a", metadata: new object[] { });
builder.AddEndpoint(endpoint3);
// 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.Equal(new[] { endpoint1, endpoint2, endpoint3, }, a.Matches);
Assert.Null(a.NodeBuilder);
Assert.Empty(a.PolicyEdges);
}
[Fact]
public void CreateCandidate_JustLiterals()
{
@ -413,7 +633,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
var builder = CreateDfaMatcherBuilder();
// Act
var candidate = builder.CreateCandidate(new MatcherBuilderEntry(endpoint));
var candidate = builder.CreateCandidate(endpoint, score: 0);
// Assert
Assert.Equal(Candidate.CandidateFlags.None, candidate.Flags);
@ -433,7 +653,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
var builder = CreateDfaMatcherBuilder();
// Act
var candidate = builder.CreateCandidate(new MatcherBuilderEntry(endpoint));
var candidate = builder.CreateCandidate(endpoint, score: 0);
// Assert
Assert.Equal(Candidate.CandidateFlags.HasCaptures, candidate.Flags);
@ -457,7 +677,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
var builder = CreateDfaMatcherBuilder();
// Act
var candidate = builder.CreateCandidate(new MatcherBuilderEntry(endpoint));
var candidate = builder.CreateCandidate(endpoint, score: 0);
// Assert
Assert.Equal(
@ -487,7 +707,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
var builder = CreateDfaMatcherBuilder();
// Act
var candidate = builder.CreateCandidate(new MatcherBuilderEntry(endpoint));
var candidate = builder.CreateCandidate(endpoint, score: 0);
// Assert
Assert.Equal(
@ -519,7 +739,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
var builder = CreateDfaMatcherBuilder();
// Act
var candidate = builder.CreateCandidate(new MatcherBuilderEntry(endpoint));
var candidate = builder.CreateCandidate(endpoint, score: 0);
// Assert
Assert.Equal(
@ -550,7 +770,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
var builder = CreateDfaMatcherBuilder();
// Act
var candidate = builder.CreateCandidate(new MatcherBuilderEntry(endpoint));
var candidate = builder.CreateCandidate(endpoint, score: 0);
// Assert
Assert.Equal(
@ -581,7 +801,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
var builder = CreateDfaMatcherBuilder();
// Act
var candidate = builder.CreateCandidate(new MatcherBuilderEntry(endpoint));
var candidate = builder.CreateCandidate(endpoint, score: 0);
// Assert
Assert.Equal( Candidate.CandidateFlags.HasMatchProcessors, candidate.Flags);
@ -591,30 +811,137 @@ namespace Microsoft.AspNetCore.Routing.Matchers
Assert.Empty(candidate.ComplexSegments);
Assert.Single(candidate.MatchProcessors);
}
[Fact]
public void CreateCandidates_CreatesScoresCorrectly()
{
// Arrange
var endpoints = new[]
{
CreateEndpoint("/a/b/c", constraints: new { a = new IntRouteConstraint(), }, metadata: new object[] { new TestMetadata1(), new TestMetadata2(), }),
CreateEndpoint("/a/b/c", constraints: new { a = new AlphaRouteConstraint(), }, metadata: new object[] { new TestMetadata1(), new TestMetadata2(), }),
CreateEndpoint("/a/b/c", constraints: new { a = new IntRouteConstraint(), }, metadata: new object[] { new TestMetadata1(), }),
CreateEndpoint("/a/b/c", constraints: new { a = new IntRouteConstraint(), }, metadata: new object[] { new TestMetadata2(), }),
CreateEndpoint("/a/b/c", constraints: new { }, metadata: new object[] { }),
CreateEndpoint("/a/b/c", constraints: new { }, metadata: new object[] { }),
};
private static DfaMatcherBuilder CreateDfaMatcherBuilder()
var builder = CreateDfaMatcherBuilder(new TestMetadata1MatcherPolicy(), new TestMetadata2MatcherPolicy());
// Act
var candidates = builder.CreateCandidates(endpoints);
// Assert
Assert.Collection(
candidates,
c => Assert.Equal(0, c.Score),
c => Assert.Equal(0, c.Score),
c => Assert.Equal(1, c.Score),
c => Assert.Equal(2, c.Score),
c => Assert.Equal(3, c.Score),
c => Assert.Equal(3, c.Score));
}
private static DfaMatcherBuilder CreateDfaMatcherBuilder(params MatcherPolicy[] policies)
{
var dataSource = new CompositeEndpointDataSource(Array.Empty<EndpointDataSource>());
return new DfaMatcherBuilder(
Mock.Of<MatchProcessorFactory>(),
new EndpointSelector(
dataSource,
new EndpointConstraintCache(dataSource, Array.Empty<IEndpointConstraintProvider>()),
NullLoggerFactory.Instance));
Mock.Of<EndpointSelector>(),
policies);
}
private MatcherEndpoint CreateEndpoint(
string template,
object defaults = null,
object constraints = null)
object constraints = null,
params object[] metadata)
{
return new MatcherEndpoint(
MatcherEndpoint.EmptyInvoker,
RoutePatternFactory.Parse(template, new RouteValueDictionary(defaults), new RouteValueDictionary(constraints)),
new RouteValueDictionary(),
0,
new EndpointMetadataCollection(Array.Empty<object>()),
new EndpointMetadataCollection(metadata),
"test");
}
private class TestMetadata1
{
public TestMetadata1()
{
}
public TestMetadata1(int state)
{
State = state;
}
public int State { get; set; }
}
private class TestMetadata1MatcherPolicy : MatcherPolicy, IEndpointComparerPolicy, INodeBuilderPolicy
{
public override int Order => 100;
public IComparer<Endpoint> Comparer => EndpointMetadataComparer<TestMetadata1>.Default;
public bool AppliesToNode(IReadOnlyList<Endpoint> endpoints)
{
return endpoints.Any(e => e.Metadata.GetMetadata<TestMetadata1>() != null);
}
public PolicyJumpTable BuildJumpTable(int exitDestination, IReadOnlyList<PolicyJumpTableEdge> edges)
{
throw new NotImplementedException();
}
public IReadOnlyList<PolicyNodeEdge> GetEdges(IReadOnlyList<Endpoint> endpoints)
{
return endpoints
.GroupBy(e => e.Metadata.GetMetadata<TestMetadata1>().State)
.Select(g => new PolicyNodeEdge(g.Key, g.ToArray()))
.ToArray();
}
}
private class TestMetadata2
{
public TestMetadata2()
{
}
public TestMetadata2(bool state)
{
State = state;
}
public bool State { get; set; }
}
private class TestMetadata2MatcherPolicy : MatcherPolicy, IEndpointComparerPolicy, INodeBuilderPolicy
{
public override int Order => 101;
public IComparer<Endpoint> Comparer => EndpointMetadataComparer<TestMetadata2>.Default;
public bool AppliesToNode(IReadOnlyList<Endpoint> endpoints)
{
return endpoints.Any(e => e.Metadata.GetMetadata<TestMetadata2>() != null);
}
public PolicyJumpTable BuildJumpTable(int exitDestination, IReadOnlyList<PolicyJumpTableEdge> edges)
{
throw new NotImplementedException();
}
public IReadOnlyList<PolicyNodeEdge> GetEdges(IReadOnlyList<Endpoint> endpoints)
{
return endpoints
.GroupBy(e => e.Metadata.GetMetadata<TestMetadata2>().State)
.Select(g => new PolicyNodeEdge(g.Key, g.ToArray()))
.ToArray();
}
}
}
}

View File

@ -1,16 +1,12 @@
// 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.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing.EndpointConstraints;
using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Routing.Matchers
@ -51,7 +47,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
CreateEndpoint("/{p:int}", 0)
});
var treeMatcher = CreateDfaMatcher(endpointDataSource);
var matcher = CreateDfaMatcher(endpointDataSource);
var httpContext = new DefaultHttpContext();
httpContext.Request.Path = "/1";
@ -59,7 +55,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
var endpointFeature = new EndpointFeature();
// Act
await treeMatcher.MatchAsync(httpContext, endpointFeature);
await matcher.MatchAsync(httpContext, endpointFeature);
// Assert
Assert.NotNull(endpointFeature.Endpoint);
@ -74,7 +70,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
CreateEndpoint("/{p:int}", 0)
});
var treeMatcher = CreateDfaMatcher(endpointDataSource);
var matcher = CreateDfaMatcher(endpointDataSource);
var httpContext = new DefaultHttpContext();
httpContext.Request.Path = "/One";
@ -82,7 +78,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
var endpointFeature = new EndpointFeature();
// Act
await treeMatcher.MatchAsync(httpContext, endpointFeature);
await matcher.MatchAsync(httpContext, endpointFeature);
// Assert
Assert.Null(endpointFeature.Endpoint);
@ -101,7 +97,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
lowerOrderEndpoint
});
var treeMatcher = CreateDfaMatcher(endpointDataSource);
var matcher = CreateDfaMatcher(endpointDataSource);
var httpContext = new DefaultHttpContext();
httpContext.Request.Path = "/Teams";
@ -109,7 +105,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
var endpointFeature = new EndpointFeature();
// Act
await treeMatcher.MatchAsync(httpContext, endpointFeature);
await matcher.MatchAsync(httpContext, endpointFeature);
// Assert
Assert.Equal(lowerOrderEndpoint, endpointFeature.Endpoint);
@ -131,7 +127,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
endpointWithConstraint
});
var treeMatcher = CreateDfaMatcher(endpointDataSource);
var matcher = CreateDfaMatcher(endpointDataSource);
var httpContext = new DefaultHttpContext();
httpContext.Request.Method = "POST";
@ -140,7 +136,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
var endpointFeature = new EndpointFeature();
// Act
await treeMatcher.MatchAsync(httpContext, endpointFeature);
await matcher.MatchAsync(httpContext, endpointFeature);
// Assert
Assert.Equal(endpointWithConstraint, endpointFeature.Endpoint);

View File

@ -0,0 +1,91 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using Microsoft.AspNetCore.Routing.TestObjects;
using Xunit;
namespace Microsoft.AspNetCore.Routing.Matchers
{
public class EndpointMetadataComparerTest
{
[Fact]
public void Compare_EndpointWithMetadata_MoreSpecific()
{
// Arrange
var endpoint1 = new TestEndpoint(new EndpointMetadataCollection(new object[] { new TestMetadata(), }), "test1");
var endpoint2 = new TestEndpoint(new EndpointMetadataCollection(new object[] { }), "test2");
// Act
var result = EndpointMetadataComparer<TestMetadata>.Default.Compare(endpoint1, endpoint2);
// Assert
Assert.Equal(-1, result);
}
[Fact]
public void Compare_EndpointWithMetadata_ReverseOrder_MoreSpecific()
{
// Arrange
var endpoint1 = new TestEndpoint(new EndpointMetadataCollection(new object[] { }), "test1");
var endpoint2 = new TestEndpoint(new EndpointMetadataCollection(new object[] { new TestMetadata(), }), "test2");
// Act
var result = EndpointMetadataComparer<TestMetadata>.Default.Compare(endpoint1, endpoint2);
// Assert
Assert.Equal(1, result);
}
[Fact]
public void Compare_BothEndpointsWithMetadata_Equal()
{
// Arrange
var endpoint1 = new TestEndpoint(new EndpointMetadataCollection(new object[] { new TestMetadata(), }), "test1");
var endpoint2 = new TestEndpoint(new EndpointMetadataCollection(new object[] { new TestMetadata(), }), "test2");
// Act
var result = EndpointMetadataComparer<TestMetadata>.Default.Compare(endpoint1, endpoint2);
// Assert
Assert.Equal(0, result);
}
[Fact]
public void Compare_BothEndpointsWithoutMetadata_Equal()
{
// Arrange
var endpoint1 = new TestEndpoint(new EndpointMetadataCollection(new object[] { }), "test1");
var endpoint2 = new TestEndpoint(new EndpointMetadataCollection(new object[] { }), "test2");
// Act
var result = EndpointMetadataComparer<TestMetadata>.Default.Compare(endpoint1, endpoint2);
// Assert
Assert.Equal(0, result);
}
[Fact]
public void Sort_EndpointWithMetadata_FirstInList()
{
// Arrange
var endpoint1 = new TestEndpoint(new EndpointMetadataCollection(new object[] { new TestMetadata(), }), "test1");
var endpoint2 = new TestEndpoint(new EndpointMetadataCollection(new object[] { }), "test2");
var list = new List<Endpoint>() { endpoint2, endpoint1, };
// Act
list.Sort(EndpointMetadataComparer<TestMetadata>.Default);
// Assert
Assert.Collection(
list,
e => Assert.Same(endpoint1, e),
e => Assert.Same(endpoint2, e));
}
private class TestMetadata
{
}
}
}

View File

@ -0,0 +1,232 @@
// 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.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing.Metadata;
using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
namespace Microsoft.AspNetCore.Routing.Matchers
{
// End-to-end tests for the HTTP method matching functionality
public class HttpMethodMatcherPolicyIntegrationTest
{
[Fact]
public async Task Match_HttpMethod()
{
// Arrange
var endpoint = CreateEndpoint("/hello", httpMethods: new string[] { "GET", });
var matcher = CreateMatcher(endpoint);
var (httpContext, feature) = CreateContext("/hello", "GET");
// Act
await matcher.MatchAsync(httpContext, feature);
// Assert
MatcherAssert.AssertMatch(feature, endpoint);
}
[Fact]
public async Task Match_HttpMethod_CaseInsensitive()
{
// Arrange
var endpoint = CreateEndpoint("/hello", httpMethods: new string[] { "GeT", });
var matcher = CreateMatcher(endpoint);
var (httpContext, feature) = CreateContext("/hello", "GET");
// Act
await matcher.MatchAsync(httpContext, feature);
// Assert
MatcherAssert.AssertMatch(feature, endpoint);
}
[Fact]
public async Task Match_NoMetadata_MatchesAnyHttpMethod()
{
// Arrange
var endpoint = CreateEndpoint("/hello");
var matcher = CreateMatcher(endpoint);
var (httpContext, feature) = CreateContext("/hello", "GET");
// Act
await matcher.MatchAsync(httpContext, feature);
// Assert
MatcherAssert.AssertMatch(feature, endpoint);
}
[Fact]
public async Task Match_EmptyMethodList_MatchesAnyHttpMethod()
{
// Arrange
var endpoint = CreateEndpoint("/hello", httpMethods: new string[] { });
var matcher = CreateMatcher(endpoint);
var (httpContext, feature) = CreateContext("/hello", "GET");
// Act
await matcher.MatchAsync(httpContext, feature);
// Assert
MatcherAssert.AssertMatch(feature, endpoint);
}
[Fact] // When all of the candidates handles specific verbs, use a 405 endpoint
public async Task NotMatch_HttpMethod_Returns405Endpoint()
{
// Arrange
var endpoint1 = CreateEndpoint("/hello", httpMethods: new string[] { "GET", "PUT" });
var endpoint2 = CreateEndpoint("/hello", httpMethods: new string[] { "DELETE" });
var matcher = CreateMatcher(endpoint1, endpoint2);
var (httpContext, feature) = CreateContext("/hello", "POST");
// Act
await matcher.MatchAsync(httpContext, feature);
// Assert
Assert.NotSame(endpoint1, feature.Endpoint);
Assert.NotSame(endpoint2, feature.Endpoint);
Assert.Same(HttpMethodEndpointSelectorPolicy.Http405EndpointDisplayName, feature.Endpoint.DisplayName);
// Invoke the endpoint
await feature.Invoker((c) => Task.CompletedTask)(httpContext);
Assert.Equal(405, httpContext.Response.StatusCode);
Assert.Equal("DELETE, GET, PUT", httpContext.Response.Headers["Allow"]);
}
[Fact] // When one of the candidates handles all verbs, dont use a 405 endpoint
public async Task NotMatch_HttpMethod_WithAllMethodEndpoint_DoesNotReturn405()
{
// Arrange
var endpoint1 = CreateEndpoint("/{x:int}", httpMethods: new string[] { });
var endpoint2 = CreateEndpoint("/hello", httpMethods: new string[] { "DELETE" });
var matcher = CreateMatcher(endpoint1, endpoint2);
var (httpContext, feature) = CreateContext("/hello", "POST");
// Act
await matcher.MatchAsync(httpContext, feature);
// Assert
MatcherAssert.AssertNotMatch(feature);
}
[Fact]
public async Task Match_EndpointWithHttpMethodPreferred()
{
// Arrange
var endpoint1 = CreateEndpoint("/hello", httpMethods: new string[] { "GET", });
var endpoint2 = CreateEndpoint("/bar");
var matcher = CreateMatcher(endpoint1, endpoint2);
var (httpContext, feature) = CreateContext("/hello", "GET");
// Act
await matcher.MatchAsync(httpContext, feature);
// Assert
MatcherAssert.AssertMatch(feature, endpoint1);
}
[Fact]
public async Task Match_EndpointWithHttpMethodPreferred_EmptyList()
{
// Arrange
var endpoint1 = CreateEndpoint("/hello", httpMethods: new string[] { "GET", });
var endpoint2 = CreateEndpoint("/bar", httpMethods: new string[] { });
var matcher = CreateMatcher(endpoint1, endpoint2);
var (httpContext, feature) = CreateContext("/hello", "GET");
// Act
await matcher.MatchAsync(httpContext, feature);
// Assert
MatcherAssert.AssertMatch(feature, endpoint1);
}
[Fact] // The non-http-method-specific endpoint is part of the same candidate set
public async Task Match_EndpointWithHttpMethodPreferred_FallsBackToNonSpecific()
{
// Arrange
var endpoint1 = CreateEndpoint("/{x}", httpMethods: new string[] { "GET", });
var endpoint2 = CreateEndpoint("/{x}", httpMethods: new string[] { });
var matcher = CreateMatcher(endpoint1, endpoint2);
var (httpContext, feature) = CreateContext("/hello", "POST");
// Act
await matcher.MatchAsync(httpContext, feature);
// Assert
MatcherAssert.AssertMatch(feature, endpoint2, ignoreValues: true);
}
private static Matcher CreateMatcher(params MatcherEndpoint[] endpoints)
{
var services = new ServiceCollection()
.AddOptions()
.AddLogging()
.AddRouting()
.BuildServiceProvider();
var builder = services.GetRequiredService<DfaMatcherBuilder>();
for (var i = 0; i < endpoints.Length; i++)
{
builder.AddEndpoint(endpoints[i]);
}
return builder.Build();
}
internal static (HttpContext httpContext, IEndpointFeature feature) CreateContext(string path, string httpMethod)
{
var httpContext = new DefaultHttpContext();
httpContext.Request.Method = httpMethod;
httpContext.Request.Path = path;
var feature = new EndpointFeature();
httpContext.Features.Set<IEndpointFeature>(feature);
return (httpContext, feature);
}
internal static MatcherEndpoint CreateEndpoint(
string template,
object defaults = null,
object constraints = null,
int order = 0,
string[] httpMethods = null)
{
var metadata = new List<object>();
if (httpMethods != null)
{
metadata.Add(new HttpMethodMetadata(httpMethods));
}
var displayName = "endpoint: " + template + " " + string.Join(", ", httpMethods ?? new[] { "(any)" });
return new MatcherEndpoint(
MatcherEndpoint.EmptyInvoker,
RoutePatternFactory.Parse(template, defaults, constraints),
new RouteValueDictionary(),
order,
new EndpointMetadataCollection(metadata),
displayName);
}
internal (Matcher matcher, MatcherEndpoint endpoint) CreateMatcher(string template)
{
var endpoint = CreateEndpoint(template);
return (CreateMatcher(endpoint), endpoint);
}
}
}

View File

@ -0,0 +1,171 @@
// 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 System.Text;
using Microsoft.AspNetCore.Routing.Metadata;
using Microsoft.AspNetCore.Routing.Patterns;
using Xunit;
namespace Microsoft.AspNetCore.Routing.Matchers
{
public class HttpMethodMatcherPolicyTest
{
[Fact]
public void AppliesToNode_EndpointWithoutMetadata_ReturnsFalse()
{
// Arrange
var endpoints = new[] { CreateEndpoint("/", null), };
var policy = CreatePolicy();
// Act
var result = policy.AppliesToNode(endpoints);
// Assert
Assert.False(result);
}
[Fact]
public void AppliesToNode_EndpointWithoutHttpMethods_ReturnsFalse()
{
// Arrange
var endpoints = new[] { CreateEndpoint("/", Array.Empty<string>()), };
var policy = CreatePolicy();
// Act
var result = policy.AppliesToNode(endpoints);
// Assert
Assert.False(result);
}
[Fact]
public void AppliesToNode_EndpointHasHttpMethods_ReturnsTrue()
{
// Arrange
var endpoints = new[] { CreateEndpoint("/", Array.Empty<string>()), CreateEndpoint("/", new[] { "GET", })};
var policy = CreatePolicy();
// Act
var result = policy.AppliesToNode(endpoints);
// Assert
Assert.True(result);
}
[Fact]
public void GetEdges_GroupsByHttpMethod()
{
// Arrange
var endpoints = new[]
{
// These are arrange in an order that we won't actually see in a product scenario. It's done
// this way so we can verify that ordering is preserved by GetEdges.
CreateEndpoint("/", new[] { "GET", }),
CreateEndpoint("/", Array.Empty<string>()),
CreateEndpoint("/", new[] { "GET", "PUT", "POST" }),
CreateEndpoint("/", new[] { "PUT", "POST" }),
CreateEndpoint("/", Array.Empty<string>()),
};
var policy = CreatePolicy();
// Act
var edges = policy.GetEdges(endpoints);
// Assert
Assert.Collection(
edges.OrderBy(e => e.State),
e =>
{
Assert.Equal(HttpMethodEndpointSelectorPolicy.AnyMethod, e.State);
Assert.Equal(new[] { endpoints[1], endpoints[4], }, e.Endpoints.ToArray());
},
e =>
{
Assert.Equal("GET", e.State);
Assert.Equal(new[] { endpoints[0], endpoints[1], endpoints[2], endpoints[4], }, e.Endpoints.ToArray());
},
e =>
{
Assert.Equal("POST", e.State);
Assert.Equal(new[] { endpoints[1], endpoints[2], endpoints[3], endpoints[4], }, e.Endpoints.ToArray());
},
e =>
{
Assert.Equal("PUT", e.State);
Assert.Equal(new[] { endpoints[1], endpoints[2], endpoints[3], endpoints[4], }, e.Endpoints.ToArray());
});
}
[Fact] // See explanation in GetEdges for how this case is different
public void GetEdges_GroupsByHttpMethod_CreatesHttp405Endpoint()
{
// Arrange
var endpoints = new[]
{
// These are arrange in an order that we won't actually see in a product scenario. It's done
// this way so we can verify that ordering is preserved by GetEdges.
CreateEndpoint("/", new[] { "GET", }),
CreateEndpoint("/", new[] { "GET", "PUT", "POST" }),
CreateEndpoint("/", new[] { "PUT", "POST" }),
};
var policy = CreatePolicy();
// Act
var edges = policy.GetEdges(endpoints);
// Assert
Assert.Collection(
edges.OrderBy(e => e.State),
e =>
{
Assert.Equal(HttpMethodEndpointSelectorPolicy.AnyMethod, e.State);
Assert.Equal(HttpMethodEndpointSelectorPolicy.Http405EndpointDisplayName, e.Endpoints.Single().DisplayName);
},
e =>
{
Assert.Equal("GET", e.State);
Assert.Equal(new[] { endpoints[0], endpoints[1], }, e.Endpoints.ToArray());
},
e =>
{
Assert.Equal("POST", e.State);
Assert.Equal(new[] { endpoints[1], endpoints[2], }, e.Endpoints.ToArray());
},
e =>
{
Assert.Equal("PUT", e.State);
Assert.Equal(new[] { endpoints[1], endpoints[2], }, e.Endpoints.ToArray());
});
}
private static MatcherEndpoint CreateEndpoint(string template, string[] httpMethods)
{
var metadata = new List<object>();
if (httpMethods != null)
{
metadata.Add(new HttpMethodMetadata(httpMethods));
}
return new MatcherEndpoint(
MatcherEndpoint.EmptyInvoker,
RoutePatternFactory.Parse(template),
new RouteValueDictionary(),
0,
new EndpointMetadataCollection(metadata),
$"test: {template}");
}
private static HttpMethodEndpointSelectorPolicy CreatePolicy()
{
return new HttpMethodEndpointSelectorPolicy();
}
}
}

View File

@ -0,0 +1,254 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using Microsoft.AspNetCore.Routing.Patterns;
using Xunit;
namespace Microsoft.AspNetCore.Routing.Matchers
{
public class MatcherEndpointComparerTest
{
[Fact]
public void Compare_PrefersOrder_IfDifferent()
{
// Arrange
var endpoint1 = CreateEndpoint("/", order: 1);
var endpoint2 = CreateEndpoint("/api/foo", order: -1);
var comparer = CreateComparer();
// Act
var result = comparer.Compare(endpoint1, endpoint2);
// Assert
Assert.Equal(1, result);
}
[Fact]
public void Compare_PrefersPrecedence_IfOrderIsSame()
{
// Arrange
var endpoint1 = CreateEndpoint("/api/foo", order: 1);
var endpoint2 = CreateEndpoint("/", order: 1);
var comparer = CreateComparer();
// Act
var result = comparer.Compare(endpoint1, endpoint2);
// Assert
Assert.Equal(1, result);
}
[Fact]
public void Compare_PrefersPolicy_IfPrecedenceIsSame()
{
// Arrange
var endpoint1 = CreateEndpoint("/", order: 1, new TestMetadata1());
var endpoint2 = CreateEndpoint("/", order: 1);
var comparer = CreateComparer(new TestMetadata1Policy());
// Act
var result = comparer.Compare(endpoint1, endpoint2);
// Assert
Assert.Equal(-1, result);
}
[Fact]
public void Compare_PrefersSecondPolicy_IfFirstPolicyIsSame()
{
// Arrange
var endpoint1 = CreateEndpoint("/", order: 1, new TestMetadata1());
var endpoint2 = CreateEndpoint("/", order: 1, new TestMetadata1(), new TestMetadata2());
var comparer = CreateComparer(new TestMetadata1Policy(), new TestMetadata2Policy());
// Act
var result = comparer.Compare(endpoint1, endpoint2);
// Assert
Assert.Equal(1, result);
}
[Fact]
public void Compare_PrefersTemplate_IfOtherCriteriaIsSame()
{
// Arrange
var endpoint1 = CreateEndpoint("/foo", order: 1, new TestMetadata1());
var endpoint2 = CreateEndpoint("/bar", order: 1, new TestMetadata1());
var comparer = CreateComparer(new TestMetadata1Policy(), new TestMetadata2Policy());
// Act
var result = comparer.Compare(endpoint1, endpoint2);
// Assert
Assert.Equal(1, result);
}
[Fact]
public void Compare_ReturnsZero_WhenIdentical()
{
// Arrange
var endpoint1 = CreateEndpoint("/foo", order: 1, new TestMetadata1());
var endpoint2 = CreateEndpoint("/foo", order: 1, new TestMetadata1());
var comparer = CreateComparer(new TestMetadata1Policy(), new TestMetadata2Policy());
// Act
var result = comparer.Compare(endpoint1, endpoint2);
// Assert
Assert.Equal(0, result);
}
[Fact]
public void Equals_NotEqual_IfOrderDifferent()
{
// Arrange
var endpoint1 = CreateEndpoint("/", order: 1);
var endpoint2 = CreateEndpoint("/api/foo", order: -1);
var comparer = CreateComparer();
// Act
var result = comparer.Equals(endpoint1, endpoint2);
// Assert
Assert.False(result);
}
[Fact]
public void Equals_NotEqual_IfPrecedenceDifferent()
{
// Arrange
var endpoint1 = CreateEndpoint("/api/foo", order: 1);
var endpoint2 = CreateEndpoint("/", order: 1);
var comparer = CreateComparer();
// Act
var result = comparer.Equals(endpoint1, endpoint2);
// Assert
Assert.False(result);
}
[Fact]
public void Equals_NotEqual_IfFirstPolicyDifferent()
{
// Arrange
var endpoint1 = CreateEndpoint("/", order: 1, new TestMetadata1());
var endpoint2 = CreateEndpoint("/", order: 1);
var comparer = CreateComparer(new TestMetadata1Policy());
// Act
var result = comparer.Equals(endpoint1, endpoint2);
// Assert
Assert.False(result);
}
[Fact]
public void Equals_NotEqual_IfSecondPolicyDifferent()
{
// Arrange
var endpoint1 = CreateEndpoint("/", order: 1, new TestMetadata1());
var endpoint2 = CreateEndpoint("/", order: 1, new TestMetadata1(), new TestMetadata2());
var comparer = CreateComparer(new TestMetadata1Policy(), new TestMetadata2Policy());
// Act
var result = comparer.Equals(endpoint1, endpoint2);
// Assert
Assert.False(result);
}
[Fact]
public void Equals_Equals_WhenTemplateIsDifferent()
{
// Arrange
var endpoint1 = CreateEndpoint("/foo", order: 1, new TestMetadata1());
var endpoint2 = CreateEndpoint("/bar", order: 1, new TestMetadata1());
var comparer = CreateComparer(new TestMetadata1Policy(), new TestMetadata2Policy());
// Act
var result = comparer.Equals(endpoint1, endpoint2);
// Assert
Assert.True(result);
}
[Fact]
public void Sort_MoreSpecific_FirstInList()
{
// Arrange
var endpoint1 = CreateEndpoint("/foo", order: -1);
var endpoint2 = CreateEndpoint("/bar/{baz}", order: -1);
var endpoint3 = CreateEndpoint("/bar", order: 0, new TestMetadata1());
var endpoint4 = CreateEndpoint("/foo", order: 0, new TestMetadata2());
var endpoint5 = CreateEndpoint("/foo", order: 0);
var endpoint6 = CreateEndpoint("/a{baz}", order: 0, new TestMetadata1(), new TestMetadata2());
var endpoint7 = CreateEndpoint("/bar{baz}", order: 0, new TestMetadata1(), new TestMetadata2());
// Endpoints listed in reverse of the desired order.
var list = new List<MatcherEndpoint>() { endpoint7, endpoint6, endpoint5, endpoint4, endpoint3, endpoint2, endpoint1, };
var comparer = CreateComparer(new TestMetadata1Policy(), new TestMetadata2Policy());
// Act
list.Sort(comparer);
// Assert
Assert.Collection(
list,
e => Assert.Same(endpoint1, e),
e => Assert.Same(endpoint2, e),
e => Assert.Same(endpoint3, e),
e => Assert.Same(endpoint4, e),
e => Assert.Same(endpoint5, e),
e => Assert.Same(endpoint6, e),
e => Assert.Same(endpoint7, e));
}
private static MatcherEndpoint CreateEndpoint(string template, int order, params object[] metadata)
{
return new MatcherEndpoint(
MatcherEndpoint.EmptyInvoker,
RoutePatternFactory.Parse(template),
new RouteValueDictionary(),
order,
new EndpointMetadataCollection(metadata),
"test: " + template);
}
private static MatcherEndpointComparer CreateComparer(params IEndpointComparerPolicy[] policies)
{
return new MatcherEndpointComparer(policies);
}
private class TestMetadata1
{
}
private class TestMetadata1Policy : IEndpointComparerPolicy
{
public IComparer<Endpoint> Comparer => EndpointMetadataComparer<TestMetadata1>.Default;
}
private class TestMetadata2
{
}
private class TestMetadata2Policy : IEndpointComparerPolicy
{
public IComparer<Endpoint> Comparer => EndpointMetadataComparer<TestMetadata2>.Default;
}
}
}

View File

@ -7,7 +7,6 @@ using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Routing.EndpointConstraints;
using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.AspNetCore.Routing.Template;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
@ -16,39 +15,37 @@ namespace Microsoft.AspNetCore.Routing.Matchers
internal class RouteMatcherBuilder : MatcherBuilder
{
private readonly IInlineConstraintResolver _constraintResolver;
private readonly List<MatcherBuilderEntry> _entries;
private readonly List<MatcherEndpoint> _endpoints;
public RouteMatcherBuilder()
{
_constraintResolver = new DefaultInlineConstraintResolver(Options.Create(new RouteOptions()));
_entries = new List<MatcherBuilderEntry>();
_endpoints = new List<MatcherEndpoint>();
}
public override void AddEndpoint(MatcherEndpoint endpoint)
{
_entries.Add(new MatcherBuilderEntry(endpoint));
_endpoints.Add(endpoint);
}
public override Matcher Build()
{
_entries.Sort();
var cache = new EndpointConstraintCache(
new CompositeEndpointDataSource(Array.Empty<EndpointDataSource>()),
new[] { new DefaultEndpointConstraintProvider(), });
var selector = new EndpointSelector(null, cache, NullLoggerFactory.Instance);
var selector = new EndpointConstraintEndpointSelector(null, cache, NullLoggerFactory.Instance);
var groups = _entries
.GroupBy(e => (e.Order, e.Precedence, e.Endpoint.RoutePattern.RawText))
var groups = _endpoints
.GroupBy(e => (e.Order, e.RoutePattern.InboundPrecedence, e.RoutePattern.RawText))
.OrderBy(g => g.Key.Order)
.ThenBy(g => g.Key.Precedence);
.ThenBy(g => g.Key.InboundPrecedence);
var routes = new RouteCollection();
foreach (var group in groups)
{
var candidates = group.Select(e => e.Endpoint).ToArray();
var endpoint = group.First().Endpoint;
var candidates = group.ToArray();
var endpoint = group.First();
// RoutePattern.Defaults contains the default values parsed from the template
// as well as those specified with a literal. We need to separate those
@ -81,12 +78,15 @@ namespace Microsoft.AspNetCore.Routing.Matchers
private class SelectorRouter : IRouter
{
private readonly EndpointSelector _selector;
private readonly Endpoint[] _candidates;
private readonly MatcherEndpoint[] _candidates;
private readonly int[] _scores;
public SelectorRouter(EndpointSelector selector, Endpoint[] candidates)
public SelectorRouter(EndpointSelector selector, MatcherEndpoint[] candidates)
{
_selector = selector;
_candidates = candidates;
_scores = new int[_candidates.Length];
}
public VirtualPathData GetVirtualPath(VirtualPathContext context)
@ -94,15 +94,19 @@ namespace Microsoft.AspNetCore.Routing.Matchers
throw new NotImplementedException();
}
public Task RouteAsync(RouteContext context)
public async Task RouteAsync(RouteContext context)
{
var endpoint = _selector.SelectBestCandidate(context.HttpContext, _candidates);
if (endpoint != null)
var feature = context.HttpContext.Features.Get<IEndpointFeature>();
// This is needed due to a quirk of our tests - they reuse the endpoint feature
// across requests.
feature.Endpoint = null;
await _selector.SelectAsync(context.HttpContext, feature, new CandidateSet(_candidates, _scores));
if (feature.Endpoint != null)
{
context.HttpContext.Features.Get<IEndpointFeature>().Endpoint = endpoint;
context.Handler = (_) => Task.CompletedTask;
}
return Task.CompletedTask;
}
}
}

View File

@ -17,22 +17,20 @@ namespace Microsoft.AspNetCore.Routing.Matchers
{
internal class TreeRouterMatcherBuilder : MatcherBuilder
{
private readonly List<MatcherBuilderEntry> _entries;
private readonly List<MatcherEndpoint> _endpoints;
public TreeRouterMatcherBuilder()
{
_entries = new List<MatcherBuilderEntry>();
_endpoints = new List<MatcherEndpoint>();
}
public override void AddEndpoint(MatcherEndpoint endpoint)
{
_entries.Add(new MatcherBuilderEntry(endpoint));
_endpoints.Add(endpoint);
}
public override Matcher Build()
{
_entries.Sort();
var builder = new TreeRouteBuilder(
NullLoggerFactory.Instance,
new DefaultObjectPool<UriBuildingContext>(new UriBuilderContextPooledObjectPolicy()),
@ -41,23 +39,23 @@ namespace Microsoft.AspNetCore.Routing.Matchers
var cache = new EndpointConstraintCache(
new CompositeEndpointDataSource(Array.Empty<EndpointDataSource>()),
new[] { new DefaultEndpointConstraintProvider(), });
var selector = new EndpointSelector(null, cache, NullLoggerFactory.Instance);
var selector = new EndpointConstraintEndpointSelector(null, cache, NullLoggerFactory.Instance);
var groups = _entries
.GroupBy(e => (e.Order, e.Precedence, e.Endpoint.RoutePattern.RawText))
var groups = _endpoints
.GroupBy(e => (e.Order, e.RoutePattern.InboundPrecedence, e.RoutePattern.RawText))
.OrderBy(g => g.Key.Order)
.ThenBy(g => g.Key.Precedence);
.ThenBy(g => g.Key.InboundPrecedence);
var routes = new RouteCollection();
foreach (var group in groups)
{
var candidates = group.Select(e => e.Endpoint).ToArray();
var candidates = group.ToArray();
// MatcherEndpoint.Values contains the default values parsed from the template
// as well as those specified with a literal. We need to separate those
// for legacy cases.
var endpoint = group.First().Endpoint;
var endpoint = group.First();
var defaults = new RouteValueDictionary(endpoint.RoutePattern.Defaults);
for (var i = 0; i < endpoint.RoutePattern.Parameters.Count; i++)
{
@ -81,12 +79,15 @@ namespace Microsoft.AspNetCore.Routing.Matchers
private class SelectorRouter : IRouter
{
private readonly EndpointSelector _selector;
private readonly Endpoint[] _candidates;
private readonly MatcherEndpoint[] _candidates;
private readonly int[] _scores;
public SelectorRouter(EndpointSelector selector, Endpoint[] candidates)
public SelectorRouter(EndpointSelector selector, MatcherEndpoint[] candidates)
{
_selector = selector;
_candidates = candidates;
_scores = new int[_candidates.Length];
}
public VirtualPathData GetVirtualPath(VirtualPathContext context)
@ -94,15 +95,18 @@ namespace Microsoft.AspNetCore.Routing.Matchers
throw new NotImplementedException();
}
public Task RouteAsync(RouteContext context)
public async Task RouteAsync(RouteContext context)
{
var endpoint = _selector.SelectBestCandidate(context.HttpContext, _candidates);
if (endpoint != null)
var feature = context.HttpContext.Features.Get<IEndpointFeature>();
// This is needed due to a quirk of our tests - they reuse the endpoint feature.
feature.Endpoint = null;
await _selector.SelectAsync(context.HttpContext, feature, new CandidateSet(_candidates, _scores));
if (feature.Endpoint != null)
{
context.HttpContext.Features.Get<IEndpointFeature>().Endpoint = endpoint;
context.Handler = (_) => Task.CompletedTask;
}
return Task.CompletedTask;
}
}
}