Skip to content

Commit

Permalink
Optimize attribute routing link generation
Browse files Browse the repository at this point in the history
  • Loading branch information
rynowak committed Aug 22, 2014
1 parent 2dcbbf7 commit 9faca78
Show file tree
Hide file tree
Showing 4 changed files with 498 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,16 @@

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; }
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
}
44 changes: 14 additions & 30 deletions src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRoute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
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;
Expand All @@ -19,17 +20,17 @@ public class AttributeRoute : IRouter
{
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"/>.
/// </summary>
/// <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)
Expand All @@ -48,11 +49,8 @@ public AttributeRoute(
.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);
Expand Down Expand Up @@ -87,31 +85,17 @@ public async Task RouteAsync([NotNull] RouteContext context)
/// <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)
{
if (!ContextHasSameValue(context, requiredLinkValue.Key, requiredLinkValue.Value))
{
isMatch = false;
break;
}
}

if (isMatch)
var path = GenerateLink(context, entry);
if (path != null)
{
var path = GenerateLink(context, entry);
if (path != null)
{
context.IsBound = true;
return path;
}
context.IsBound = true;
return path;
}
}

Expand Down
Loading

0 comments on commit 9faca78

Please sign in to comment.