This repository has been archived by the owner on Nov 27, 2018. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 122
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
f6616db
commit 4d69ad0
Showing
33 changed files
with
4,512 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
16 changes: 16 additions & 0 deletions
16
src/Microsoft.AspNet.Routing.DecisionTree.Sources/DecisionCriterion.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
// 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.Collections.Generic; | ||
|
||
namespace Microsoft.AspNet.Routing.DecisionTree | ||
{ | ||
internal class DecisionCriterion<TItem> | ||
{ | ||
public string Key { get; set; } | ||
|
||
public Dictionary<object, DecisionTreeNode<TItem>> Branches { get; set; } | ||
|
||
public DecisionTreeNode<TItem> Fallback { get; set; } | ||
} | ||
} |
27 changes: 27 additions & 0 deletions
27
src/Microsoft.AspNet.Routing.DecisionTree.Sources/DecisionCriterionValue.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
// 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. | ||
|
||
namespace Microsoft.AspNet.Routing.DecisionTree | ||
{ | ||
internal struct DecisionCriterionValue | ||
{ | ||
private readonly bool _isCatchAll; | ||
private readonly object _value; | ||
|
||
public DecisionCriterionValue(object value, bool isCatchAll) | ||
{ | ||
_value = value; | ||
_isCatchAll = isCatchAll; | ||
} | ||
|
||
public bool IsCatchAll | ||
{ | ||
get { return _isCatchAll; } | ||
} | ||
|
||
public object Value | ||
{ | ||
get { return _value; } | ||
} | ||
} | ||
} |
34 changes: 34 additions & 0 deletions
34
src/Microsoft.AspNet.Routing.DecisionTree.Sources/DecisionCriterionValueEqualityComparer.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
// 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.Collections.Generic; | ||
|
||
namespace Microsoft.AspNet.Routing.DecisionTree | ||
{ | ||
internal class DecisionCriterionValueEqualityComparer : IEqualityComparer<DecisionCriterionValue> | ||
{ | ||
public DecisionCriterionValueEqualityComparer(IEqualityComparer<object> innerComparer) | ||
{ | ||
InnerComparer = innerComparer; | ||
} | ||
|
||
public IEqualityComparer<object> InnerComparer { get; private set; } | ||
|
||
public bool Equals(DecisionCriterionValue x, DecisionCriterionValue y) | ||
{ | ||
return x.IsCatchAll == y.IsCatchAll || InnerComparer.Equals(x.Value, y.Value); | ||
} | ||
|
||
public int GetHashCode(DecisionCriterionValue obj) | ||
{ | ||
if (obj.IsCatchAll) | ||
{ | ||
return 0; | ||
} | ||
else | ||
{ | ||
return InnerComparer.GetHashCode(obj.Value); | ||
} | ||
} | ||
} | ||
} |
234 changes: 234 additions & 0 deletions
234
src/Microsoft.AspNet.Routing.DecisionTree.Sources/DecisionTreeBuilder.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,234 @@ | ||
// 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; | ||
|
||
namespace Microsoft.AspNet.Routing.DecisionTree | ||
{ | ||
// This code generates a minimal tree of decision criteria that map known categorical data | ||
// (key-value-pairs) to a set of inputs. Action Selection is the best example of how this | ||
// can be used, so the comments here will describe the process from the point-of-view, | ||
// though the decision tree is generally applicable to like-problems. | ||
// | ||
// Care has been taken here to keep the performance of building the data-structure at a | ||
// reasonable level, as this has an impact on startup cost for action selection. Additionally | ||
// we want to hold on to the minimal amount of memory needed once we've built the tree. | ||
// | ||
// Ex: | ||
// Given actions like the following, create a decision tree that will help action | ||
// selection work efficiently. | ||
// | ||
// Given any set of route data it should be possible to traverse the tree using the | ||
// presence our route data keys (like action), and whether or not they match any of | ||
// the known values for that route data key, to find the set of actions that match | ||
// the route data. | ||
// | ||
// Actions: | ||
// | ||
// { controller = "Home", action = "Index" } | ||
// { controller = "Products", action = "Index" } | ||
// { controller = "Products", action = "Buy" } | ||
// { area = "Admin", controller = "Users", action = "AddUser" } | ||
// | ||
// The generated tree looks like this (json-like-notation): | ||
// | ||
// { | ||
// action : { | ||
// "AddUser" : { | ||
// controller : { | ||
// "Users" : { | ||
// area : { | ||
// "Admin" : match { area = "Admin", controller = "Users", action = "AddUser" } | ||
// } | ||
// } | ||
// } | ||
// }, | ||
// "Buy" : { | ||
// controller : { | ||
// "Products" : { | ||
// area : { | ||
// null : match { controller = "Products", action = "Buy" } | ||
// } | ||
// } | ||
// } | ||
// }, | ||
// "Index" : { | ||
// controller : { | ||
// "Home" : { | ||
// area : { | ||
// null : match { controller = "Home", action = "Index" } | ||
// } | ||
// } | ||
// "Products" : { | ||
// area : { | ||
// "null" : match { controller = "Products", action = "Index" } | ||
// } | ||
// } | ||
// } | ||
// } | ||
// } | ||
// } | ||
internal static class DecisionTreeBuilder<TItem> | ||
{ | ||
public static DecisionTreeNode<TItem> GenerateTree(IReadOnlyList<TItem> items, IClassifier<TItem> classifier) | ||
{ | ||
var itemDescriptors = new List<ItemDescriptor<TItem>>(); | ||
for (var i = 0; i < items.Count; i++) | ||
{ | ||
itemDescriptors.Add(new ItemDescriptor<TItem>() | ||
{ | ||
Criteria = classifier.GetCriteria(items[i]), | ||
Index = i, | ||
Item = items[i], | ||
}); | ||
} | ||
|
||
var comparer = new DecisionCriterionValueEqualityComparer(classifier.ValueComparer); | ||
return GenerateNode( | ||
new TreeBuilderContext(), | ||
comparer, | ||
itemDescriptors); | ||
} | ||
|
||
private static DecisionTreeNode<TItem> GenerateNode( | ||
TreeBuilderContext context, | ||
DecisionCriterionValueEqualityComparer comparer, | ||
IList<ItemDescriptor<TItem>> items) | ||
{ | ||
// The extreme use of generics here is intended to reduce the number of intermediate | ||
// allocations of wrapper classes. Performance testing found that building these trees allocates | ||
// significant memory that we can avoid and that it has a real impact on startup. | ||
var criteria = new Dictionary<string, Criterion>(StringComparer.OrdinalIgnoreCase); | ||
|
||
// Matches are items that have no remaining criteria - at this point in the tree | ||
// they are considered accepted. | ||
var matches = new List<TItem>(); | ||
|
||
// For each item in the working set, we want to map it to it's possible criteria-branch | ||
// pairings, then reduce that tree to the minimal set. | ||
foreach (var item in items) | ||
{ | ||
var unsatisfiedCriteria = 0; | ||
|
||
foreach (var kvp in item.Criteria) | ||
{ | ||
// context.CurrentCriteria is the logical 'stack' of criteria that we've already processed | ||
// on this branch of the tree. | ||
if (context.CurrentCriteria.Contains(kvp.Key)) | ||
{ | ||
continue; | ||
} | ||
|
||
unsatisfiedCriteria++; | ||
|
||
Criterion criterion; | ||
if (!criteria.TryGetValue(kvp.Key, out criterion)) | ||
{ | ||
criterion = new Criterion(comparer); | ||
criteria.Add(kvp.Key, criterion); | ||
} | ||
|
||
List<ItemDescriptor<TItem>> branch; | ||
if (!criterion.TryGetValue(kvp.Value, out branch)) | ||
{ | ||
branch = new List<ItemDescriptor<TItem>>(); | ||
criterion.Add(kvp.Value, branch); | ||
} | ||
|
||
branch.Add(item); | ||
} | ||
|
||
// If all of the criteria on item are satisfied by the 'stack' then this item is a match. | ||
if (unsatisfiedCriteria == 0) | ||
{ | ||
matches.Add(item.Item); | ||
} | ||
} | ||
|
||
// Iterate criteria in order of branchiness to determine which one to explore next. If a criterion | ||
// has no 'new' matches under it then we can just eliminate that part of the tree. | ||
var reducedCriteria = new List<DecisionCriterion<TItem>>(); | ||
foreach (var criterion in criteria.OrderByDescending(c => c.Value.Count)) | ||
{ | ||
var reducedBranches = new Dictionary<object, DecisionTreeNode<TItem>>(comparer.InnerComparer); | ||
DecisionTreeNode<TItem> fallback = null; | ||
|
||
foreach (var branch in criterion.Value) | ||
{ | ||
var reducedItems = new List<ItemDescriptor<TItem>>(); | ||
foreach (var item in branch.Value) | ||
{ | ||
if (context.MatchedItems.Add(item)) | ||
{ | ||
reducedItems.Add(item); | ||
} | ||
} | ||
|
||
if (reducedItems.Count > 0) | ||
{ | ||
var childContext = new TreeBuilderContext(context); | ||
childContext.CurrentCriteria.Add(criterion.Key); | ||
|
||
var newBranch = GenerateNode(childContext, comparer, branch.Value); | ||
if (branch.Key.IsCatchAll) | ||
{ | ||
fallback = newBranch; | ||
} | ||
else | ||
{ | ||
reducedBranches.Add(branch.Key.Value, newBranch); | ||
} | ||
} | ||
} | ||
|
||
if (reducedBranches.Count > 0 || fallback != null) | ||
{ | ||
var newCriterion = new DecisionCriterion<TItem>() | ||
{ | ||
Key = criterion.Key, | ||
Branches = reducedBranches, | ||
Fallback = fallback, | ||
}; | ||
|
||
reducedCriteria.Add(newCriterion); | ||
} | ||
} | ||
|
||
return new DecisionTreeNode<TItem>() | ||
{ | ||
Criteria = reducedCriteria.ToList(), | ||
Matches = matches, | ||
}; | ||
} | ||
|
||
private class TreeBuilderContext | ||
{ | ||
public TreeBuilderContext() | ||
{ | ||
CurrentCriteria = new HashSet<string>(StringComparer.OrdinalIgnoreCase); | ||
MatchedItems = new HashSet<ItemDescriptor<TItem>>(); | ||
} | ||
|
||
public TreeBuilderContext(TreeBuilderContext other) | ||
{ | ||
CurrentCriteria = new HashSet<string>(other.CurrentCriteria, StringComparer.OrdinalIgnoreCase); | ||
MatchedItems = new HashSet<ItemDescriptor<TItem>>(); | ||
} | ||
|
||
public HashSet<string> CurrentCriteria { get; private set; } | ||
|
||
public HashSet<ItemDescriptor<TItem>> MatchedItems { get; private set; } | ||
} | ||
|
||
// Subclass just to give a logical name to a mess of generics | ||
private class Criterion : Dictionary<DecisionCriterionValue, List<ItemDescriptor<TItem>>> | ||
{ | ||
public Criterion(DecisionCriterionValueEqualityComparer comparer) | ||
: base(comparer) | ||
{ | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.