// 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.AspNet.Http; using Microsoft.AspNet.Mvc.Abstractions; using Microsoft.AspNet.Mvc.ActionConstraints; using Microsoft.AspNet.Mvc.Core; using Microsoft.AspNet.Mvc.Logging; using Microsoft.AspNet.Mvc.Routing; using Microsoft.AspNet.Routing; using Microsoft.Extensions.Logging; namespace Microsoft.AspNet.Mvc.Infrastructure { /// /// A default implementation. /// public class DefaultActionSelector : IActionSelector { private readonly IActionSelectorDecisionTreeProvider _decisionTreeProvider; private readonly IActionConstraintProvider[] _actionConstraintProviders; private ILogger _logger; /// /// Creates a new . /// /// The . /// The set of instances. /// The . public DefaultActionSelector( IActionSelectorDecisionTreeProvider decisionTreeProvider, IEnumerable actionConstraintProviders, ILoggerFactory loggerFactory) { _decisionTreeProvider = decisionTreeProvider; _actionConstraintProviders = actionConstraintProviders.OrderBy(item => item.Order).ToArray(); _logger = loggerFactory.CreateLogger(); } /// public ActionDescriptor Select(RouteContext context) { if (context == null) { throw new ArgumentNullException(nameof(context)); } var tree = _decisionTreeProvider.DecisionTree; var matchingRouteConstraints = tree.Select(context.RouteData.Values); var candidates = new List(); // Perf: Avoid allocations for (var i = 0; i < matchingRouteConstraints.Count; i++) { var action = matchingRouteConstraints[i]; var constraints = GetConstraints(context.HttpContext, action); candidates.Add(new ActionSelectorCandidate(action, constraints)); } var matchingActionConstraints = EvaluateActionConstraints(context, candidates, startingOrder: null); List matchingActions = null; if (matchingActionConstraints != null) { matchingActions = new List(matchingActionConstraints.Count); // Perf: Avoid allocations for (var i = 0; i < matchingActionConstraints.Count; i++) { var candidate = matchingActionConstraints[i]; matchingActions.Add(candidate.Action); } } var finalMatches = SelectBestActions(matchingActions); 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)); _logger.AmbiguousActions(actionNames); var message = Resources.FormatDefaultActionSelector_AmbiguousActions( Environment.NewLine, actionNames); throw new AmbiguousActionException(message); } } /// /// Returns the set of best matching actions. /// /// The set of actions that satisfy all constraints. /// A list of the best matching actions. protected virtual IReadOnlyList SelectBestActions(IReadOnlyList actions) { return actions; } private IReadOnlyList EvaluateActionConstraints( RouteContext context, IReadOnlyList 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(); var actionsWithoutConstraint = new List(); var constraintContext = new ActionConstraintContext(); constraintContext.Candidates = candidates; constraintContext.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; _logger.ConstraintMismatch( 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 = EvaluateActionConstraints(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 EvaluateActionConstraints(context, actionsWithoutConstraint, order); } } private IReadOnlyList GetConstraints(HttpContext httpContext, ActionDescriptor action) { if (action.ActionConstraints == null || action.ActionConstraints.Count == 0) { return null; } var items = new List(action.ActionConstraints.Count); for (var i = 0; i < action.ActionConstraints.Count; i++) { items.Add(new ActionConstraintItem(action.ActionConstraints[i])); } var context = new ActionConstraintProviderContext(httpContext, action, items); for (var i = 0; i < _actionConstraintProviders.Length; i++) { _actionConstraintProviders[i].OnProvidersExecuting(context); } for (var i = _actionConstraintProviders.Length - 1; i >= 0; i--) { _actionConstraintProviders[i].OnProvidersExecuted(context); } var count = 0; for (var i = 0; i < context.Results.Count; i++) { if (context.Results[i].Constraint != null) { count++; } } if (count == 0) { return null; } var results = new IActionConstraint[count]; for (int i = 0, j = 0; i < context.Results.Count; i++) { var constraint = context.Results[i].Constraint; if (constraint != null) { results[j++] = constraint; } } return results; } } }