Remove EndpointConstraints

Adds IEndpointSelectorPolicy so that MVC can plug in to the
EndpointSelector to run action constraints.
This commit is contained in:
Ryan Nowak 2018-07-26 23:20:44 -07:00
parent f870503cdd
commit f8b3b73ca7
13 changed files with 73 additions and 1281 deletions

View File

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

View File

@ -7,7 +7,6 @@ using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using Microsoft.AspNetCore.Routing.EndpointConstraints;
using Microsoft.AspNetCore.Routing.Matchers;
using Microsoft.AspNetCore.Routing.Metadata;
using Microsoft.Extensions.Primitives;
@ -138,11 +137,8 @@ namespace Microsoft.AspNetCore.Routing
var httpMethodMetadata = matcherEndpoint.Metadata.GetMetadata<IHttpMethodMetadata>();
if (httpMethodMetadata != null)
{
foreach (var httpMethod in httpMethodMetadata.HttpMethods)
{
sb.Append(", Http Methods: ");
sb.Append(string.Join(", ", httpMethod));
}
sb.Append(", Http Methods: ");
sb.Append(string.Join(", ", httpMethodMetadata.HttpMethods));
}
sb.AppendLine();

View File

@ -4,7 +4,6 @@
using System;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Constraints;
using Microsoft.AspNetCore.Routing.EndpointConstraints;
using Microsoft.AspNetCore.Routing.EndpointFinders;
using Microsoft.AspNetCore.Routing.Internal;
using Microsoft.AspNetCore.Routing.Matchers;
@ -73,18 +72,13 @@ namespace Microsoft.Extensions.DependencyInjection
services.TryAddSingleton<IEndpointFinder<string>, NameBasedEndpointFinder>();
services.TryAddSingleton<IEndpointFinder<RouteValuesBasedEndpointFinderContext>, RouteValuesBasedEndpointFinder>();
services.TryAddSingleton<LinkGenerator, DefaultLinkGenerator>();
//
// Endpoint Selection
//
services.TryAddSingleton<EndpointSelector, EndpointConstraintEndpointSelector>();
services.TryAddSingleton<EndpointConstraintCache>();
services.TryAddSingleton<EndpointSelector, DefaultEndpointSelector>();
services.TryAddEnumerable(ServiceDescriptor.Singleton<MatcherPolicy, HttpMethodMatcherPolicy>());
// Will be cached by the EndpointSelector
services.TryAddEnumerable(
ServiceDescriptor.Transient<IEndpointConstraintProvider, DefaultEndpointConstraintProvider>());
return services;
}

View File

@ -1,208 +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 Microsoft.AspNetCore.Http;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
namespace Microsoft.AspNetCore.Routing.EndpointConstraints
{
internal class EndpointConstraintCache
{
private readonly CompositeEndpointDataSource _dataSource;
private readonly IEndpointConstraintProvider[] _endpointConstraintProviders;
private volatile InnerCache _currentCache;
public EndpointConstraintCache(
CompositeEndpointDataSource dataSource,
IEnumerable<IEndpointConstraintProvider> endpointConstraintProviders)
{
_dataSource = dataSource;
_endpointConstraintProviders = endpointConstraintProviders.OrderBy(item => item.Order).ToArray();
}
private InnerCache CurrentCache
{
get
{
var current = _currentCache;
var endpointDescriptors = _dataSource.Endpoints;
if (current == null)
{
current = new InnerCache();
_currentCache = current;
}
return current;
}
}
public IReadOnlyList<IEndpointConstraint> GetEndpointConstraints(HttpContext httpContext, Endpoint endpoint)
{
var cache = CurrentCache;
if (cache.Entries.TryGetValue(endpoint, out var entry))
{
return GetEndpointConstraintsFromEntry(entry, httpContext, endpoint);
}
List<EndpointConstraintItem> items = null;
if (endpoint.Metadata != null && endpoint.Metadata.Count > 0)
{
items = endpoint.Metadata
.OfType<IEndpointConstraintMetadata>()
.Select(m => new EndpointConstraintItem(m))
.ToList();
}
IReadOnlyList<IEndpointConstraint> endpointConstraints = null;
if (items != null && items.Count > 0)
{
ExecuteProviders(httpContext, endpoint, items);
endpointConstraints = ExtractEndpointConstraints(items);
var allEndpointConstraintsCached = true;
for (var i = 0; i < items.Count; i++)
{
var item = items[i];
if (!item.IsReusable)
{
item.Constraint = null;
allEndpointConstraintsCached = false;
}
}
if (allEndpointConstraintsCached)
{
entry = new CacheEntry(endpointConstraints);
}
else
{
entry = new CacheEntry(items);
}
}
else
{
// No constraints
entry = new CacheEntry();
}
cache.Entries.TryAdd(endpoint, entry);
return endpointConstraints;
}
private IReadOnlyList<IEndpointConstraint> GetEndpointConstraintsFromEntry(CacheEntry entry, HttpContext httpContext, Endpoint endpoint)
{
if (entry.EndpointConstraints != null)
{
return entry.EndpointConstraints;
}
if (entry.Items == null)
{
// Endpoint has no constraints
return null;
}
var items = new List<EndpointConstraintItem>(entry.Items.Count);
for (var i = 0; i < entry.Items.Count; i++)
{
var item = entry.Items[i];
if (item.IsReusable)
{
items.Add(item);
}
else
{
items.Add(new EndpointConstraintItem(item.Metadata));
}
}
ExecuteProviders(httpContext, endpoint, items);
return ExtractEndpointConstraints(items);
}
private void ExecuteProviders(HttpContext httpContext, Endpoint endpoint, List<EndpointConstraintItem> items)
{
var context = new EndpointConstraintProviderContext(httpContext, endpoint, items);
for (var i = 0; i < _endpointConstraintProviders.Length; i++)
{
_endpointConstraintProviders[i].OnProvidersExecuting(context);
}
for (var i = _endpointConstraintProviders.Length - 1; i >= 0; i--)
{
_endpointConstraintProviders[i].OnProvidersExecuted(context);
}
}
private IReadOnlyList<IEndpointConstraint> ExtractEndpointConstraints(List<EndpointConstraintItem> items)
{
var count = 0;
for (var i = 0; i < items.Count; i++)
{
if (items[i].Constraint != null)
{
count++;
}
}
if (count == 0)
{
return null;
}
var endpointConstraints = new IEndpointConstraint[count];
var endpointConstraintIndex = 0;
for (int i = 0; i < items.Count; i++)
{
var endpointConstraint = items[i].Constraint;
if (endpointConstraint != null)
{
endpointConstraints[endpointConstraintIndex++] = endpointConstraint;
}
}
return endpointConstraints;
}
private class InnerCache
{
public InnerCache()
{
}
public ConcurrentDictionary<Endpoint, CacheEntry> Entries { get; } =
new ConcurrentDictionary<Endpoint, CacheEntry>();
}
private struct CacheEntry
{
public CacheEntry(IReadOnlyList<IEndpointConstraint> endpointConstraints)
{
EndpointConstraints = endpointConstraints;
Items = null;
}
public CacheEntry(List<EndpointConstraintItem> items)
{
Items = items;
EndpointConstraints = null;
}
public IReadOnlyList<IEndpointConstraint> EndpointConstraints { get; }
public List<EndpointConstraintItem> Items { get; }
}
}
}

View File

@ -1,265 +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 System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing.Matchers;
using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Routing.EndpointConstraints
{
internal class EndpointConstraintEndpointSelector : EndpointSelector
{
private static readonly IReadOnlyList<Endpoint> EmptyEndpoints = Array.Empty<Endpoint>();
private readonly CompositeEndpointDataSource _dataSource;
private readonly EndpointConstraintCache _endpointConstraintCache;
private readonly ILogger _logger;
public EndpointConstraintEndpointSelector(
CompositeEndpointDataSource dataSource,
EndpointConstraintCache endpointConstraintCache,
ILoggerFactory loggerFactory)
{
_dataSource = dataSource;
_logger = loggerFactory.CreateLogger<EndpointSelector>();
_endpointConstraintCache = endpointConstraintCache;
}
public override Task SelectAsync(
HttpContext httpContext,
IEndpointFeature feature,
CandidateSet candidates)
{
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}
if (feature == null)
{
throw new ArgumentNullException(nameof(feature));
}
if (candidates == null)
{
throw new ArgumentNullException(nameof(candidates));
}
var finalMatches = EvaluateEndpointConstraints(httpContext, candidates);
if (finalMatches == null || finalMatches.Count == 0)
{
return Task.CompletedTask;
}
else if (finalMatches.Count == 1)
{
var endpoint = finalMatches[0].Endpoint;
var values = finalMatches[0].Values;
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.Endpoint.DisplayName));
Log.MatchAmbiguous(_logger, httpContext, finalMatches);
var message = Resources.FormatAmbiguousEndpoints(
Environment.NewLine,
string.Join(Environment.NewLine, endpointNames));
throw new AmbiguousMatchException(message);
}
}
private IReadOnlyList<EndpointSelectorCandidate> EvaluateEndpointConstraints(
HttpContext context,
CandidateSet candidateSet)
{
var candidates = new List<EndpointSelectorCandidate>();
// Perf: Avoid allocations
for (var i = 0; i < candidateSet.Count; i++)
{
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<EndpointSelectorCandidate> results = null;
if (matches != null)
{
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)
{
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;
}
}
return results;
}
private IReadOnlyList<EndpointSelectorCandidate> EvaluateEndpointConstraintsCore(
HttpContext context,
IReadOnlyList<EndpointSelectorCandidate> candidates,
int? startingOrder)
{
// Find the next group of constraints to process. This will be the lowest value of
// order that is higher than startingOrder.
int? order = null;
// Perf: Avoid allocations
for (var i = 0; i < candidates.Count; i++)
{
var candidate = candidates[i];
if (candidate.Constraints != null)
{
for (var j = 0; j < candidate.Constraints.Count; j++)
{
var constraint = candidate.Constraints[j];
if ((startingOrder == null || constraint.Order > startingOrder) &&
(order == null || constraint.Order < order))
{
order = constraint.Order;
}
}
}
}
// If we don't find a next then there's nothing left to do.
if (order == null)
{
return candidates;
}
// Since we have a constraint to process, bisect the set of endpoints into those with and without a
// constraint for the current order.
var endpointsWithConstraint = new List<EndpointSelectorCandidate>();
var endpointsWithoutConstraint = new List<EndpointSelectorCandidate>();
var constraintContext = new EndpointConstraintContext();
constraintContext.Candidates = candidates;
constraintContext.HttpContext = context;
// Perf: Avoid allocations
for (var i = 0; i < candidates.Count; i++)
{
var candidate = candidates[i];
var isMatch = true;
var foundMatchingConstraint = false;
if (candidate.Constraints != null)
{
constraintContext.CurrentCandidate = candidate;
for (var j = 0; j < candidate.Constraints.Count; j++)
{
var constraint = candidate.Constraints[j];
if (constraint.Order == order)
{
foundMatchingConstraint = true;
if (!constraint.Accept(constraintContext))
{
isMatch = false;
//_logger.ConstraintMismatch(
// candidate.Endpoint.DisplayName,
// candidate.Endpoint.Id,
// constraint);
break;
}
}
}
}
if (isMatch && foundMatchingConstraint)
{
endpointsWithConstraint.Add(candidate);
}
else if (isMatch)
{
endpointsWithoutConstraint.Add(candidate);
}
}
// If we have matches with constraints, those are better so try to keep processing those
if (endpointsWithConstraint.Count > 0)
{
var matches = EvaluateEndpointConstraintsCore(context, endpointsWithConstraint, order);
if (matches?.Count > 0)
{
return matches;
}
}
// If the set of matches with constraints can't work, then process the set without constraints.
if (endpointsWithoutConstraint.Count == 0)
{
return null;
}
else
{
return EvaluateEndpointConstraintsCore(context, endpointsWithoutConstraint, order);
}
}
private static class Log
{
private static readonly Action<ILogger, PathString, IEnumerable<string>, Exception> _matchAmbiguous = LoggerMessage.Define<PathString, IEnumerable<string>>(
LogLevel.Error,
new EventId(1, "MatchAmbiguous"),
"Request matched multiple endpoints for request path '{Path}'. Matching endpoints: {AmbiguousEndpoints}");
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.Endpoint.DisplayName), null);
}
}
}
}
}

View File

@ -1,75 +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.Collections.ObjectModel;
using Microsoft.AspNetCore.Routing.Metadata;
namespace Microsoft.AspNetCore.Routing.EndpointConstraints
{
public class HttpMethodEndpointConstraint : IEndpointConstraint, IHttpMethodMetadata
{
public static readonly int HttpMethodConstraintOrder = 100;
private readonly IReadOnlyList<string> _httpMethods;
// Empty collection means any method will be accepted.
public HttpMethodEndpointConstraint(IEnumerable<string> httpMethods)
{
if (httpMethods == null)
{
throw new ArgumentNullException(nameof(httpMethods));
}
var methods = new List<string>();
foreach (var method in httpMethods)
{
if (string.IsNullOrEmpty(method))
{
throw new ArgumentException("httpMethod cannot be null or empty");
}
methods.Add(method);
}
_httpMethods = new ReadOnlyCollection<string>(methods);
}
public IEnumerable<string> HttpMethods => _httpMethods;
public int Order => HttpMethodConstraintOrder;
IReadOnlyList<string> IHttpMethodMetadata.HttpMethods => _httpMethods;
bool IHttpMethodMetadata.AcceptCorsPreflight => false;
public virtual bool Accept(EndpointConstraintContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (_httpMethods.Count == 0)
{
return true;
}
var request = context.HttpContext.Request;
var method = request.Method;
for (var i = 0; i < _httpMethods.Count; i++)
{
var supportedMethod = _httpMethods[i];
if (string.Equals(supportedMethod, method, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
}
}
}

View File

@ -1,190 +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 Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Routing.EndpointConstraints
{
public class EndpointConstraintContext
{
public IReadOnlyList<EndpointSelectorCandidate> Candidates { get; set; }
public EndpointSelectorCandidate CurrentCandidate { get; set; }
public HttpContext HttpContext { get; set; }
}
public interface IEndpointConstraint : IEndpointConstraintMetadata
{
int Order { get; }
bool Accept(EndpointConstraintContext context);
}
public interface IEndpointConstraintMetadata
{
}
public readonly struct EndpointSelectorCandidate
{
public EndpointSelectorCandidate(
Endpoint endpoint,
int score,
RouteValueDictionary values,
IReadOnlyList<IEndpointConstraint> constraints)
{
if (endpoint == null)
{
throw new ArgumentNullException(nameof(endpoint));
}
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; }
}
public class EndpointConstraintItem
{
public EndpointConstraintItem(IEndpointConstraintMetadata metadata)
{
if (metadata == null)
{
throw new ArgumentNullException(nameof(metadata));
}
Metadata = metadata;
}
public IEndpointConstraint Constraint { get; set; }
public IEndpointConstraintMetadata Metadata { get; }
public bool IsReusable { get; set; }
}
public interface IEndpointConstraintProvider
{
int Order { get; }
void OnProvidersExecuting(EndpointConstraintProviderContext context);
void OnProvidersExecuted(EndpointConstraintProviderContext context);
}
public class EndpointConstraintProviderContext
{
public EndpointConstraintProviderContext(
HttpContext context,
Endpoint endpoint,
IList<EndpointConstraintItem> items)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (endpoint == null)
{
throw new ArgumentNullException(nameof(endpoint));
}
if (items == null)
{
throw new ArgumentNullException(nameof(items));
}
HttpContext = context;
Endpoint = endpoint;
Results = items;
}
public HttpContext HttpContext { get; }
public Endpoint Endpoint { get; }
public IList<EndpointConstraintItem> Results { get; }
}
public class DefaultEndpointConstraintProvider : IEndpointConstraintProvider
{
/// <inheritdoc />
public int Order => -1000;
/// <inheritdoc />
public void OnProvidersExecuting(EndpointConstraintProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
for (var i = 0; i < context.Results.Count; i++)
{
ProvideConstraint(context.Results[i], context.HttpContext.RequestServices);
}
}
/// <inheritdoc />
public void OnProvidersExecuted(EndpointConstraintProviderContext context)
{
}
private void ProvideConstraint(EndpointConstraintItem item, IServiceProvider services)
{
// Don't overwrite anything that was done by a previous provider.
if (item.Constraint != null)
{
return;
}
if (item.Metadata is IEndpointConstraint constraint)
{
item.Constraint = constraint;
item.IsReusable = true;
return;
}
if (item.Metadata is IEndpointConstraintFactory factory)
{
item.Constraint = factory.CreateInstance(services);
item.IsReusable = factory.IsReusable;
return;
}
}
}
public interface IEndpointConstraintFactory : IEndpointConstraintMetadata
{
bool IsReusable { get; }
IEndpointConstraint CreateInstance(IServiceProvider services);
}
}

View File

@ -11,17 +11,34 @@ namespace Microsoft.AspNetCore.Routing.Matchers
{
internal class DefaultEndpointSelector : EndpointSelector
{
private readonly IEndpointSelectorPolicy[] _selectorPolicies;
public DefaultEndpointSelector(IEnumerable<MatcherPolicy> matcherPolicies)
{
if (matcherPolicies == null)
{
throw new ArgumentNullException(nameof(matcherPolicies));
}
_selectorPolicies = matcherPolicies.OrderBy(p => p.Order).OfType<IEndpointSelectorPolicy>().ToArray();
}
public override Task SelectAsync(
HttpContext httpContext,
IEndpointFeature feature,
CandidateSet candidates)
CandidateSet candidateSet)
{
for (var i = 0; i < _selectorPolicies.Length; i++)
{
_selectorPolicies[i].Apply(httpContext, candidateSet);
}
MatcherEndpoint endpoint = null;
RouteValueDictionary values = null;
int? foundScore = null;
for (var i = 0; i < candidates.Count; i++)
for (var i = 0; i < candidateSet.Count; i++)
{
ref var state = ref candidates[i];
ref var state = ref candidateSet[i];
var isValid = state.IsValidCandidate;
if (isValid && foundScore == null)
@ -46,7 +63,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
//
// Don't worry about the 'null == state.Score' case, it returns false.
ReportAmbiguity(candidates);
ReportAmbiguity(candidateSet);
// Unreachable, ReportAmbiguity always throws.
throw new NotSupportedException();

View File

@ -0,0 +1,12 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Routing.Matchers
{
public interface IEndpointSelectorPolicy
{
void Apply(HttpContext httpContext, CandidateSet candidates);
}
}

View File

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

View File

@ -4,6 +4,7 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing.Patterns;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Routing.Matchers
@ -174,6 +175,37 @@ test: /test3", ex.Message);
Assert.Null(feature.Endpoint);
}
[Fact]
public async Task SelectAsync_RunsEndpointSelectorPolicies()
{
// Arrange
var endpoints = new MatcherEndpoint[] { CreateEndpoint("/test1"), CreateEndpoint("/test2"), CreateEndpoint("/test3"), };
var scores = new int[] { 0, 0, 1 };
var candidateSet = CreateCandidateSet(endpoints, scores);
var policy = new Mock<MatcherPolicy>();
policy
.As<IEndpointSelectorPolicy>()
.Setup(p => p.Apply(It.IsAny<HttpContext>(), It.IsAny<CandidateSet>()))
.Callback<HttpContext, CandidateSet>((c, cs) =>
{
cs[1].IsValidCandidate = false;
});
candidateSet[0].IsValidCandidate = false;
candidateSet[1].IsValidCandidate = true;
candidateSet[2].IsValidCandidate = true;
var (httpContext, feature) = CreateContext();
var selector = CreateSelector(policy.Object);
// Act
await selector.SelectAsync(httpContext, feature, candidateSet);
// Assert
Assert.Same(endpoints[2], feature.Endpoint);
}
private static (HttpContext httpContext, IEndpointFeature feature) CreateContext()
{
return (new DefaultHttpContext(), new EndpointFeature());
@ -195,9 +227,9 @@ test: /test3", ex.Message);
return new CandidateSet(endpoints, scores);
}
private static DefaultEndpointSelector CreateSelector()
private static DefaultEndpointSelector CreateSelector(params MatcherPolicy[] policies)
{
return new DefaultEndpointSelector();
return new DefaultEndpointSelector(policies);
}
}
}

View File

@ -5,9 +5,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Routing.EndpointConstraints;
using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Routing.Matchers
@ -30,10 +28,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
public override Matcher Build()
{
var cache = new EndpointConstraintCache(
new CompositeEndpointDataSource(Array.Empty<EndpointDataSource>()),
new[] { new DefaultEndpointConstraintProvider(), });
var selector = new EndpointConstraintEndpointSelector(null, cache, NullLoggerFactory.Instance);
var selector = new DefaultEndpointSelector(Array.Empty<MatcherPolicy>());
var groups = _endpoints
.GroupBy(e => (e.Order, e.RoutePattern.InboundPrecedence, e.RoutePattern.RawText))

View File

@ -5,7 +5,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Routing.EndpointConstraints;
using Microsoft.AspNetCore.Routing.Internal;
using Microsoft.AspNetCore.Routing.Template;
using Microsoft.AspNetCore.Routing.Tree;
@ -36,10 +35,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
new DefaultObjectPool<UriBuildingContext>(new UriBuilderContextPooledObjectPolicy()),
new DefaultInlineConstraintResolver(Options.Create(new RouteOptions())));
var cache = new EndpointConstraintCache(
new CompositeEndpointDataSource(Array.Empty<EndpointDataSource>()),
new[] { new DefaultEndpointConstraintProvider(), });
var selector = new EndpointConstraintEndpointSelector(null, cache, NullLoggerFactory.Instance);
var selector = new DefaultEndpointSelector(Array.Empty<MatcherPolicy>());
var groups = _endpoints
.GroupBy(e => (e.Order, e.RoutePattern.InboundPrecedence, e.RoutePattern.RawText))