Optimize attribute routing link generation
This commit is contained in:
parent
2dcbbf70b0
commit
9faca78a84
|
|
@ -5,10 +5,16 @@ using System.Collections.Generic;
|
|||
|
||||
namespace Microsoft.AspNet.Mvc.Internal.DecisionTree
|
||||
{
|
||||
// Data structure representing a node in a decision tree. These are created in DecisionTreeBuilder
|
||||
// and walked to find a set of items matching some input criteria.
|
||||
public class DecisionTreeNode<TItem>
|
||||
{
|
||||
// The list of matches for the current node. This represents a set of items that have had all
|
||||
// of their criteria matched if control gets to this point in the tree.
|
||||
public List<TItem> Matches { get; set; }
|
||||
|
||||
// Additional criteria that further branch out from this node. Walk these to fine more items
|
||||
// matching the input data.
|
||||
public List<DecisionCriterion<TItem>> Criteria { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,147 @@
|
|||
// 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 Microsoft.AspNet.Mvc.Internal.DecisionTree;
|
||||
using Microsoft.AspNet.Mvc.Routing;
|
||||
using Microsoft.AspNet.Routing;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.Internal.Routing
|
||||
{
|
||||
// A decision tree that matches link generation entries based on route data.
|
||||
public class LinkGenerationDecisionTree
|
||||
{
|
||||
private readonly DecisionTreeNode<AttributeRouteLinkGenerationEntry> _root;
|
||||
|
||||
public LinkGenerationDecisionTree(IReadOnlyList<AttributeRouteLinkGenerationEntry> entries)
|
||||
{
|
||||
_root = DecisionTreeBuilder<AttributeRouteLinkGenerationEntry>.GenerateTree(
|
||||
entries,
|
||||
new AttributeRouteLinkGenerationEntryClassifier());
|
||||
}
|
||||
|
||||
public List<AttributeRouteLinkGenerationEntry> GetMatches(VirtualPathContext context)
|
||||
{
|
||||
var results = new List<AttributeRouteLinkGenerationEntry>();
|
||||
Walk(results, context, _root);
|
||||
results.Sort(AttributeRouteLinkGenerationEntryComparer.Instance);
|
||||
return results;
|
||||
}
|
||||
|
||||
// We need to recursively walk the decision tree based on the provided route data
|
||||
// (context.Values + context.AmbientValues) to find all entries that match. This process is
|
||||
// virtually identical to action selection.
|
||||
//
|
||||
// Each entry has a collection of 'required link values' that must be satisfied. These are
|
||||
// key-value pairs that make up the decision tree.
|
||||
//
|
||||
// A 'require link value' is considered satisfied IF:
|
||||
// 1. The value in context.Values matches the required value OR
|
||||
// 2. There is no value in context.Values and the value in context.AmbientValues matches OR
|
||||
// 3. The required value is 'null' and there is no value in context.Values.
|
||||
//
|
||||
// Ex:
|
||||
// entry requires { area = null, controller = Store, action = Buy }
|
||||
// context.Values = { controller = Store, action = Buy }
|
||||
// context.AmbientValues = { area = Help, controller = AboutStore, action = HowToBuyThings }
|
||||
//
|
||||
// In this case the entry is a match. The 'controller' and 'action' are both supplied by context.Values,
|
||||
// and the 'area' is satisfied because there's NOT a value in context.Values. It's OK to ignore ambient
|
||||
// values in link generation.
|
||||
//
|
||||
// If another entry existed like { area = Help, controller = Store, action = Buy }, this would also
|
||||
// match.
|
||||
//
|
||||
// The decision tree uses a tree data structure to execute these rules across all candidates at once.
|
||||
private void Walk(
|
||||
List<AttributeRouteLinkGenerationEntry> results,
|
||||
VirtualPathContext context,
|
||||
DecisionTreeNode<AttributeRouteLinkGenerationEntry> node)
|
||||
{
|
||||
// Any entries in node.Matches have had all their required values satisfied, so add them
|
||||
// to the results.
|
||||
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;
|
||||
if (context.Values.TryGetValue(key, out value))
|
||||
{
|
||||
DecisionTreeNode<AttributeRouteLinkGenerationEntry> branch;
|
||||
if (criterion.Branches.TryGetValue(value ?? string.Empty, out branch))
|
||||
{
|
||||
Walk(results, context, branch);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// If a value wasn't explicitly supplied, match BOTH the ambient value and the empty value
|
||||
// if an ambient value was supplied.
|
||||
DecisionTreeNode<AttributeRouteLinkGenerationEntry> branch;
|
||||
if (context.AmbientValues.TryGetValue(key, out value) &&
|
||||
!criterion.Branches.Comparer.Equals(value, string.Empty))
|
||||
{
|
||||
if (criterion.Branches.TryGetValue(value, out branch))
|
||||
{
|
||||
Walk(results, context, branch);
|
||||
}
|
||||
}
|
||||
|
||||
if (criterion.Branches.TryGetValue(string.Empty, out branch))
|
||||
{
|
||||
Walk(results, context, branch);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class AttributeRouteLinkGenerationEntryClassifier : IClassifier<AttributeRouteLinkGenerationEntry>
|
||||
{
|
||||
public AttributeRouteLinkGenerationEntryClassifier()
|
||||
{
|
||||
ValueComparer = new RouteValueEqualityComparer();
|
||||
}
|
||||
|
||||
public IEqualityComparer<object> ValueComparer { get; private set; }
|
||||
|
||||
public IDictionary<string, DecisionCriterionValue> GetCriteria(AttributeRouteLinkGenerationEntry item)
|
||||
{
|
||||
var results = new Dictionary<string, DecisionCriterionValue>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var kvp in item.RequiredLinkValues)
|
||||
{
|
||||
results.Add(kvp.Key, new DecisionCriterionValue(kvp.Value ?? string.Empty, isCatchAll: false));
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
private class AttributeRouteLinkGenerationEntryComparer : IComparer<AttributeRouteLinkGenerationEntry>
|
||||
{
|
||||
public static readonly AttributeRouteLinkGenerationEntryComparer Instance =
|
||||
new AttributeRouteLinkGenerationEntryComparer();
|
||||
|
||||
public int Compare(AttributeRouteLinkGenerationEntry x, AttributeRouteLinkGenerationEntry y)
|
||||
{
|
||||
if (x.Order != y.Order)
|
||||
{
|
||||
return x.Order.CompareTo(y.Order);
|
||||
}
|
||||
|
||||
if (x.Precedence != y.Precedence)
|
||||
{
|
||||
return x.Precedence.CompareTo(y.Precedence);
|
||||
}
|
||||
|
||||
return StringComparer.Ordinal.Compare(x.TemplateText, y.TemplateText);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNet.Mvc.Internal.Routing;
|
||||
using Microsoft.AspNet.Mvc.Logging;
|
||||
using Microsoft.AspNet.Routing;
|
||||
using Microsoft.AspNet.Routing.Template;
|
||||
|
|
@ -19,9 +20,9 @@ namespace Microsoft.AspNet.Mvc.Routing
|
|||
{
|
||||
private readonly IRouter _next;
|
||||
private readonly TemplateRoute[] _matchingRoutes;
|
||||
private readonly AttributeRouteLinkGenerationEntry[] _linkGenerationEntries;
|
||||
private ILogger _logger;
|
||||
private ILogger _constraintLogger;
|
||||
private readonly LinkGenerationDecisionTree _linkGenerationTree;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="AttributeRoute"/>.
|
||||
|
|
@ -29,7 +30,7 @@ namespace Microsoft.AspNet.Mvc.Routing
|
|||
/// <param name="next">The next router. Invoked when a route entry matches.</param>
|
||||
/// <param name="entries">The set of route entries.</param>
|
||||
public AttributeRoute(
|
||||
[NotNull] IRouter next,
|
||||
[NotNull] IRouter next,
|
||||
[NotNull] IEnumerable<AttributeRouteMatchingEntry> matchingEntries,
|
||||
[NotNull] IEnumerable<AttributeRouteLinkGenerationEntry> linkGenerationEntries,
|
||||
[NotNull] ILoggerFactory factory)
|
||||
|
|
@ -48,11 +49,8 @@ namespace Microsoft.AspNet.Mvc.Routing
|
|||
.Select(e => e.Route)
|
||||
.ToArray();
|
||||
|
||||
_linkGenerationEntries = linkGenerationEntries
|
||||
.OrderBy(o => o.Order)
|
||||
.ThenBy(e => e.Precedence)
|
||||
.ThenBy(e => e.TemplateText, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
// The decision tree will take care of ordering for these entries.
|
||||
_linkGenerationTree = new LinkGenerationDecisionTree(linkGenerationEntries.ToArray());
|
||||
|
||||
_logger = factory.Create<AttributeRoute>();
|
||||
_constraintLogger = factory.Create(typeof(RouteConstraintMatcher).FullName);
|
||||
|
|
@ -87,31 +85,17 @@ namespace Microsoft.AspNet.Mvc.Routing
|
|||
/// <inheritdoc />
|
||||
public string GetVirtualPath([NotNull] VirtualPathContext context)
|
||||
{
|
||||
// To generate a link, we iterate the collection of entries (in order of precedence) and execute
|
||||
// each one that matches the 'required link values' - which will typically be a value for action
|
||||
// and controller.
|
||||
//
|
||||
// Building a proper data structure to optimize this is tracked by #741
|
||||
foreach (var entry in _linkGenerationEntries)
|
||||
// The decision tree will give us back all entries that match the provided route data in the correct
|
||||
// order. We just need to iterate them and use the first one that can generate a link.
|
||||
var matches = _linkGenerationTree.GetMatches(context);
|
||||
|
||||
foreach (var entry in matches)
|
||||
{
|
||||
var isMatch = true;
|
||||
foreach (var requiredLinkValue in entry.RequiredLinkValues)
|
||||
var path = GenerateLink(context, entry);
|
||||
if (path != null)
|
||||
{
|
||||
if (!ContextHasSameValue(context, requiredLinkValue.Key, requiredLinkValue.Value))
|
||||
{
|
||||
isMatch = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (isMatch)
|
||||
{
|
||||
var path = GenerateLink(context, entry);
|
||||
if (path != null)
|
||||
{
|
||||
context.IsBound = true;
|
||||
return path;
|
||||
}
|
||||
context.IsBound = true;
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,331 @@
|
|||
// 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.Collections.Generic;
|
||||
using Microsoft.AspNet.Mvc.Routing;
|
||||
using Microsoft.AspNet.PipelineCore;
|
||||
using Microsoft.AspNet.Routing;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.Internal.Routing
|
||||
{
|
||||
public class LinkGenerationDecisionTreeTest
|
||||
{
|
||||
[Fact]
|
||||
public void SelectSingleEntry_NoCriteria()
|
||||
{
|
||||
// Arrange
|
||||
var entries = new List<AttributeRouteLinkGenerationEntry>();
|
||||
|
||||
var entry = CreateEntry(new { });
|
||||
entries.Add(entry);
|
||||
|
||||
var tree = new LinkGenerationDecisionTree(entries);
|
||||
|
||||
var context = CreateContext(new { });
|
||||
|
||||
// Act
|
||||
var matches = tree.GetMatches(context);
|
||||
|
||||
// Assert
|
||||
Assert.Same(entry, Assert.Single(matches));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectSingleEntry_MultipleCriteria()
|
||||
{
|
||||
// Arrange
|
||||
var entries = new List<AttributeRouteLinkGenerationEntry>();
|
||||
|
||||
var entry = CreateEntry(new { controller = "Store", action = "Buy" });
|
||||
entries.Add(entry);
|
||||
|
||||
var tree = new LinkGenerationDecisionTree(entries);
|
||||
|
||||
var context = CreateContext(new { controller = "Store", action = "Buy" });
|
||||
|
||||
// Act
|
||||
var matches = tree.GetMatches(context);
|
||||
|
||||
// Assert
|
||||
Assert.Same(entry, Assert.Single(matches));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectSingleEntry_MultipleCriteria_AmbientValues()
|
||||
{
|
||||
// Arrange
|
||||
var entries = new List<AttributeRouteLinkGenerationEntry>();
|
||||
|
||||
var entry = CreateEntry(new { controller = "Store", action = "Buy" });
|
||||
entries.Add(entry);
|
||||
|
||||
var tree = new LinkGenerationDecisionTree(entries);
|
||||
|
||||
var context = CreateContext(values: null, ambientValues: new { controller = "Store", action = "Buy" });
|
||||
|
||||
// Act
|
||||
var matches = tree.GetMatches(context);
|
||||
|
||||
// Assert
|
||||
Assert.Same(entry, Assert.Single(matches));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectSingleEntry_MultipleCriteria_Replaced()
|
||||
{
|
||||
// Arrange
|
||||
var entries = new List<AttributeRouteLinkGenerationEntry>();
|
||||
|
||||
var entry = CreateEntry(new { controller = "Store", action = "Buy" });
|
||||
entries.Add(entry);
|
||||
|
||||
var tree = new LinkGenerationDecisionTree(entries);
|
||||
|
||||
var context = CreateContext(
|
||||
values: new { action = "Buy" },
|
||||
ambientValues: new { controller = "Store", action = "Cart" });
|
||||
|
||||
// Act
|
||||
var matches = tree.GetMatches(context);
|
||||
|
||||
// Assert
|
||||
Assert.Same(entry, Assert.Single(matches));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectSingleEntry_MultipleCriteria_AmbientValue_Ignored()
|
||||
{
|
||||
// Arrange
|
||||
var entries = new List<AttributeRouteLinkGenerationEntry>();
|
||||
|
||||
var entry = CreateEntry(new { controller = "Store", action = (string)null });
|
||||
entries.Add(entry);
|
||||
|
||||
var tree = new LinkGenerationDecisionTree(entries);
|
||||
|
||||
var context = CreateContext(
|
||||
values: new { controller = "Store" },
|
||||
ambientValues: new { controller = "Store", action = "Buy" });
|
||||
|
||||
// Act
|
||||
var matches = tree.GetMatches(context);
|
||||
|
||||
// Assert
|
||||
Assert.Same(entry, Assert.Single(matches));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectSingleEntry_MultipleCriteria_NoMatch()
|
||||
{
|
||||
// Arrange
|
||||
var entries = new List<AttributeRouteLinkGenerationEntry>();
|
||||
|
||||
var entry = CreateEntry(new { controller = "Store", action = "Buy" });
|
||||
entries.Add(entry);
|
||||
|
||||
var tree = new LinkGenerationDecisionTree(entries);
|
||||
|
||||
var context = CreateContext(new { controller = "Store", action = "AddToCart" });
|
||||
|
||||
// Act
|
||||
var matches = tree.GetMatches(context);
|
||||
|
||||
// Assert
|
||||
Assert.Empty(matches);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectSingleEntry_MultipleCriteria_AmbientValue_NoMatch()
|
||||
{
|
||||
// Arrange
|
||||
var entries = new List<AttributeRouteLinkGenerationEntry>();
|
||||
|
||||
var entry = CreateEntry(new { controller = "Store", action = "Buy" });
|
||||
entries.Add(entry);
|
||||
|
||||
var tree = new LinkGenerationDecisionTree(entries);
|
||||
|
||||
var context = CreateContext(
|
||||
values: new { controller = "Store" },
|
||||
ambientValues: new { controller = "Store", action = "Cart" });
|
||||
|
||||
// Act
|
||||
var matches = tree.GetMatches(context);
|
||||
|
||||
// Assert
|
||||
Assert.Empty(matches);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectMultipleEntries_OneDoesntMatch()
|
||||
{
|
||||
// Arrange
|
||||
var entries = new List<AttributeRouteLinkGenerationEntry>();
|
||||
|
||||
var entry1 = CreateEntry(new { controller = "Store", action = "Buy" });
|
||||
entries.Add(entry1);
|
||||
|
||||
var entry2 = CreateEntry(new { controller = "Store", action = "Cart" });
|
||||
entries.Add(entry2);
|
||||
|
||||
var tree = new LinkGenerationDecisionTree(entries);
|
||||
|
||||
var context = CreateContext(
|
||||
values: new { controller = "Store" },
|
||||
ambientValues: new { controller = "Store", action = "Buy" });
|
||||
|
||||
// Act
|
||||
var matches = tree.GetMatches(context);
|
||||
|
||||
// Assert
|
||||
Assert.Same(entry1, Assert.Single(matches));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectMultipleEntries_BothMatch_CriteriaSubset()
|
||||
{
|
||||
// Arrange
|
||||
var entries = new List<AttributeRouteLinkGenerationEntry>();
|
||||
|
||||
var entry1 = CreateEntry(new { controller = "Store", action = "Buy" });
|
||||
entries.Add(entry1);
|
||||
|
||||
var entry2 = CreateEntry(new { controller = "Store" });
|
||||
entry2.Order = 1;
|
||||
entries.Add(entry2);
|
||||
|
||||
var tree = new LinkGenerationDecisionTree(entries);
|
||||
|
||||
var context = CreateContext(
|
||||
values: new { controller = "Store" },
|
||||
ambientValues: new { controller = "Store", action = "Buy" });
|
||||
|
||||
// Act
|
||||
var matches = tree.GetMatches(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(entries, matches);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectMultipleEntries_BothMatch_NonOverlappingCriteria()
|
||||
{
|
||||
// Arrange
|
||||
var entries = new List<AttributeRouteLinkGenerationEntry>();
|
||||
|
||||
var entry1 = CreateEntry(new { controller = "Store", action = "Buy" });
|
||||
entries.Add(entry1);
|
||||
|
||||
var entry2 = CreateEntry(new { slug = "1234" });
|
||||
entry2.Order = 1;
|
||||
entries.Add(entry2);
|
||||
|
||||
var tree = new LinkGenerationDecisionTree(entries);
|
||||
|
||||
var context = CreateContext(new { controller = "Store", action = "Buy", slug = "1234" });
|
||||
|
||||
// Act
|
||||
var matches = tree.GetMatches(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(entries, matches);
|
||||
}
|
||||
|
||||
// Precedence is ignored for sorting because they have different order
|
||||
[Fact]
|
||||
public void SelectMultipleEntries_BothMatch_OrderedByOrder()
|
||||
{
|
||||
// Arrange
|
||||
var entries = new List<AttributeRouteLinkGenerationEntry>();
|
||||
|
||||
var entry1 = CreateEntry(new { controller = "Store", action = "Buy" });
|
||||
entry1.Precedence = 1;
|
||||
entries.Add(entry1);
|
||||
|
||||
var entry2 = CreateEntry(new { controller = "Store", action = "Buy" });
|
||||
entry2.Order = 1;
|
||||
entry2.Precedence = 0;
|
||||
entries.Add(entry2);
|
||||
|
||||
var tree = new LinkGenerationDecisionTree(entries);
|
||||
|
||||
var context = CreateContext(new { controller = "Store", action = "Buy" });
|
||||
|
||||
// Act
|
||||
var matches = tree.GetMatches(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(entries, matches);
|
||||
}
|
||||
|
||||
// Precedence is used for sorting because they have the same order
|
||||
[Fact]
|
||||
public void SelectMultipleEntries_BothMatch_OrderedByPrecedence()
|
||||
{
|
||||
// Arrange
|
||||
var entries = new List<AttributeRouteLinkGenerationEntry>();
|
||||
|
||||
var entry1 = CreateEntry(new { controller = "Store", action = "Buy" });
|
||||
entry1.Precedence = 0;
|
||||
entries.Add(entry1);
|
||||
|
||||
var entry2 = CreateEntry(new { controller = "Store", action = "Buy" });
|
||||
entry2.Precedence = 1;
|
||||
entries.Add(entry2);
|
||||
|
||||
var tree = new LinkGenerationDecisionTree(entries);
|
||||
|
||||
var context = CreateContext(new { controller = "Store", action = "Buy" });
|
||||
|
||||
// Act
|
||||
var matches = tree.GetMatches(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(entries, matches);
|
||||
}
|
||||
|
||||
// Template is used for sorting because they have the same order
|
||||
[Fact]
|
||||
public void SelectMultipleEntries_BothMatch_OrderedByTemplate()
|
||||
{
|
||||
// Arrange
|
||||
var entries = new List<AttributeRouteLinkGenerationEntry>();
|
||||
|
||||
var entry1 = CreateEntry(new { controller = "Store", action = "Buy" });
|
||||
entry1.TemplateText = "a";
|
||||
entries.Add(entry1);
|
||||
|
||||
var entry2 = CreateEntry(new { controller = "Store", action = "Buy" });
|
||||
entry2.TemplateText = "b";
|
||||
entries.Add(entry2);
|
||||
|
||||
var tree = new LinkGenerationDecisionTree(entries);
|
||||
|
||||
var context = CreateContext(new { controller = "Store", action = "Buy" });
|
||||
|
||||
// Act
|
||||
var matches = tree.GetMatches(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(entries, matches);
|
||||
}
|
||||
|
||||
private AttributeRouteLinkGenerationEntry CreateEntry(object requiredValues)
|
||||
{
|
||||
var entry = new AttributeRouteLinkGenerationEntry();
|
||||
entry.RequiredLinkValues = new RouteValueDictionary(requiredValues);
|
||||
return entry;
|
||||
}
|
||||
|
||||
private VirtualPathContext CreateContext(object values, object ambientValues = null)
|
||||
{
|
||||
var context = new VirtualPathContext(
|
||||
new DefaultHttpContext(),
|
||||
new RouteValueDictionary(ambientValues),
|
||||
new RouteValueDictionary(values));
|
||||
|
||||
return context;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue