aspnetcore/src/Microsoft.AspNet.Mvc.Core/Routing/ActionSelectorDecisionTree.cs

188 lines
6.8 KiB
C#

// Copyright (c) Microsoft Open Technologies, Inc. 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.ComponentModel;
using System.Diagnostics;
using Microsoft.AspNet.Mvc.Internal.DecisionTree;
namespace Microsoft.AspNet.Mvc.Routing
{
/// <inheritdoc />
public class ActionSelectionDecisionTree : IActionSelectionDecisionTree
{
private readonly DecisionTreeNode<ActionDescriptor> _root;
/// <summary>
/// Creates a new <see cref="ActionSelectionDecisionTree"/>.
/// </summary>
/// <param name="actions">The <see cref="ActionDescriptorsCollection"/>.</param>
public ActionSelectionDecisionTree(ActionDescriptorsCollection actions)
{
Version = actions.Version;
_root = DecisionTreeBuilder<ActionDescriptor>.GenerateTree(
actions.Items,
new ActionDescriptorClassifier());
}
/// <inheritdoc />
public int Version { get; private set; }
/// <inheritdoc />
public IReadOnlyList<ActionDescriptor> Select(IDictionary<string, object> routeValues)
{
var results = new List<ActionDescriptor>();
Walk(results, routeValues, _root);
// If we have a match that isn't using catch-all, then it's considered better than matches with catch all
// so filter those out.
var hasNonCatchAll = false;
// The common case for MVC has no catch-alls, so avoid allocating.
List<ActionDescriptor> filtered = null;
foreach (var action in results)
{
var actionHasCatchAll = false;
if (action.RouteConstraints != null)
{
foreach (var constraint in action.RouteConstraints)
{
if (constraint.KeyHandling == RouteKeyHandling.CatchAll)
{
actionHasCatchAll = true;
break;
}
}
}
if (hasNonCatchAll && actionHasCatchAll)
{
// Do nothing - we've already found a better match.
}
else if (actionHasCatchAll)
{
if (filtered == null)
{
filtered = new List<ActionDescriptor>();
}
filtered.Add(action);
}
else if (hasNonCatchAll)
{
Debug.Assert(filtered != null);
filtered.Add(action);
}
else
{
// This is the first non-catch-all we've found.
hasNonCatchAll = true;
if (filtered == null)
{
filtered = new List<ActionDescriptor>();
}
else
{
filtered.Clear();
}
filtered.Add(action);
}
}
return filtered ?? results;
}
private void Walk(
List<ActionDescriptor> results,
IDictionary<string, object> routeValues,
DecisionTreeNode<ActionDescriptor> node)
{
for (var i = 0; i < node.Matches.Count; i++)
{
results.Add(node.Matches[i]);
}
for (var i = 0; i < node.Criteria.Count; i++)
{
var criterion = node.Criteria[i];
var key = criterion.Key;
object value;
var hasValue = routeValues.TryGetValue(key, out value);
DecisionTreeNode<ActionDescriptor> branch;
if (criterion.Branches.TryGetValue(value ?? string.Empty, out branch))
{
Walk(results, routeValues, branch);
}
// If there's a fallback node we always need to process it when we have a value. We'll prioritize
// non-fallback matches later in the process.
if (hasValue && criterion.Fallback != null)
{
Walk(results, routeValues, criterion.Fallback);
}
}
}
private class ActionDescriptorClassifier : IClassifier<ActionDescriptor>
{
public ActionDescriptorClassifier()
{
ValueComparer = new RouteValueEqualityComparer();
}
public IEqualityComparer<object> ValueComparer { get; private set; }
public IDictionary<string, DecisionCriterionValue> GetCriteria(ActionDescriptor item)
{
var results = new Dictionary<string, DecisionCriterionValue>(StringComparer.OrdinalIgnoreCase);
if (item.RouteConstraints != null)
{
foreach (var constraint in item.RouteConstraints)
{
DecisionCriterionValue value;
if (constraint.KeyHandling == RouteKeyHandling.CatchAll)
{
value = new DecisionCriterionValue(value: null, isCatchAll: true);
}
else if (constraint.KeyHandling == RouteKeyHandling.DenyKey)
{
// null and string.Empty are equivalent for route values, so just treat nulls as
// string.Empty.
value = new DecisionCriterionValue(value: string.Empty, isCatchAll: false);
}
else if (constraint.KeyHandling == RouteKeyHandling.RequireKey)
{
value = new DecisionCriterionValue(value: constraint.RouteValue, isCatchAll: false);
}
else
{
// We'd already have failed before getting here. The RouteDataActionConstraint constructor
// would throw.
#if ASPNET50
throw new InvalidEnumArgumentException(
"item",
(int)constraint.KeyHandling,
typeof(RouteKeyHandling));
#else
throw new ArgumentOutOfRangeException("item");
#endif
}
results.Add(constraint.RouteKey, value);
}
}
return results;
}
}
}
}