Merge branch 'merge/release/2.2-to-master'

# Conflicts:
#	build/dependencies.props
This commit is contained in:
Kiran Challa 2018-07-25 07:13:51 -07:00
commit c4bc628aa6
60 changed files with 3481 additions and 957 deletions

View File

@ -1,4 +1,4 @@
Contributing
======
Information on contributing to this repo is in the [Contributing Guide](https://github.com/aspnet/Home/blob/dev/CONTRIBUTING.md) in the Home repo.
Information on contributing to this repo is in the [Contributing Guide](https://github.com/aspnet/Home/blob/master/CONTRIBUTING.md) in the Home repo.

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

@ -0,0 +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.
namespace Microsoft.AspNetCore.Routing
{
public interface ISuppressLinkGenerationMetadata
{
}
}

View File

@ -3,12 +3,17 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using Microsoft.AspNetCore.Routing.EndpointConstraints;
using Microsoft.AspNetCore.Routing.Matchers;
using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNetCore.Routing
{
[DebuggerDisplay("{DebuggerDisplayString,nq}")]
public class CompositeEndpointDataSource : EndpointDataSource
{
private readonly EndpointDataSource[] _dataSources;
@ -29,13 +34,12 @@ namespace Microsoft.AspNetCore.Routing
_lock = new object();
}
public override IChangeToken ChangeToken
public override IChangeToken ChangeToken => GetChangeToken();
public override IChangeToken GetChangeToken()
{
get
{
EnsureInitialized();
return _consumerChangeToken;
}
EnsureInitialized();
return _consumerChangeToken;
}
public override IReadOnlyList<Endpoint> Endpoints
@ -106,5 +110,51 @@ namespace Microsoft.AspNetCore.Routing
_cts = new CancellationTokenSource();
_consumerChangeToken = new CancellationChangeToken(_cts.Token);
}
private string DebuggerDisplayString
{
get
{
// Try using private variable '_endpoints' to avoid initialization
if (_endpoints == null)
{
return "No endpoints";
}
var sb = new StringBuilder();
foreach (var endpoint in _endpoints)
{
if (endpoint is MatcherEndpoint matcherEndpoint)
{
var template = matcherEndpoint.RoutePattern.RawText;
template = string.IsNullOrEmpty(template) ? "\"\"" : template;
sb.Append(template);
var requiredValues = matcherEndpoint.RequiredValues.Select(kvp => $"{kvp.Key} = \"{kvp.Value ?? "null"}\"");
sb.Append(", Required Values: new { ");
sb.Append(string.Join(", ", requiredValues));
sb.Append(" }");
sb.Append(", Order:");
sb.Append(matcherEndpoint.Order);
var httpEndpointConstraints = matcherEndpoint.Metadata.GetOrderedMetadata<IEndpointConstraintMetadata>()
.OfType<HttpMethodEndpointConstraint>();
foreach (var constraint in httpEndpointConstraints)
{
sb.Append(", Http Methods: ");
sb.Append(string.Join(", ", constraint.HttpMethods));
sb.Append(", Constraint Order:");
sb.Append(constraint.Order);
}
sb.AppendLine();
}
else
{
sb.Append("Non-MatcherEndpoint. DisplayName:");
sb.AppendLine(endpoint.DisplayName);
}
}
return sb.ToString();
}
}
}
}

View File

@ -23,7 +23,9 @@ namespace Microsoft.AspNetCore.Routing
_endpoints.AddRange(endpoints);
}
public override IChangeToken ChangeToken { get; } = NullChangeToken.Singleton;
public override IChangeToken ChangeToken => GetChangeToken();
public override IChangeToken GetChangeToken() => NullChangeToken.Singleton;
public override IReadOnlyList<Endpoint> Endpoints => _endpoints;
}

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,33 @@ namespace Microsoft.AspNetCore.Routing.EndpointConstraints
}
Endpoint = endpoint;
Score = score;
Values = values;
Constraints = constraints;
}
// Temporarily added to not break MVC build
public EndpointSelectorCandidate(
Endpoint endpoint,
IReadOnlyList<IEndpointConstraint> constraints)
{
if (endpoint == null)
{
throw new ArgumentNullException(nameof(endpoint));
}
Endpoint = endpoint;
Score = 0;
Values = null;
Constraints = constraints;
}
public Endpoint Endpoint { get; }
public int Score { get; }
public RouteValueDictionary Values { get; }
public IReadOnlyList<IEndpointConstraint> Constraints { get; }
}

View File

@ -3,12 +3,16 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using Microsoft.AspNetCore.Routing.DecisionTree;
using Microsoft.AspNetCore.Routing.Tree;
namespace Microsoft.AspNetCore.Routing.Internal
{
// A decision tree that matches link generation entries based on route data.
[DebuggerDisplay("{DebuggerDisplayString,nq}")]
public class LinkGenerationDecisionTree
{
private readonly DecisionTreeNode<OutboundMatch> _root;
@ -160,5 +164,51 @@ namespace Microsoft.AspNetCore.Routing.Internal
y.Match.Entry.RouteTemplate.TemplateText);
}
}
// Example output:
//
// => action: Buy => controller: Store => version: V1(Matches: Store/Buy/V1)
// => action: Buy => controller: Store => version: V2(Matches: Store/Buy/V2)
// => action: Buy => controller: Store => area: Admin(Matches: Admin/Store/Buy)
// => action: Buy => controller: Products(Matches: Products/Buy)
// => action: Cart => controller: Store(Matches: Store/Cart)
internal string DebuggerDisplayString
{
get
{
var sb = new StringBuilder();
var branchStack = new Stack<string>();
branchStack.Push(string.Empty);
FlattenTree(branchStack, sb, _root);
return sb.ToString();
}
}
private void FlattenTree(Stack<string> branchStack, StringBuilder sb, DecisionTreeNode<OutboundMatch> node)
{
// leaf node
if (node.Criteria.Count == 0)
{
var matchesSb = new StringBuilder();
foreach (var branch in branchStack)
{
matchesSb.Insert(0, branch);
}
sb.Append(matchesSb.ToString());
sb.Append(" (Matches: ");
sb.Append(string.Join(", ", node.Matches.Select(m => m.Entry.RouteTemplate.TemplateText)));
sb.AppendLine(")");
}
foreach (var criterion in node.Criteria)
{
foreach (var branch in criterion.Branches)
{
branchStack.Push($" => {criterion.Key}: {branch.Key}");
FlattenTree(branchStack, sb, branch.Value);
branchStack.Pop();
}
}
}
}
}

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

@ -107,6 +107,13 @@ namespace Microsoft.AspNetCore.Routing
var endpoints = _endpointDataSource.Endpoints.OfType<MatcherEndpoint>();
foreach (var endpoint in endpoints)
{
// Do not consider an endpoint for link generation if the following marker metadata is on it
var suppressLinkGeneration = endpoint.Metadata.GetMetadata<ISuppressLinkGenerationMetadata>();
if (suppressLinkGeneration != null)
{
continue;
}
var entry = CreateOutboundRouteEntry(endpoint);
var outboundMatch = new OutboundMatch() { Entry = entry };

View File

@ -173,7 +173,8 @@ namespace Microsoft.AspNetCore.Routing
_token = new CancellationChangeToken(_cts.Token);
}
public override IChangeToken ChangeToken => _token;
public override IChangeToken GetChangeToken() => _token;
public override IChangeToken ChangeToken => GetChangeToken();
public override IReadOnlyList<Endpoint> Endpoints => Array.Empty<Endpoint>();
}
}

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

@ -1,9 +1,11 @@
// 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.Patterns;
using Microsoft.AspNetCore.Routing.Template;
using Microsoft.AspNetCore.Routing.Tree;
using Xunit;
@ -318,11 +320,45 @@ namespace Microsoft.AspNetCore.Routing.Internal.Routing
Assert.Equal(entries, matches);
}
private OutboundMatch CreateMatch(object requiredValues)
[Fact]
public void ToDebuggerDisplayString_GivesAFlattenedTree()
{
// Arrange
var entries = new List<OutboundMatch>();
entries.Add(CreateMatch(new { action = "Buy", controller = "Store", version = "V1" }, "Store/Buy/V1"));
entries.Add(CreateMatch(new { action = "Buy", controller = "Store", area = "Admin" }, "Admin/Store/Buy"));
entries.Add(CreateMatch(new { action = "Buy", controller = "Products" }, "Products/Buy"));
entries.Add(CreateMatch(new { action = "Buy", controller = "Store", version = "V2" }, "Store/Buy/V2"));
entries.Add(CreateMatch(new { action = "Cart", controller = "Store" }, "Store/Cart"));
entries.Add(CreateMatch(new { action = "Index", controller = "Home" }, "Home/Index/{id?}"));
var tree = new LinkGenerationDecisionTree(entries);
var newLine = Environment.NewLine;
var expected =
" => action: Buy => controller: Store => version: V1 (Matches: Store/Buy/V1)" + newLine +
" => action: Buy => controller: Store => version: V2 (Matches: Store/Buy/V2)" + newLine +
" => action: Buy => controller: Store => area: Admin (Matches: Admin/Store/Buy)" + newLine +
" => action: Buy => controller: Products (Matches: Products/Buy)" + newLine +
" => action: Cart => controller: Store (Matches: Store/Cart)" + newLine +
" => action: Index => controller: Home (Matches: Home/Index/{id?})" + newLine;
// Act
var flattenedTree = tree.DebuggerDisplayString;
// Assert
Assert.Equal(expected, flattenedTree);
}
private OutboundMatch CreateMatch(object requiredValues, string routeTemplate = null)
{
var match = new OutboundMatch();
match.Entry = new OutboundRouteEntry();
match.Entry.RequiredLinkValues = new RouteValueDictionary(requiredValues);
if (!string.IsNullOrEmpty(routeTemplate))
{
match.Entry.RouteTemplate = new RouteTemplate(RoutePatternFactory.Parse(routeTemplate));
}
return match;
}

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

View File

@ -224,6 +224,21 @@ namespace Microsoft.AspNetCore.Routing
Assert.Same(expected, actual);
}
[Fact]
public void GetOutboundMatches_DoesNotInclude_EndpointsWithSuppressLinkGenerationMetadata()
{
// Arrange
var endpoint = CreateEndpoint(
"/a",
metadataCollection: new EndpointMetadataCollection(new[] { new SuppressLinkGenerationMetadata() }));
// Act
var finder = CreateEndpointFinder(endpoint);
// Assert
Assert.Empty(finder.AllMatches);
}
private CustomRouteValuesBasedEndpointFinder CreateEndpointFinder(params Endpoint[] endpoints)
{
return CreateEndpointFinder(new DefaultEndpointDataSource(endpoints));
@ -304,5 +319,7 @@ namespace Microsoft.AspNetCore.Routing
return matches;
}
}
private class SuppressLinkGenerationMetadata : ISuppressLinkGenerationMetadata { }
}
}

View File

@ -23,7 +23,9 @@ namespace Microsoft.AspNetCore.Routing.TestObjects
CreateChangeToken();
}
public override IChangeToken ChangeToken => _changeToken;
public override IChangeToken GetChangeToken() => _changeToken;
public override IChangeToken ChangeToken => GetChangeToken();
public override IReadOnlyList<Endpoint> Endpoints => _endpoints;

View File

@ -1399,7 +1399,6 @@ namespace Microsoft.AspNetCore.Routing.Tree
Assert.Empty(pathData.DataTokens);
}
[Fact]
public void TreeRouter_GenerateLink_Match_WithParameters()
{
@ -1965,6 +1964,94 @@ namespace Microsoft.AspNetCore.Routing.Tree
Assert.DoesNotContain(nestedValues, kvp => kvp.Key == "category1");
}
[Fact]
public void TreeRouter_GenerateLink_MatchesNullRequiredValue_WithNullRequestValueString()
{
// Arrange
var builder = CreateBuilder();
var entry = MapOutboundEntry(
builder,
"Help/Store",
requiredValues: new { area = (string)null, action = "Edit", controller = "Store" });
var route = builder.Build();
var context = CreateVirtualPathContext(new { area = (string)null, action = "Edit", controller = "Store" });
// Act
var pathData = route.GetVirtualPath(context);
// Assert
Assert.NotNull(pathData);
Assert.Equal("/Help/Store", pathData.VirtualPath);
Assert.Same(route, pathData.Router);
Assert.Empty(pathData.DataTokens);
}
[Fact]
public void TreeRouter_GenerateLink_MatchesNullRequiredValue_WithEmptyRequestValueString()
{
// Arrange
var builder = CreateBuilder();
var entry = MapOutboundEntry(
builder,
"Help/Store",
requiredValues: new { area = (string)null, action = "Edit", controller = "Store" });
var route = builder.Build();
var context = CreateVirtualPathContext(new { area = "", action = "Edit", controller = "Store" });
// Act
var pathData = route.GetVirtualPath(context);
// Assert
Assert.NotNull(pathData);
Assert.Equal("/Help/Store", pathData.VirtualPath);
Assert.Same(route, pathData.Router);
Assert.Empty(pathData.DataTokens);
}
[Fact]
public void TreeRouter_GenerateLink_MatchesEmptyStringRequiredValue_WithNullRequestValueString()
{
// Arrange
var builder = CreateBuilder();
var entry = MapOutboundEntry(
builder,
"Help/Store",
requiredValues: new { foo = "", action = "Edit", controller = "Store" });
var route = builder.Build();
var context = CreateVirtualPathContext(new { foo = (string)null, action = "Edit", controller = "Store" });
// Act
var pathData = route.GetVirtualPath(context);
// Assert
Assert.NotNull(pathData);
Assert.Equal("/Help/Store", pathData.VirtualPath);
Assert.Same(route, pathData.Router);
Assert.Empty(pathData.DataTokens);
}
[Fact]
public void TreeRouter_GenerateLink_MatchesEmptyStringRequiredValue_WithEmptyRequestValueString()
{
// Arrange
var builder = CreateBuilder();
var entry = MapOutboundEntry(
builder,
"Help/Store",
requiredValues: new { foo = "", action = "Edit", controller = "Store" });
var route = builder.Build();
var context = CreateVirtualPathContext(new { foo = "", action = "Edit", controller = "Store" });
// Act
var pathData = route.GetVirtualPath(context);
// Assert
Assert.NotNull(pathData);
Assert.Equal("/Help/Store", pathData.VirtualPath);
Assert.Same(route, pathData.Router);
Assert.Empty(pathData.DataTokens);
}
private static RouteContext CreateRouteContext(string requestPath)
{
var request = new Mock<HttpRequest>(MockBehavior.Strict);