Merge branch 'merge/release/2.2-to-master'
# Conflicts: # build/dependencies.props
This commit is contained in:
commit
c4bc628aa6
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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()})";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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>();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 { }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in New Issue