|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#nullable enable
using System.Linq;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ActionConstraints;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Logging;
using Resources = Microsoft.AspNetCore.Mvc.Core.Resources;
namespace Microsoft.AspNetCore.Mvc.Infrastructure;
/// <summary>
/// A default <see cref="IActionSelector"/> implementation.
/// </summary>
internal sealed partial class ActionSelector : IActionSelector
{
private readonly IActionDescriptorCollectionProvider _actionDescriptorCollectionProvider;
private readonly ActionConstraintCache _actionConstraintCache;
private readonly ILogger _logger;
private ActionSelectionTable<ActionDescriptor>? _cache;
/// <summary>
/// Creates a new <see cref="ActionSelector"/>.
/// </summary>
/// <param name="actionDescriptorCollectionProvider">
/// The <see cref="IActionDescriptorCollectionProvider"/>.
/// </param>
/// <param name="actionConstraintCache">The <see cref="ActionConstraintCache"/> that
/// providers a set of <see cref="IActionConstraint"/> instances.</param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
public ActionSelector(
IActionDescriptorCollectionProvider actionDescriptorCollectionProvider,
ActionConstraintCache actionConstraintCache,
ILoggerFactory loggerFactory)
{
_actionDescriptorCollectionProvider = actionDescriptorCollectionProvider;
_logger = loggerFactory.CreateLogger<ActionSelector>();
_actionConstraintCache = actionConstraintCache;
}
private ActionSelectionTable<ActionDescriptor> Current
{
get
{
var actions = _actionDescriptorCollectionProvider.ActionDescriptors;
var cache = Volatile.Read(ref _cache);
if (cache != null && cache.Version == actions.Version)
{
return cache;
}
cache = ActionSelectionTable<ActionDescriptor>.Create(actions);
Volatile.Write(ref _cache, cache);
return cache;
}
}
public IReadOnlyList<ActionDescriptor> SelectCandidates(RouteContext context)
{
ArgumentNullException.ThrowIfNull(context);
var cache = Current;
var matches = cache.Select(context.RouteData.Values);
if (matches.Count > 0)
{
return matches;
}
_logger.NoActionsMatched(context.RouteData.Values);
return matches;
}
public ActionDescriptor? SelectBestCandidate(RouteContext context, IReadOnlyList<ActionDescriptor> candidates)
{
ArgumentNullException.ThrowIfNull(context);
ArgumentNullException.ThrowIfNull(candidates);
var finalMatches = EvaluateActionConstraints(context, candidates);
if (finalMatches == null || finalMatches.Count == 0)
{
return null;
}
else if (finalMatches.Count == 1)
{
var selectedAction = finalMatches[0];
return selectedAction;
}
else
{
var actionNames = string.Join(
Environment.NewLine,
finalMatches.Select(a => a.DisplayName));
Log.AmbiguousActions(_logger, actionNames);
var message = Resources.FormatDefaultActionSelector_AmbiguousActions(
Environment.NewLine,
actionNames);
throw new AmbiguousActionException(message);
}
}
private IReadOnlyList<ActionDescriptor>? EvaluateActionConstraints(
RouteContext context,
IReadOnlyList<ActionDescriptor> actions)
{
var actionsCount = actions.Count;
var candidates = new List<ActionSelectorCandidate>(actionsCount);
// Perf: Avoid allocations
for (var i = 0; i < actionsCount; i++)
{
var action = actions[i];
var constraints = _actionConstraintCache.GetActionConstraints(context.HttpContext, action);
candidates.Add(new ActionSelectorCandidate(action, constraints));
}
var matches = EvaluateActionConstraintsCore(context, candidates, startingOrder: null);
List<ActionDescriptor>? results = null;
if (matches != null)
{
var matchesCount = matches.Count;
results = new List<ActionDescriptor>(matchesCount);
// Perf: Avoid allocations
for (var i = 0; i < matchesCount; i++)
{
var candidate = matches[i];
results.Add(candidate.Action);
}
}
return results;
}
private IReadOnlyList<ActionSelectorCandidate>? EvaluateActionConstraintsCore(
RouteContext context,
IReadOnlyList<ActionSelectorCandidate> 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 actions into those with and without a
// constraint for the current order.
var actionsWithConstraint = new List<ActionSelectorCandidate>();
var actionsWithoutConstraint = new List<ActionSelectorCandidate>();
var constraintContext = new ActionConstraintContext
{
Candidates = candidates,
RouteContext = 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;
Log.ConstraintMismatch(
_logger,
candidate.Action.DisplayName,
candidate.Action.Id,
constraint);
break;
}
}
}
}
if (isMatch && foundMatchingConstraint)
{
actionsWithConstraint.Add(candidate);
}
else if (isMatch)
{
actionsWithoutConstraint.Add(candidate);
}
}
// If we have matches with constraints, those are better so try to keep processing those
if (actionsWithConstraint.Count > 0)
{
var matches = EvaluateActionConstraintsCore(context, actionsWithConstraint, order);
if (matches?.Count > 0)
{
return matches;
}
}
// If the set of matches with constraints can't work, then process the set without constraints.
if (actionsWithoutConstraint.Count == 0)
{
return null;
}
else
{
return EvaluateActionConstraintsCore(context, actionsWithoutConstraint, order);
}
}
private static partial class Log
{
[LoggerMessage(1, LogLevel.Error, "Request matched multiple actions resulting in ambiguity. Matching actions: {AmbiguousActions}", EventName = "AmbiguousActions")]
public static partial void AmbiguousActions(ILogger logger, string ambiguousActions);
[LoggerMessage(2, LogLevel.Debug, "Action '{ActionName}' with id '{ActionId}' did not match the constraint '{ActionConstraint}'", EventName = "ConstraintMismatch")]
public static partial void ConstraintMismatch(ILogger logger, string? actionName, string actionId, IActionConstraint actionConstraint);
}
}
|