aspnetcore/src/Microsoft.AspNetCore.Routing/EndpointConstraints/EndpointConstraintEndpointS...

265 lines
9.8 KiB
C#

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