277 lines
11 KiB
C#
277 lines
11 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.Mvc.Abstractions;
|
|
using Microsoft.AspNetCore.Mvc.ActionConstraints;
|
|
using Microsoft.AspNetCore.Mvc.Internal;
|
|
using Microsoft.AspNetCore.Routing;
|
|
using Microsoft.AspNetCore.Routing.Matching;
|
|
|
|
namespace Microsoft.AspNetCore.Mvc.Routing
|
|
{
|
|
// This is a bridge that allows us to execute IActionConstraint instance when
|
|
// used with Matcher.
|
|
internal class ActionConstraintMatcherPolicy : MatcherPolicy, IEndpointSelectorPolicy
|
|
{
|
|
private static readonly IReadOnlyList<Endpoint> EmptyEndpoints = Array.Empty<Endpoint>();
|
|
|
|
// We need to be able to run IActionConstraints on Endpoints that aren't associated
|
|
// with an action. This is a sentinel value we use when the endpoint isn't from MVC.
|
|
internal static readonly ActionDescriptor NonAction = new ActionDescriptor();
|
|
|
|
private readonly ActionConstraintCache _actionConstraintCache;
|
|
|
|
public ActionConstraintMatcherPolicy(ActionConstraintCache actionConstraintCache)
|
|
{
|
|
_actionConstraintCache = actionConstraintCache;
|
|
}
|
|
|
|
// Run really late.
|
|
public override int Order => 100000;
|
|
|
|
public bool AppliesToEndpoints(IReadOnlyList<Endpoint> endpoints)
|
|
{
|
|
if (endpoints == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(endpoints));
|
|
}
|
|
|
|
// We can skip over action constraints when they aren't any for this set
|
|
// of endpoints. This happens once on startup so it removes this component
|
|
// from the code path in most scenarios.
|
|
for (var i = 0; i < endpoints.Count; i++)
|
|
{
|
|
var endpoint = endpoints[i];
|
|
var action = endpoint.Metadata.GetMetadata<ActionDescriptor>();
|
|
if (action?.ActionConstraints?.Count > 0 && HasSignificantActionConstraint(action))
|
|
{
|
|
// We need to check for some specific action constraint implementations.
|
|
// We've implemented consumes, and HTTP method support inside endpoint routing, so
|
|
// we don't need to run an 'action constraint phase' if those are the only constraints.
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
|
|
bool HasSignificantActionConstraint(ActionDescriptor a)
|
|
{
|
|
for (var i = 0; i < a.ActionConstraints.Count; i++)
|
|
{
|
|
var actionConstraint = a.ActionConstraints[i];
|
|
if (actionConstraint.GetType() == typeof(HttpMethodActionConstraint))
|
|
{
|
|
// This one is OK, we implement this in endpoint routing.
|
|
}
|
|
else if (actionConstraint.GetType().FullName == "Microsoft.AspNetCore.Mvc.Cors.Internal.CorsHttpMethodActionConstraint")
|
|
{
|
|
// This one is OK, we implement this in endpoint routing.
|
|
}
|
|
else if (actionConstraint.GetType() == typeof(ConsumesAttribute))
|
|
{
|
|
// This one is OK, we implement this in endpoint routing.
|
|
}
|
|
else
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public Task ApplyAsync(HttpContext httpContext, EndpointSelectorContext context, CandidateSet candidateSet)
|
|
{
|
|
var finalMatches = EvaluateActionConstraints(httpContext, candidateSet);
|
|
|
|
// We've computed the set of actions that still apply (and their indices)
|
|
// First, mark everything as invalid, and then mark everything in the matching
|
|
// set as valid. This is O(2n) vs O(n**2)
|
|
for (var i = 0; i < candidateSet.Count; i++)
|
|
{
|
|
candidateSet.SetValidity(i, false);
|
|
}
|
|
|
|
if (finalMatches != null)
|
|
{
|
|
for (var i = 0; i < finalMatches.Count; i++)
|
|
{
|
|
candidateSet.SetValidity(finalMatches[i].index, true);
|
|
}
|
|
}
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
// This is almost the same as the code in ActionSelector, but we can't really share the logic
|
|
// because we need to track the index of each candidate - and, each candidate has its own route
|
|
// values.
|
|
private IReadOnlyList<(int index, ActionSelectorCandidate candidate)> EvaluateActionConstraints(
|
|
HttpContext httpContext,
|
|
CandidateSet candidateSet)
|
|
{
|
|
var items = new List<(int index, ActionSelectorCandidate candidate)>();
|
|
|
|
// We want to execute a group at a time (based on score) so keep track of the score that we've seen.
|
|
int? score = null;
|
|
|
|
// Perf: Avoid allocations
|
|
for (var i = 0; i < candidateSet.Count; i++)
|
|
{
|
|
if (candidateSet.IsValidCandidate(i))
|
|
{
|
|
ref var candidate = ref candidateSet[i];
|
|
if (score != null && score != candidate.Score)
|
|
{
|
|
// This is the end of a group.
|
|
var matches = EvaluateActionConstraintsCore(httpContext, candidateSet, items, startingOrder: null);
|
|
if (matches?.Count > 0)
|
|
{
|
|
return matches;
|
|
}
|
|
|
|
// If we didn't find matches, then reset.
|
|
items.Clear();
|
|
}
|
|
|
|
score = candidate.Score;
|
|
|
|
// If we get here, this is either the first endpoint or the we just (unsuccessfully)
|
|
// executed constraints for a group.
|
|
//
|
|
// So keep adding constraints.
|
|
var endpoint = candidate.Endpoint;
|
|
var actionDescriptor = endpoint.Metadata.GetMetadata<ActionDescriptor>();
|
|
|
|
IReadOnlyList<IActionConstraint> constraints = Array.Empty<IActionConstraint>();
|
|
if (actionDescriptor != null)
|
|
{
|
|
constraints = _actionConstraintCache.GetActionConstraints(httpContext, actionDescriptor);
|
|
}
|
|
|
|
// Capture the index. We need this later to look up the endpoint/route values.
|
|
items.Add((i, new ActionSelectorCandidate(actionDescriptor ?? NonAction, constraints)));
|
|
}
|
|
}
|
|
|
|
// Handle residue
|
|
return EvaluateActionConstraintsCore(httpContext, candidateSet, items, startingOrder: null);
|
|
}
|
|
|
|
private IReadOnlyList<(int index, ActionSelectorCandidate candidate)> EvaluateActionConstraintsCore(
|
|
HttpContext httpContext,
|
|
CandidateSet candidateSet,
|
|
IReadOnlyList<(int index, ActionSelectorCandidate candidate)> items,
|
|
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 < items.Count; i++)
|
|
{
|
|
var item = items[i];
|
|
var constraints = item.candidate.Constraints;
|
|
if (constraints != null)
|
|
{
|
|
for (var j = 0; j < constraints.Count; j++)
|
|
{
|
|
var constraint = 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 items;
|
|
}
|
|
|
|
// 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<(int index, ActionSelectorCandidate candidate)>();
|
|
var endpointsWithoutConstraint = new List<(int index, ActionSelectorCandidate candidate)>();
|
|
|
|
var constraintContext = new ActionConstraintContext
|
|
{
|
|
Candidates = items.Select(i => i.candidate).ToArray()
|
|
};
|
|
|
|
// Perf: Avoid allocations
|
|
for (var i = 0; i < items.Count; i++)
|
|
{
|
|
var item = items[i];
|
|
var isMatch = true;
|
|
var foundMatchingConstraint = false;
|
|
|
|
var constraints = item.candidate.Constraints;
|
|
if (constraints != null)
|
|
{
|
|
constraintContext.CurrentCandidate = item.candidate;
|
|
for (var j = 0; j < constraints.Count; j++)
|
|
{
|
|
var constraint = constraints[j];
|
|
if (constraint.Order == order)
|
|
{
|
|
foundMatchingConstraint = true;
|
|
|
|
// Before we run the constraint, we need to initialize the route values.
|
|
// In endpoint routing, the route values are per-endpoint.
|
|
constraintContext.RouteContext = new RouteContext(httpContext)
|
|
{
|
|
RouteData = new RouteData(candidateSet[item.index].Values),
|
|
};
|
|
if (!constraint.Accept(constraintContext))
|
|
{
|
|
isMatch = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (isMatch && foundMatchingConstraint)
|
|
{
|
|
endpointsWithConstraint.Add(item);
|
|
}
|
|
else if (isMatch)
|
|
{
|
|
endpointsWithoutConstraint.Add(item);
|
|
}
|
|
}
|
|
|
|
// If we have matches with constraints, those are better so try to keep processing those
|
|
if (endpointsWithConstraint.Count > 0)
|
|
{
|
|
var matches = EvaluateActionConstraintsCore(httpContext, candidateSet, 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 EvaluateActionConstraintsCore(httpContext, candidateSet, endpointsWithoutConstraint, order);
|
|
}
|
|
}
|
|
}
|
|
}
|