Moving Attribute Routing to Routing
This commit is contained in:
parent
73f557002f
commit
01102bba3f
|
|
@ -15,6 +15,7 @@ using Microsoft.AspNet.Mvc.Core;
|
||||||
using Microsoft.AspNet.Mvc.Filters;
|
using Microsoft.AspNet.Mvc.Filters;
|
||||||
using Microsoft.AspNet.Mvc.Infrastructure;
|
using Microsoft.AspNet.Mvc.Infrastructure;
|
||||||
using Microsoft.AspNet.Mvc.Routing;
|
using Microsoft.AspNet.Mvc.Routing;
|
||||||
|
using Microsoft.AspNet.Routing.Tree;
|
||||||
|
|
||||||
namespace Microsoft.AspNet.Mvc.Controllers
|
namespace Microsoft.AspNet.Mvc.Controllers
|
||||||
{
|
{
|
||||||
|
|
@ -120,7 +121,7 @@ namespace Microsoft.AspNet.Mvc.Controllers
|
||||||
if (hasAttributeRoutes)
|
if (hasAttributeRoutes)
|
||||||
{
|
{
|
||||||
actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint(
|
actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint(
|
||||||
AttributeRouting.RouteGroupKey,
|
TreeRouter.RouteGroupKey,
|
||||||
string.Empty));
|
string.Empty));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -534,7 +535,7 @@ namespace Microsoft.AspNet.Mvc.Controllers
|
||||||
|
|
||||||
var routeConstraints = new List<RouteDataActionConstraint>();
|
var routeConstraints = new List<RouteDataActionConstraint>();
|
||||||
routeConstraints.Add(new RouteDataActionConstraint(
|
routeConstraints.Add(new RouteDataActionConstraint(
|
||||||
AttributeRouting.RouteGroupKey,
|
TreeRouter.RouteGroupKey,
|
||||||
routeGroupValue));
|
routeGroupValue));
|
||||||
|
|
||||||
actionDescriptor.RouteConstraints = routeConstraints;
|
actionDescriptor.RouteConstraints = routeConstraints;
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,8 @@ using Microsoft.AspNet.Mvc.Core;
|
||||||
using Microsoft.AspNet.Mvc.Diagnostics;
|
using Microsoft.AspNet.Mvc.Diagnostics;
|
||||||
using Microsoft.AspNet.Mvc.Internal;
|
using Microsoft.AspNet.Mvc.Internal;
|
||||||
using Microsoft.AspNet.Mvc.Logging;
|
using Microsoft.AspNet.Mvc.Logging;
|
||||||
using Microsoft.AspNet.Mvc.Routing;
|
|
||||||
using Microsoft.AspNet.Routing;
|
using Microsoft.AspNet.Routing;
|
||||||
|
using Microsoft.AspNet.Routing.Tree;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
|
@ -80,7 +80,7 @@ namespace Microsoft.AspNet.Mvc.Infrastructure
|
||||||
}
|
}
|
||||||
|
|
||||||
// Removing RouteGroup from RouteValues to simulate the result of conventional routing
|
// Removing RouteGroup from RouteValues to simulate the result of conventional routing
|
||||||
newRouteData.Values.Remove(AttributeRouting.RouteGroupKey);
|
newRouteData.Values.Remove(TreeRouter.RouteGroupKey);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
// 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.Mvc.Internal.DecisionTree
|
|
||||||
{
|
|
||||||
public class DecisionCriterion<TItem>
|
|
||||||
{
|
|
||||||
public string Key { get; set; }
|
|
||||||
|
|
||||||
public Dictionary<object, DecisionTreeNode<TItem>> Branches { get; set; }
|
|
||||||
|
|
||||||
public DecisionTreeNode<TItem> Fallback { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
// 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.Mvc.Internal.DecisionTree
|
|
||||||
{
|
|
||||||
public 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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
// 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.Mvc.Internal.DecisionTree
|
|
||||||
{
|
|
||||||
public 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,234 +0,0 @@
|
||||||
// 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.Mvc.Internal.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" }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
public 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)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
// 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.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 IList<TItem> Matches { get; set; }
|
|
||||||
|
|
||||||
// Additional criteria that further branch out from this node. Walk these to fine more items
|
|
||||||
// matching the input data.
|
|
||||||
public IList<DecisionCriterion<TItem>> Criteria { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
// 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.Mvc.Internal.DecisionTree
|
|
||||||
{
|
|
||||||
public interface IClassifier<TItem>
|
|
||||||
{
|
|
||||||
IDictionary<string, DecisionCriterionValue> GetCriteria(TItem item);
|
|
||||||
|
|
||||||
IEqualityComparer<object> ValueComparer { get; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
// 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.Mvc.Internal.DecisionTree
|
|
||||||
{
|
|
||||||
public class ItemDescriptor<TItem>
|
|
||||||
{
|
|
||||||
public IDictionary<string, DecisionCriterionValue> Criteria { get; set; }
|
|
||||||
|
|
||||||
public int Index { get; set; }
|
|
||||||
|
|
||||||
public TItem Item { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,156 +0,0 @@
|
||||||
// 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 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<TreeRouteLinkGenerationEntry> _root;
|
|
||||||
|
|
||||||
public LinkGenerationDecisionTree(IReadOnlyList<TreeRouteLinkGenerationEntry> entries)
|
|
||||||
{
|
|
||||||
_root = DecisionTreeBuilder<TreeRouteLinkGenerationEntry>.GenerateTree(
|
|
||||||
entries,
|
|
||||||
new AttributeRouteLinkGenerationEntryClassifier());
|
|
||||||
}
|
|
||||||
|
|
||||||
public IList<LinkGenerationMatch> GetMatches(VirtualPathContext context)
|
|
||||||
{
|
|
||||||
var results = new List<LinkGenerationMatch>();
|
|
||||||
Walk(results, context, _root, isFallbackPath: false);
|
|
||||||
results.Sort(LinkGenerationMatchComparer.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<LinkGenerationMatch> results,
|
|
||||||
VirtualPathContext context,
|
|
||||||
DecisionTreeNode<TreeRouteLinkGenerationEntry> node,
|
|
||||||
bool isFallbackPath)
|
|
||||||
{
|
|
||||||
// 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(new LinkGenerationMatch(node.Matches[i], isFallbackPath));
|
|
||||||
}
|
|
||||||
|
|
||||||
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<TreeRouteLinkGenerationEntry> branch;
|
|
||||||
if (criterion.Branches.TryGetValue(value ?? string.Empty, out branch))
|
|
||||||
{
|
|
||||||
Walk(results, context, branch, isFallbackPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// If a value wasn't explicitly supplied, match BOTH the ambient value and the empty value
|
|
||||||
// if an ambient value was supplied. The path explored with the empty value is considered
|
|
||||||
// the fallback path.
|
|
||||||
DecisionTreeNode<TreeRouteLinkGenerationEntry> 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, isFallbackPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (criterion.Branches.TryGetValue(string.Empty, out branch))
|
|
||||||
{
|
|
||||||
Walk(results, context, branch, isFallbackPath: true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class AttributeRouteLinkGenerationEntryClassifier : IClassifier<TreeRouteLinkGenerationEntry>
|
|
||||||
{
|
|
||||||
public AttributeRouteLinkGenerationEntryClassifier()
|
|
||||||
{
|
|
||||||
ValueComparer = new RouteValueEqualityComparer();
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEqualityComparer<object> ValueComparer { get; private set; }
|
|
||||||
|
|
||||||
public IDictionary<string, DecisionCriterionValue> GetCriteria(TreeRouteLinkGenerationEntry 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 LinkGenerationMatchComparer : IComparer<LinkGenerationMatch>
|
|
||||||
{
|
|
||||||
public static readonly LinkGenerationMatchComparer Instance = new LinkGenerationMatchComparer();
|
|
||||||
|
|
||||||
public int Compare(LinkGenerationMatch x, LinkGenerationMatch y)
|
|
||||||
{
|
|
||||||
// For this comparison lower is better.
|
|
||||||
if (x.Entry.Order != y.Entry.Order)
|
|
||||||
{
|
|
||||||
return x.Entry.Order.CompareTo(y.Entry.Order);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (x.Entry.GenerationPrecedence != y.Entry.GenerationPrecedence)
|
|
||||||
{
|
|
||||||
// Reversed because higher is better
|
|
||||||
return y.Entry.GenerationPrecedence.CompareTo(x.Entry.GenerationPrecedence);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (x.IsFallbackMatch != y.IsFallbackMatch)
|
|
||||||
{
|
|
||||||
// A fallback match is worse than a non-fallback
|
|
||||||
return x.IsFallbackMatch.CompareTo(y.IsFallbackMatch);
|
|
||||||
}
|
|
||||||
|
|
||||||
return StringComparer.Ordinal.Compare(x.Entry.TemplateText, y.Entry.TemplateText);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
// 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 Microsoft.AspNet.Mvc.Routing;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNet.Mvc.Internal.Routing
|
|
||||||
{
|
|
||||||
public struct LinkGenerationMatch
|
|
||||||
{
|
|
||||||
private readonly bool _isFallbackMatch;
|
|
||||||
private readonly TreeRouteLinkGenerationEntry _entry;
|
|
||||||
|
|
||||||
public LinkGenerationMatch(TreeRouteLinkGenerationEntry entry, bool isFallbackMatch)
|
|
||||||
{
|
|
||||||
_entry = entry;
|
|
||||||
_isFallbackMatch = isFallbackMatch;
|
|
||||||
}
|
|
||||||
|
|
||||||
public TreeRouteLinkGenerationEntry Entry { get { return _entry; } }
|
|
||||||
|
|
||||||
public bool IsFallbackMatch { get { return _isFallbackMatch; } }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
// 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 Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNet.Mvc.Logging
|
|
||||||
{
|
|
||||||
internal static class TreeRouterLoggerExtensions
|
|
||||||
{
|
|
||||||
private static readonly Action<ILogger, string, string, Exception> _matchedRouteName;
|
|
||||||
|
|
||||||
static TreeRouterLoggerExtensions()
|
|
||||||
{
|
|
||||||
_matchedRouteName = LoggerMessage.Define<string, string>(
|
|
||||||
LogLevel.Verbose,
|
|
||||||
1,
|
|
||||||
"Request successfully matched the route with name '{RouteName}' and template '{RouteTemplate}'.");
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void MatchedRouteName(
|
|
||||||
this ILogger logger,
|
|
||||||
string routeName,
|
|
||||||
string routeTemplate)
|
|
||||||
{
|
|
||||||
_matchedRouteName(logger, routeName, routeTemplate, null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -506,22 +506,6 @@ namespace Microsoft.AspNet.Mvc.Core
|
||||||
return string.Format(CultureInfo.CurrentCulture, GetString("UnableToFindServices"), p0, p1, p2);
|
return string.Format(CultureInfo.CurrentCulture, GetString("UnableToFindServices"), p0, p1, p2);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Two or more routes named '{0}' have different templates.
|
|
||||||
/// </summary>
|
|
||||||
internal static string AttributeRoute_DifferentLinkGenerationEntries_SameName
|
|
||||||
{
|
|
||||||
get { return GetString("AttributeRoute_DifferentLinkGenerationEntries_SameName"); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Two or more routes named '{0}' have different templates.
|
|
||||||
/// </summary>
|
|
||||||
internal static string FormatAttributeRoute_DifferentLinkGenerationEntries_SameName(object p0)
|
|
||||||
{
|
|
||||||
return string.Format(CultureInfo.CurrentCulture, GetString("AttributeRoute_DifferentLinkGenerationEntries_SameName"), p0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Action: '{0}' - Template: '{1}'
|
/// Action: '{0}' - Template: '{1}'
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -213,9 +213,6 @@
|
||||||
<data name="UnableToFindServices" xml:space="preserve">
|
<data name="UnableToFindServices" xml:space="preserve">
|
||||||
<value>Unable to find the required services. Please add all the required services by calling '{0}' inside the call to '{1}' or '{2}' in the application startup code.</value>
|
<value>Unable to find the required services. Please add all the required services by calling '{0}' inside the call to '{1}' or '{2}' in the application startup code.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="AttributeRoute_DifferentLinkGenerationEntries_SameName" xml:space="preserve">
|
|
||||||
<value>Two or more routes named '{0}' have different templates.</value>
|
|
||||||
</data>
|
|
||||||
<data name="AttributeRoute_DuplicateNames_Item" xml:space="preserve">
|
<data name="AttributeRoute_DuplicateNames_Item" xml:space="preserve">
|
||||||
<value>Action: '{0}' - Template: '{1}'</value>
|
<value>Action: '{0}' - Template: '{1}'</value>
|
||||||
<comment>Formats an action descriptor display name and it's associated template.</comment>
|
<comment>Formats an action descriptor display name and it's associated template.</comment>
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,8 @@ using System.ComponentModel;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using Microsoft.AspNet.Mvc.Abstractions;
|
using Microsoft.AspNet.Mvc.Abstractions;
|
||||||
using Microsoft.AspNet.Mvc.Infrastructure;
|
using Microsoft.AspNet.Mvc.Infrastructure;
|
||||||
using Microsoft.AspNet.Mvc.Internal.DecisionTree;
|
using Microsoft.AspNet.Routing;
|
||||||
|
using Microsoft.AspNet.Routing.DecisionTree;
|
||||||
|
|
||||||
namespace Microsoft.AspNet.Mvc.Routing
|
namespace Microsoft.AspNet.Mvc.Routing
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ using Microsoft.AspNet.Mvc.Core;
|
||||||
using Microsoft.AspNet.Mvc.Infrastructure;
|
using Microsoft.AspNet.Mvc.Infrastructure;
|
||||||
using Microsoft.AspNet.Routing;
|
using Microsoft.AspNet.Routing;
|
||||||
using Microsoft.AspNet.Routing.Template;
|
using Microsoft.AspNet.Routing.Template;
|
||||||
|
using Microsoft.AspNet.Routing.Tree;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Microsoft.AspNet.Mvc.Routing
|
namespace Microsoft.AspNet.Mvc.Routing
|
||||||
|
|
@ -19,8 +20,7 @@ namespace Microsoft.AspNet.Mvc.Routing
|
||||||
private readonly IRouter _target;
|
private readonly IRouter _target;
|
||||||
private readonly IActionDescriptorsCollectionProvider _actionDescriptorsCollectionProvider;
|
private readonly IActionDescriptorsCollectionProvider _actionDescriptorsCollectionProvider;
|
||||||
private readonly IInlineConstraintResolver _constraintResolver;
|
private readonly IInlineConstraintResolver _constraintResolver;
|
||||||
private readonly ILogger _routeLogger;
|
private readonly ILoggerFactory _loggerFactory;
|
||||||
private readonly ILogger _constraintLogger;
|
|
||||||
|
|
||||||
private TreeRouter _router;
|
private TreeRouter _router;
|
||||||
|
|
||||||
|
|
@ -53,9 +53,7 @@ namespace Microsoft.AspNet.Mvc.Routing
|
||||||
_target = target;
|
_target = target;
|
||||||
_actionDescriptorsCollectionProvider = actionDescriptorsCollectionProvider;
|
_actionDescriptorsCollectionProvider = actionDescriptorsCollectionProvider;
|
||||||
_constraintResolver = constraintResolver;
|
_constraintResolver = constraintResolver;
|
||||||
|
_loggerFactory = loggerFactory;
|
||||||
_routeLogger = loggerFactory.CreateLogger<TreeRouter>();
|
|
||||||
_constraintLogger = loggerFactory.CreateLogger(typeof(RouteConstraintMatcher).FullName);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|
@ -88,7 +86,7 @@ namespace Microsoft.AspNet.Mvc.Routing
|
||||||
|
|
||||||
private TreeRouter BuildRoute(ActionDescriptorsCollection actions)
|
private TreeRouter BuildRoute(ActionDescriptorsCollection actions)
|
||||||
{
|
{
|
||||||
var routeBuilder = new TreeRouteBuilder(_target, _routeLogger, _constraintLogger);
|
var routeBuilder = new TreeRouteBuilder(_target, _loggerFactory);
|
||||||
var routeInfos = GetRouteInfos(_constraintResolver, actions.Items);
|
var routeInfos = GetRouteInfos(_constraintResolver, actions.Items);
|
||||||
|
|
||||||
// We're creating one AttributeRouteGenerationEntry per action. This allows us to match the intended
|
// We're creating one AttributeRouteGenerationEntry per action. This allows us to match the intended
|
||||||
|
|
@ -105,7 +103,6 @@ namespace Microsoft.AspNet.Mvc.Routing
|
||||||
RequiredLinkValues = routeInfo.ActionDescriptor.RouteValueDefaults,
|
RequiredLinkValues = routeInfo.ActionDescriptor.RouteValueDefaults,
|
||||||
RouteGroup = routeInfo.RouteGroup,
|
RouteGroup = routeInfo.RouteGroup,
|
||||||
Template = routeInfo.ParsedTemplate,
|
Template = routeInfo.ParsedTemplate,
|
||||||
TemplateText = routeInfo.RouteTemplate,
|
|
||||||
Name = routeInfo.Name,
|
Name = routeInfo.Name,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -127,7 +124,7 @@ namespace Microsoft.AspNet.Mvc.Routing
|
||||||
routeInfo.ParsedTemplate,
|
routeInfo.ParsedTemplate,
|
||||||
new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase)
|
new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase)
|
||||||
{
|
{
|
||||||
{ AttributeRouting.RouteGroupKey, routeInfo.RouteGroup }
|
{ TreeRouter.RouteGroupKey, routeInfo.RouteGroup }
|
||||||
}),
|
}),
|
||||||
Constraints = routeInfo.Constraints
|
Constraints = routeInfo.Constraints
|
||||||
});
|
});
|
||||||
|
|
@ -203,7 +200,7 @@ namespace Microsoft.AspNet.Mvc.Routing
|
||||||
ActionDescriptor action)
|
ActionDescriptor action)
|
||||||
{
|
{
|
||||||
var constraint = action.RouteConstraints
|
var constraint = action.RouteConstraints
|
||||||
.Where(c => c.RouteKey == AttributeRouting.RouteGroupKey)
|
.Where(c => c.RouteKey == TreeRouter.RouteGroupKey)
|
||||||
.FirstOrDefault();
|
.FirstOrDefault();
|
||||||
if (constraint == null ||
|
if (constraint == null ||
|
||||||
constraint.KeyHandling != RouteKeyHandling.RequireKey ||
|
constraint.KeyHandling != RouteKeyHandling.RequireKey ||
|
||||||
|
|
@ -260,8 +257,8 @@ namespace Microsoft.AspNet.Mvc.Routing
|
||||||
|
|
||||||
routeInfo.Order = action.AttributeRouteInfo.Order;
|
routeInfo.Order = action.AttributeRouteInfo.Order;
|
||||||
|
|
||||||
routeInfo.MatchPrecedence = AttributeRoutePrecedence.ComputeMatched(routeInfo.ParsedTemplate);
|
routeInfo.MatchPrecedence = RoutePrecedence.ComputeMatched(routeInfo.ParsedTemplate);
|
||||||
routeInfo.GenerationPrecedence = AttributeRoutePrecedence.ComputeGenerated(routeInfo.ParsedTemplate);
|
routeInfo.GenerationPrecedence = RoutePrecedence.ComputeGenerated(routeInfo.ParsedTemplate);
|
||||||
|
|
||||||
routeInfo.Name = action.AttributeRouteInfo.Name;
|
routeInfo.Name = action.AttributeRouteInfo.Name;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,132 +0,0 @@
|
||||||
// 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.Diagnostics;
|
|
||||||
using System.Linq;
|
|
||||||
using Microsoft.AspNet.Routing.Template;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNet.Mvc.Routing
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Computes precedence for an attribute route template.
|
|
||||||
/// </summary>
|
|
||||||
public static class AttributeRoutePrecedence
|
|
||||||
{
|
|
||||||
// Compute the precedence for matching a provided url
|
|
||||||
// e.g.: /api/template == 1.1
|
|
||||||
// /api/template/{id} == 1.13
|
|
||||||
// /api/{id:int} == 1.2
|
|
||||||
// /api/template/{id:int} == 1.12
|
|
||||||
public static decimal ComputeMatched(RouteTemplate template)
|
|
||||||
{
|
|
||||||
// Each precedence digit corresponds to one decimal place. For example, 3 segments with precedences 2, 1,
|
|
||||||
// and 4 results in a combined precedence of 2.14 (decimal).
|
|
||||||
var precedence = 0m;
|
|
||||||
|
|
||||||
for (var i = 0; i < template.Segments.Count; i++)
|
|
||||||
{
|
|
||||||
var segment = template.Segments[i];
|
|
||||||
|
|
||||||
var digit = ComputeMatchDigit(segment);
|
|
||||||
Debug.Assert(digit >= 0 && digit < 10);
|
|
||||||
|
|
||||||
precedence += decimal.Divide(digit, (decimal)Math.Pow(10, i));
|
|
||||||
}
|
|
||||||
|
|
||||||
return precedence;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compute the precedence for generating a url
|
|
||||||
// e.g.: /api/template == 5.5
|
|
||||||
// /api/template/{id} == 5.53
|
|
||||||
// /api/{id:int} == 5.4
|
|
||||||
// /api/template/{id:int} == 5.54
|
|
||||||
public static decimal ComputeGenerated(RouteTemplate template)
|
|
||||||
{
|
|
||||||
// Each precedence digit corresponds to one decimal place. For example, 3 segments with precedences 2, 1,
|
|
||||||
// and 4 results in a combined precedence of 2.14 (decimal).
|
|
||||||
var precedence = 0m;
|
|
||||||
|
|
||||||
for (var i = 0; i < template.Segments.Count; i++)
|
|
||||||
{
|
|
||||||
var segment = template.Segments[i];
|
|
||||||
|
|
||||||
var digit = ComputeGenerationDigit(segment);
|
|
||||||
Debug.Assert(digit >= 0 && digit < 10);
|
|
||||||
|
|
||||||
precedence += decimal.Divide(digit, (decimal)Math.Pow(10, i));
|
|
||||||
}
|
|
||||||
|
|
||||||
return precedence;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Segments have the following order:
|
|
||||||
// 5 - Literal segments
|
|
||||||
// 4 - Multi-part segments && Constrained parameter segments
|
|
||||||
// 3 - Unconstrained parameter segements
|
|
||||||
// 2 - Constrained wildcard parameter segments
|
|
||||||
// 1 - Unconstrained wildcard parameter segments
|
|
||||||
private static int ComputeGenerationDigit(TemplateSegment segment)
|
|
||||||
{
|
|
||||||
if(segment.Parts.Count > 1)
|
|
||||||
{
|
|
||||||
return 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
var part = segment.Parts[0];
|
|
||||||
if(part.IsLiteral)
|
|
||||||
{
|
|
||||||
return 5;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Debug.Assert(part.IsParameter);
|
|
||||||
var digit = part.IsCatchAll ? 1 : 3;
|
|
||||||
|
|
||||||
if (part.InlineConstraints != null && part.InlineConstraints.Any())
|
|
||||||
{
|
|
||||||
digit++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return digit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Segments have the following order:
|
|
||||||
// 1 - Literal segments
|
|
||||||
// 2 - Constrained parameter segments / Multi-part segments
|
|
||||||
// 3 - Unconstrained parameter segments
|
|
||||||
// 4 - Constrained wildcard parameter segments
|
|
||||||
// 5 - Unconstrained wildcard parameter segments
|
|
||||||
private static int ComputeMatchDigit(TemplateSegment segment)
|
|
||||||
{
|
|
||||||
if (segment.Parts.Count > 1)
|
|
||||||
{
|
|
||||||
// Multi-part segments should appear after literal segments and along with parameter segments
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
var part = segment.Parts[0];
|
|
||||||
// Literal segments always go first
|
|
||||||
if (part.IsLiteral)
|
|
||||||
{
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Debug.Assert(part.IsParameter);
|
|
||||||
var digit = part.IsCatchAll ? 5 : 3;
|
|
||||||
|
|
||||||
// If there is a route constraint for the parameter, reduce order by 1
|
|
||||||
// Constrained parameters end up with order 2, Constrained catch alls end up with order 4
|
|
||||||
if (part.InlineConstraints != null && part.InlineConstraints.Any())
|
|
||||||
{
|
|
||||||
digit--;
|
|
||||||
}
|
|
||||||
|
|
||||||
return digit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -11,10 +11,6 @@ namespace Microsoft.AspNet.Mvc.Routing
|
||||||
{
|
{
|
||||||
public static class AttributeRouting
|
public static class AttributeRouting
|
||||||
{
|
{
|
||||||
// Key used by routing and action selection to match an attribute route entry to a
|
|
||||||
// group of action descriptors.
|
|
||||||
public static readonly string RouteGroupKey = "!__route_group";
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates an attribute route using the provided services and provided target router.
|
/// Creates an attribute route using the provided services and provided target router.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
// 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.Globalization;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNet.Mvc.Routing
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// An <see cref="IEqualityComparer{object}"/> implementation that compares objects as-if
|
|
||||||
/// they were route value strings.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Values that are are not strings are converted to strings using
|
|
||||||
/// <c>Convert.ToString(x, CultureInfo.InvariantCulture)</c>. <c>null</c> values are converted
|
|
||||||
/// to the empty string.
|
|
||||||
///
|
|
||||||
/// strings are compared using <see cref="StringComparison.OrdinalIgnoreCase"/>.
|
|
||||||
/// </remarks>
|
|
||||||
public class RouteValueEqualityComparer : IEqualityComparer<object>
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public new bool Equals(object x, object y)
|
|
||||||
{
|
|
||||||
var stringX = x as string ?? Convert.ToString(x, CultureInfo.InvariantCulture);
|
|
||||||
var stringY = y as string ?? Convert.ToString(y, CultureInfo.InvariantCulture);
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(stringX) && string.IsNullOrEmpty(stringY))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return string.Equals(stringX, stringY, StringComparison.OrdinalIgnoreCase);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public int GetHashCode(object obj)
|
|
||||||
{
|
|
||||||
var stringObj = obj as string ?? Convert.ToString(obj, CultureInfo.InvariantCulture);
|
|
||||||
if (string.IsNullOrEmpty(stringObj))
|
|
||||||
{
|
|
||||||
return StringComparer.OrdinalIgnoreCase.GetHashCode(string.Empty);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return StringComparer.OrdinalIgnoreCase.GetHashCode(stringObj);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,167 +0,0 @@
|
||||||
// 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.Diagnostics;
|
|
||||||
using System.Linq;
|
|
||||||
using Microsoft.AspNet.Routing;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNet.Mvc.Routing
|
|
||||||
{
|
|
||||||
public class TreeRouteBuilder
|
|
||||||
{
|
|
||||||
private readonly IRouter _target;
|
|
||||||
private readonly List<TreeRouteLinkGenerationEntry> _generatingEntries;
|
|
||||||
private readonly List<TreeRouteMatchingEntry> _matchingEntries;
|
|
||||||
|
|
||||||
private readonly ILogger _logger;
|
|
||||||
private readonly ILogger _constraintLogger;
|
|
||||||
|
|
||||||
public TreeRouteBuilder(IRouter target, ILogger routeLogger, ILogger constraintLogger)
|
|
||||||
{
|
|
||||||
_target = target;
|
|
||||||
_generatingEntries = new List<TreeRouteLinkGenerationEntry>();
|
|
||||||
_matchingEntries = new List<TreeRouteMatchingEntry>();
|
|
||||||
|
|
||||||
_logger = routeLogger;
|
|
||||||
_constraintLogger = constraintLogger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Add(TreeRouteLinkGenerationEntry entry)
|
|
||||||
{
|
|
||||||
_generatingEntries.Add(entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Add(TreeRouteMatchingEntry entry)
|
|
||||||
{
|
|
||||||
_matchingEntries.Add(entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
public TreeRouter Build(int version)
|
|
||||||
{
|
|
||||||
var trees = new Dictionary<int, UrlMatchingTree>();
|
|
||||||
|
|
||||||
foreach (var entry in _matchingEntries)
|
|
||||||
{
|
|
||||||
UrlMatchingTree tree;
|
|
||||||
if (!trees.TryGetValue(entry.Order, out tree))
|
|
||||||
{
|
|
||||||
tree = new UrlMatchingTree(entry.Order);
|
|
||||||
trees.Add(entry.Order, tree);
|
|
||||||
}
|
|
||||||
|
|
||||||
AddEntryToTree(tree, entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new TreeRouter(
|
|
||||||
_target,
|
|
||||||
trees.Values.OrderBy(tree => tree.Order).ToArray(),
|
|
||||||
_generatingEntries,
|
|
||||||
_logger,
|
|
||||||
_constraintLogger,
|
|
||||||
version);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Clear()
|
|
||||||
{
|
|
||||||
_generatingEntries.Clear();
|
|
||||||
_matchingEntries.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AddEntryToTree(UrlMatchingTree tree, TreeRouteMatchingEntry entry)
|
|
||||||
{
|
|
||||||
var current = tree.Root;
|
|
||||||
|
|
||||||
for (var i = 0; i < entry.RouteTemplate.Segments.Count; i++)
|
|
||||||
{
|
|
||||||
var segment = entry.RouteTemplate.Segments[i];
|
|
||||||
if (!segment.IsSimple)
|
|
||||||
{
|
|
||||||
// Treat complex segments as a constrained parameter
|
|
||||||
if (current.ConstrainedParameters == null)
|
|
||||||
{
|
|
||||||
current.ConstrainedParameters = new UrlMatchingNode(length: i + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
current = current.ConstrainedParameters;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
Debug.Assert(segment.Parts.Count == 1);
|
|
||||||
var part = segment.Parts[0];
|
|
||||||
if (part.IsLiteral)
|
|
||||||
{
|
|
||||||
UrlMatchingNode next;
|
|
||||||
if (!current.Literals.TryGetValue(part.Text, out next))
|
|
||||||
{
|
|
||||||
next = new UrlMatchingNode(length: i + 1);
|
|
||||||
current.Literals.Add(part.Text, next);
|
|
||||||
}
|
|
||||||
|
|
||||||
current = next;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (part.IsParameter && (part.IsOptional || part.IsCatchAll))
|
|
||||||
{
|
|
||||||
current.Matches.Add(entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (part.IsParameter && part.InlineConstraints.Any() && !part.IsCatchAll)
|
|
||||||
{
|
|
||||||
if (current.ConstrainedParameters == null)
|
|
||||||
{
|
|
||||||
current.ConstrainedParameters = new UrlMatchingNode(length: i + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
current = current.ConstrainedParameters;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (part.IsParameter && !part.IsCatchAll)
|
|
||||||
{
|
|
||||||
if (current.Parameters == null)
|
|
||||||
{
|
|
||||||
current.Parameters = new UrlMatchingNode(length: i + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
current = current.Parameters;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (part.IsParameter && part.InlineConstraints.Any() && part.IsCatchAll)
|
|
||||||
{
|
|
||||||
if (current.ConstrainedCatchAlls == null)
|
|
||||||
{
|
|
||||||
current.ConstrainedCatchAlls = new UrlMatchingNode(length: i + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
current = current.ConstrainedCatchAlls;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (part.IsParameter && part.IsCatchAll)
|
|
||||||
{
|
|
||||||
if (current.CatchAlls == null)
|
|
||||||
{
|
|
||||||
current.CatchAlls = new UrlMatchingNode(length: i + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
current = current.CatchAlls;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
Debug.Fail("We shouldn't get here.");
|
|
||||||
}
|
|
||||||
|
|
||||||
current.Matches.Add(entry);
|
|
||||||
current.Matches.Sort((x, y) =>
|
|
||||||
{
|
|
||||||
var result = x.Precedence.CompareTo(y.Precedence);
|
|
||||||
return result == 0 ? x.RouteTemplate.Template.CompareTo(y.RouteTemplate.Template) : result;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,66 +0,0 @@
|
||||||
// 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;
|
|
||||||
using Microsoft.AspNet.Routing;
|
|
||||||
using Microsoft.AspNet.Routing.Template;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNet.Mvc.Routing
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Used to build a <see cref="TreeRouter"/>. Represents an individual URL-generating route that will be
|
|
||||||
/// aggregated into the <see cref="TreeRouter"/>.
|
|
||||||
/// </summary>
|
|
||||||
public class TreeRouteLinkGenerationEntry
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The <see cref="TemplateBinder"/>.
|
|
||||||
/// </summary>
|
|
||||||
public TemplateBinder Binder { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The route constraints.
|
|
||||||
/// </summary>
|
|
||||||
public IReadOnlyDictionary<string, IRouteConstraint> Constraints { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The route defaults.
|
|
||||||
/// </summary>
|
|
||||||
public IReadOnlyDictionary<string, object> Defaults { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The order of the template.
|
|
||||||
/// </summary>
|
|
||||||
public int Order { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The precedence of the template for link generation. Greater number means higher precedence.
|
|
||||||
/// </summary>
|
|
||||||
public decimal GenerationPrecedence { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The name of the route.
|
|
||||||
/// </summary>
|
|
||||||
public string Name { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The route group.
|
|
||||||
/// </summary>
|
|
||||||
public string RouteGroup { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The set of values that must be present for link genration.
|
|
||||||
/// </summary>
|
|
||||||
public IDictionary<string, object> RequiredLinkValues { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The <see cref="Template"/>.
|
|
||||||
/// </summary>
|
|
||||||
public RouteTemplate Template { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The original <see cref="string"/> representing the route template.
|
|
||||||
/// </summary>
|
|
||||||
public string TemplateText { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
// 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;
|
|
||||||
using Microsoft.AspNet.Routing;
|
|
||||||
using Microsoft.AspNet.Routing.Template;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNet.Mvc.Routing
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Used to build an <see cref="TreeRouter"/>. Represents an individual URL-matching route that will be
|
|
||||||
/// aggregated into the <see cref="TreeRouter"/>.
|
|
||||||
/// </summary>
|
|
||||||
public class TreeRouteMatchingEntry
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The order of the template.
|
|
||||||
/// </summary>
|
|
||||||
public int Order { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The precedence of the template.
|
|
||||||
/// </summary>
|
|
||||||
public decimal Precedence { get; set; }
|
|
||||||
|
|
||||||
public IRouter Target { get; set; }
|
|
||||||
|
|
||||||
public string RouteName { get; set; }
|
|
||||||
|
|
||||||
public RouteTemplate RouteTemplate { get; set; }
|
|
||||||
|
|
||||||
public TemplateMatcher TemplateMatcher { get; set; }
|
|
||||||
|
|
||||||
public IReadOnlyDictionary<string, IRouteConstraint> Constraints { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,478 +0,0 @@
|
||||||
// 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;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.AspNet.Mvc.Core;
|
|
||||||
using Microsoft.AspNet.Mvc.Internal.Routing;
|
|
||||||
using Microsoft.AspNet.Mvc.Logging;
|
|
||||||
using Microsoft.AspNet.Routing;
|
|
||||||
using Microsoft.AspNet.Routing.Internal;
|
|
||||||
using Microsoft.Extensions.Internal;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNet.Mvc.Routing
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// An <see cref="IRouter"/> implementation for attribute routing.
|
|
||||||
/// </summary>
|
|
||||||
public class TreeRouter : IRouter
|
|
||||||
{
|
|
||||||
private readonly IRouter _next;
|
|
||||||
private readonly LinkGenerationDecisionTree _linkGenerationTree;
|
|
||||||
private readonly UrlMatchingTree[] _trees;
|
|
||||||
private readonly IDictionary<string, TreeRouteLinkGenerationEntry> _namedEntries;
|
|
||||||
|
|
||||||
private readonly ILogger _logger;
|
|
||||||
private readonly ILogger _constraintLogger;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a new <see cref="TreeRouter"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="next">The next router. Invoked when a route entry matches.</param>
|
|
||||||
/// <param name="trees">The list of <see cref="UrlMatchingTree"/> that contains the route entries.</param>
|
|
||||||
/// <param name="linkGenerationEntries">The set of <see cref="TreeRouteLinkGenerationEntry"/>.</param>
|
|
||||||
/// <param name="routeLogger">The <see cref="ILogger"/> instance.</param>
|
|
||||||
/// <param name="constraintLogger">The <see cref="ILogger"/> instance used
|
|
||||||
/// in <see cref="RouteConstraintMatcher"/>.</param>
|
|
||||||
/// <param name="version">The version of this route.</param>
|
|
||||||
public TreeRouter(
|
|
||||||
IRouter next,
|
|
||||||
UrlMatchingTree[] trees,
|
|
||||||
IEnumerable<TreeRouteLinkGenerationEntry> linkGenerationEntries,
|
|
||||||
ILogger routeLogger,
|
|
||||||
ILogger constraintLogger,
|
|
||||||
int version)
|
|
||||||
{
|
|
||||||
if (next == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(next));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (trees == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(trees));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (linkGenerationEntries == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(linkGenerationEntries));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (routeLogger == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(routeLogger));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (constraintLogger == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(constraintLogger));
|
|
||||||
}
|
|
||||||
|
|
||||||
_next = next;
|
|
||||||
_trees = trees;
|
|
||||||
_logger = routeLogger;
|
|
||||||
_constraintLogger = constraintLogger;
|
|
||||||
|
|
||||||
var namedEntries = new Dictionary<string, TreeRouteLinkGenerationEntry>(
|
|
||||||
StringComparer.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
foreach (var entry in linkGenerationEntries)
|
|
||||||
{
|
|
||||||
// Skip unnamed entries
|
|
||||||
if (entry.Name == null)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We only need to keep one AttributeRouteLinkGenerationEntry per route template
|
|
||||||
// so in case two entries have the same name and the same template we only keep
|
|
||||||
// the first entry.
|
|
||||||
TreeRouteLinkGenerationEntry namedEntry = null;
|
|
||||||
if (namedEntries.TryGetValue(entry.Name, out namedEntry) &&
|
|
||||||
!namedEntry.TemplateText.Equals(entry.TemplateText, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
throw new ArgumentException(
|
|
||||||
Resources.FormatAttributeRoute_DifferentLinkGenerationEntries_SameName(entry.Name),
|
|
||||||
nameof(linkGenerationEntries));
|
|
||||||
}
|
|
||||||
else if (namedEntry == null)
|
|
||||||
{
|
|
||||||
namedEntries.Add(entry.Name, entry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_namedEntries = namedEntries;
|
|
||||||
|
|
||||||
// The decision tree will take care of ordering for these entries.
|
|
||||||
_linkGenerationTree = new LinkGenerationDecisionTree(linkGenerationEntries.ToArray());
|
|
||||||
|
|
||||||
Version = version;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the version of this route. This corresponds to the value of
|
|
||||||
/// <see cref="Infrastructure.ActionDescriptorsCollection.Version"/> when this route was created.
|
|
||||||
/// </summary>
|
|
||||||
public int Version { get; }
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public VirtualPathData GetVirtualPath(VirtualPathContext context)
|
|
||||||
{
|
|
||||||
if (context == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(context));
|
|
||||||
}
|
|
||||||
|
|
||||||
// If it's a named route we will try to generate a link directly and
|
|
||||||
// if we can't, we will not try to generate it using an unnamed route.
|
|
||||||
if (context.RouteName != null)
|
|
||||||
{
|
|
||||||
return GetVirtualPathForNamedRoute(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 match in matches)
|
|
||||||
{
|
|
||||||
var path = GenerateVirtualPath(context, match.Entry);
|
|
||||||
if (path != null)
|
|
||||||
{
|
|
||||||
context.IsBound = true;
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public async Task RouteAsync(RouteContext context)
|
|
||||||
{
|
|
||||||
foreach (var tree in _trees)
|
|
||||||
{
|
|
||||||
var tokenizer = new PathTokenizer(context.HttpContext.Request.Path);
|
|
||||||
var enumerator = tokenizer.GetEnumerator();
|
|
||||||
var root = tree.Root;
|
|
||||||
|
|
||||||
var treeEnumerator = new TreeEnumerator(root, tokenizer);
|
|
||||||
|
|
||||||
while (treeEnumerator.MoveNext())
|
|
||||||
{
|
|
||||||
var node = treeEnumerator.Current;
|
|
||||||
foreach (var item in node.Matches)
|
|
||||||
{
|
|
||||||
var values = item.TemplateMatcher.Match(context.HttpContext.Request.Path);
|
|
||||||
if (values == null)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var match = new TemplateMatch(item, values);
|
|
||||||
|
|
||||||
var oldRouteData = context.RouteData;
|
|
||||||
|
|
||||||
var newRouteData = new RouteData(oldRouteData);
|
|
||||||
|
|
||||||
newRouteData.Routers.Add(match.Entry.Target);
|
|
||||||
MergeValues(newRouteData.Values, match.Values);
|
|
||||||
|
|
||||||
if (!RouteConstraintMatcher.Match(
|
|
||||||
match.Entry.Constraints,
|
|
||||||
newRouteData.Values,
|
|
||||||
context.HttpContext,
|
|
||||||
this,
|
|
||||||
RouteDirection.IncomingRequest,
|
|
||||||
_constraintLogger))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.MatchedRouteName(match.Entry.RouteName, match.Entry.RouteTemplate.Template);
|
|
||||||
|
|
||||||
context.RouteData = newRouteData;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await match.Entry.Target.RouteAsync(context);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
if (!context.IsHandled)
|
|
||||||
{
|
|
||||||
// Restore the original values to prevent polluting the route data.
|
|
||||||
context.RouteData = oldRouteData;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (context.IsHandled)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct TreeEnumerator : IEnumerator<UrlMatchingNode>
|
|
||||||
{
|
|
||||||
private readonly Stack<UrlMatchingNode> _stack;
|
|
||||||
private readonly PathTokenizer _tokenizer;
|
|
||||||
|
|
||||||
private int _segmentIndex;
|
|
||||||
|
|
||||||
public TreeEnumerator(UrlMatchingNode root, PathTokenizer tokenizer)
|
|
||||||
{
|
|
||||||
_stack = new Stack<UrlMatchingNode>();
|
|
||||||
_tokenizer = tokenizer;
|
|
||||||
Current = null;
|
|
||||||
_segmentIndex = -1;
|
|
||||||
|
|
||||||
_stack.Push(root);
|
|
||||||
}
|
|
||||||
|
|
||||||
public UrlMatchingNode Current { get; private set; }
|
|
||||||
|
|
||||||
object IEnumerator.Current => Current;
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool MoveNext()
|
|
||||||
{
|
|
||||||
if (_stack == null)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
while (_stack.Count > 0)
|
|
||||||
{
|
|
||||||
var next = _stack.Pop();
|
|
||||||
if (++_segmentIndex >= _tokenizer.Count)
|
|
||||||
{
|
|
||||||
_segmentIndex--;
|
|
||||||
if (next.Matches.Count > 0)
|
|
||||||
{
|
|
||||||
Current = next;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_tokenizer.Count == 0)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (next.CatchAlls != null)
|
|
||||||
{
|
|
||||||
_stack.Push(next.CatchAlls);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (next.ConstrainedCatchAlls != null)
|
|
||||||
{
|
|
||||||
_stack.Push(next.ConstrainedCatchAlls);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (next.Parameters != null)
|
|
||||||
{
|
|
||||||
_stack.Push(next.Parameters);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (next.ConstrainedParameters != null)
|
|
||||||
{
|
|
||||||
_stack.Push(next.ConstrainedParameters);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (next.Literals.Count > 0)
|
|
||||||
{
|
|
||||||
UrlMatchingNode node;
|
|
||||||
if (next.Literals.TryGetValue(_tokenizer[_segmentIndex].Value, out node))
|
|
||||||
{
|
|
||||||
_stack.Push(node);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Reset()
|
|
||||||
{
|
|
||||||
_stack.Clear();
|
|
||||||
Current = null;
|
|
||||||
_segmentIndex = -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void MergeValues(
|
|
||||||
IDictionary<string, object> destination,
|
|
||||||
IDictionary<string, object> values)
|
|
||||||
{
|
|
||||||
foreach (var kvp in values)
|
|
||||||
{
|
|
||||||
if (kvp.Value != null)
|
|
||||||
{
|
|
||||||
// This will replace the original value for the specified key.
|
|
||||||
// Values from the matched route will take preference over previous
|
|
||||||
// data in the route context.
|
|
||||||
destination[kvp.Key] = kvp.Value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct TemplateMatch : IEquatable<TemplateMatch>
|
|
||||||
{
|
|
||||||
public TemplateMatch(TreeRouteMatchingEntry entry, IDictionary<string, object> values)
|
|
||||||
{
|
|
||||||
Entry = entry;
|
|
||||||
Values = values;
|
|
||||||
}
|
|
||||||
|
|
||||||
public TreeRouteMatchingEntry Entry { get; }
|
|
||||||
|
|
||||||
public IDictionary<string, object> Values { get; }
|
|
||||||
|
|
||||||
public override bool Equals(object obj)
|
|
||||||
{
|
|
||||||
if (obj is TemplateMatch)
|
|
||||||
{
|
|
||||||
return Equals((TemplateMatch)obj);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Equals(TemplateMatch other)
|
|
||||||
{
|
|
||||||
return
|
|
||||||
object.ReferenceEquals(Entry, other.Entry) &&
|
|
||||||
object.ReferenceEquals(Values, other.Values);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override int GetHashCode()
|
|
||||||
{
|
|
||||||
var hash = new HashCodeCombiner();
|
|
||||||
hash.Add(Entry);
|
|
||||||
hash.Add(Values);
|
|
||||||
return hash.CombinedHash;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool operator ==(TemplateMatch left, TemplateMatch right)
|
|
||||||
{
|
|
||||||
return left.Equals(right);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool operator !=(TemplateMatch left, TemplateMatch right)
|
|
||||||
{
|
|
||||||
return !left.Equals(right);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private VirtualPathData GetVirtualPathForNamedRoute(VirtualPathContext context)
|
|
||||||
{
|
|
||||||
TreeRouteLinkGenerationEntry entry;
|
|
||||||
if (_namedEntries.TryGetValue(context.RouteName, out entry))
|
|
||||||
{
|
|
||||||
var path = GenerateVirtualPath(context, entry);
|
|
||||||
if (path != null)
|
|
||||||
{
|
|
||||||
context.IsBound = true;
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private VirtualPathData GenerateVirtualPath(VirtualPathContext context, TreeRouteLinkGenerationEntry entry)
|
|
||||||
{
|
|
||||||
// In attribute the context includes the values that are used to select this entry - typically
|
|
||||||
// these will be the standard 'action', 'controller' and maybe 'area' tokens. However, we don't
|
|
||||||
// want to pass these to the link generation code, or else they will end up as query parameters.
|
|
||||||
//
|
|
||||||
// So, we need to exclude from here any values that are 'required link values', but aren't
|
|
||||||
// parameters in the template.
|
|
||||||
//
|
|
||||||
// Ex:
|
|
||||||
// template: api/Products/{action}
|
|
||||||
// required values: { id = "5", action = "Buy", Controller = "CoolProducts" }
|
|
||||||
//
|
|
||||||
// result: { id = "5", action = "Buy" }
|
|
||||||
var inputValues = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
foreach (var kvp in context.Values)
|
|
||||||
{
|
|
||||||
if (entry.RequiredLinkValues.ContainsKey(kvp.Key))
|
|
||||||
{
|
|
||||||
var parameter = entry.Template.Parameters
|
|
||||||
.FirstOrDefault(p => string.Equals(p.Name, kvp.Key, StringComparison.OrdinalIgnoreCase));
|
|
||||||
|
|
||||||
if (parameter == null)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inputValues.Add(kvp.Key, kvp.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
var bindingResult = entry.Binder.GetValues(context.AmbientValues, inputValues);
|
|
||||||
if (bindingResult == null)
|
|
||||||
{
|
|
||||||
// A required parameter in the template didn't get a value.
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var matched = RouteConstraintMatcher.Match(
|
|
||||||
entry.Constraints,
|
|
||||||
bindingResult.CombinedValues,
|
|
||||||
context.Context,
|
|
||||||
this,
|
|
||||||
RouteDirection.UrlGeneration,
|
|
||||||
_constraintLogger);
|
|
||||||
|
|
||||||
if (!matched)
|
|
||||||
{
|
|
||||||
// A constraint rejected this link.
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// These values are used to signal to the next route what we would produce if we round-tripped
|
|
||||||
// (generate a link and then parse). In MVC the 'next route' is typically the MvcRouteHandler.
|
|
||||||
var providedValues = new Dictionary<string, object>(
|
|
||||||
bindingResult.AcceptedValues,
|
|
||||||
StringComparer.OrdinalIgnoreCase);
|
|
||||||
providedValues.Add(AttributeRouting.RouteGroupKey, entry.RouteGroup);
|
|
||||||
|
|
||||||
var childContext = new VirtualPathContext(context.Context, context.AmbientValues, context.Values)
|
|
||||||
{
|
|
||||||
ProvidedValues = providedValues,
|
|
||||||
};
|
|
||||||
|
|
||||||
var pathData = _next.GetVirtualPath(childContext);
|
|
||||||
if (pathData != null)
|
|
||||||
{
|
|
||||||
// If path is non-null then the target router short-circuited, we don't expect this
|
|
||||||
// in typical MVC scenarios.
|
|
||||||
return pathData;
|
|
||||||
}
|
|
||||||
else if (!childContext.IsBound)
|
|
||||||
{
|
|
||||||
// The target router has rejected these values. We don't expect this in typical MVC scenarios.
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var path = entry.Binder.BindValues(bindingResult.AcceptedValues);
|
|
||||||
if (path == null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new VirtualPathData(this, path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
// 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;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNet.Mvc.Routing
|
|
||||||
{
|
|
||||||
public class UrlMatchingNode
|
|
||||||
{
|
|
||||||
public UrlMatchingNode(int length)
|
|
||||||
{
|
|
||||||
Length = length;
|
|
||||||
|
|
||||||
Matches = new List<TreeRouteMatchingEntry>();
|
|
||||||
Literals = new Dictionary<string, UrlMatchingNode>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int Length { get; }
|
|
||||||
|
|
||||||
// These entries are sorted by precedence then template
|
|
||||||
public List<TreeRouteMatchingEntry> Matches { get; }
|
|
||||||
|
|
||||||
public Dictionary<string, UrlMatchingNode> Literals { get; }
|
|
||||||
|
|
||||||
public UrlMatchingNode ConstrainedParameters { get; set; }
|
|
||||||
|
|
||||||
public UrlMatchingNode Parameters { get; set; }
|
|
||||||
|
|
||||||
public UrlMatchingNode ConstrainedCatchAlls { get; set; }
|
|
||||||
|
|
||||||
public UrlMatchingNode CatchAlls { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
// 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.Mvc.Routing
|
|
||||||
{
|
|
||||||
public class UrlMatchingTree
|
|
||||||
{
|
|
||||||
public UrlMatchingTree(int order)
|
|
||||||
{
|
|
||||||
Order = order;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int Order { get; }
|
|
||||||
|
|
||||||
public UrlMatchingNode Root { get; } = new UrlMatchingNode(length: 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -14,15 +14,15 @@
|
||||||
"Microsoft.AspNet.FileProviders.Abstractions": "1.0.0-*",
|
"Microsoft.AspNet.FileProviders.Abstractions": "1.0.0-*",
|
||||||
"Microsoft.AspNet.Hosting.Abstractions": "1.0.0-*",
|
"Microsoft.AspNet.Hosting.Abstractions": "1.0.0-*",
|
||||||
"Microsoft.AspNet.Mvc.Abstractions": "6.0.0-*",
|
"Microsoft.AspNet.Mvc.Abstractions": "6.0.0-*",
|
||||||
|
"Microsoft.AspNet.Routing.DecisionTree.Sources": {
|
||||||
|
"type": "build",
|
||||||
|
"version": "1.0.0-*"
|
||||||
|
},
|
||||||
"Microsoft.Extensions.PlatformAbstractions": "1.0.0-*",
|
"Microsoft.Extensions.PlatformAbstractions": "1.0.0-*",
|
||||||
"Microsoft.Extensions.ClosedGenericMatcher.Sources": {
|
"Microsoft.Extensions.ClosedGenericMatcher.Sources": {
|
||||||
"version": "1.0.0-*",
|
"version": "1.0.0-*",
|
||||||
"type": "build"
|
"type": "build"
|
||||||
},
|
},
|
||||||
"Microsoft.Extensions.HashCodeCombiner.Sources": {
|
|
||||||
"type": "build",
|
|
||||||
"version": "1.0.0-*"
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.Logging.Abstractions": "1.0.0-*",
|
"Microsoft.Extensions.Logging.Abstractions": "1.0.0-*",
|
||||||
"Microsoft.Extensions.MemoryPool": "1.0.0-*",
|
"Microsoft.Extensions.MemoryPool": "1.0.0-*",
|
||||||
"Microsoft.Extensions.PropertyActivator.Sources": {
|
"Microsoft.Extensions.PropertyActivator.Sources": {
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ using Microsoft.AspNet.Mvc.Filters;
|
||||||
using Microsoft.AspNet.Mvc.Infrastructure;
|
using Microsoft.AspNet.Mvc.Infrastructure;
|
||||||
using Microsoft.AspNet.Mvc.ModelBinding;
|
using Microsoft.AspNet.Mvc.ModelBinding;
|
||||||
using Microsoft.AspNet.Mvc.Routing;
|
using Microsoft.AspNet.Mvc.Routing;
|
||||||
|
using Microsoft.AspNet.Routing.Tree;
|
||||||
using Moq;
|
using Moq;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
|
|
@ -250,7 +251,7 @@ namespace Microsoft.AspNet.Mvc.Controllers
|
||||||
|
|
||||||
var routeconstraint = Assert.Single(action.RouteConstraints);
|
var routeconstraint = Assert.Single(action.RouteConstraints);
|
||||||
Assert.Equal(RouteKeyHandling.RequireKey, routeconstraint.KeyHandling);
|
Assert.Equal(RouteKeyHandling.RequireKey, routeconstraint.KeyHandling);
|
||||||
Assert.Equal(AttributeRouting.RouteGroupKey, routeconstraint.RouteKey);
|
Assert.Equal(TreeRouter.RouteGroupKey, routeconstraint.RouteKey);
|
||||||
|
|
||||||
var controller = Assert.Single(action.RouteValueDefaults,
|
var controller = Assert.Single(action.RouteValueDefaults,
|
||||||
rc => rc.Key.Equals("controller"));
|
rc => rc.Key.Equals("controller"));
|
||||||
|
|
@ -954,7 +955,7 @@ namespace Microsoft.AspNet.Mvc.Controllers
|
||||||
{
|
{
|
||||||
Assert.Equal(6, actionDescriptor.RouteConstraints.Count);
|
Assert.Equal(6, actionDescriptor.RouteConstraints.Count);
|
||||||
var routeGroupConstraint = Assert.Single(actionDescriptor.RouteConstraints,
|
var routeGroupConstraint = Assert.Single(actionDescriptor.RouteConstraints,
|
||||||
rc => rc.RouteKey.Equals(AttributeRouting.RouteGroupKey));
|
rc => rc.RouteKey.Equals(TreeRouter.RouteGroupKey));
|
||||||
Assert.Equal(RouteKeyHandling.DenyKey, routeGroupConstraint.KeyHandling);
|
Assert.Equal(RouteKeyHandling.DenyKey, routeGroupConstraint.KeyHandling);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -978,7 +979,7 @@ namespace Microsoft.AspNet.Mvc.Controllers
|
||||||
|
|
||||||
Assert.Equal(1, indexAction.RouteConstraints.Count);
|
Assert.Equal(1, indexAction.RouteConstraints.Count);
|
||||||
|
|
||||||
var routeGroupConstraint = Assert.Single(indexAction.RouteConstraints, rc => rc.RouteKey.Equals(AttributeRouting.RouteGroupKey));
|
var routeGroupConstraint = Assert.Single(indexAction.RouteConstraints, rc => rc.RouteKey.Equals(TreeRouter.RouteGroupKey));
|
||||||
Assert.Equal(RouteKeyHandling.RequireKey, routeGroupConstraint.KeyHandling);
|
Assert.Equal(RouteKeyHandling.RequireKey, routeGroupConstraint.KeyHandling);
|
||||||
Assert.NotNull(routeGroupConstraint.RouteValue);
|
Assert.NotNull(routeGroupConstraint.RouteValue);
|
||||||
|
|
||||||
|
|
@ -1027,7 +1028,7 @@ namespace Microsoft.AspNet.Mvc.Controllers
|
||||||
|
|
||||||
var groupIds = actions.Select(
|
var groupIds = actions.Select(
|
||||||
a => a.RouteConstraints
|
a => a.RouteConstraints
|
||||||
.Where(rc => rc.RouteKey == AttributeRouting.RouteGroupKey)
|
.Where(rc => rc.RouteKey == TreeRouter.RouteGroupKey)
|
||||||
.Select(rc => rc.RouteValue)
|
.Select(rc => rc.RouteValue)
|
||||||
.Single())
|
.Single())
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,8 @@ using System.Threading.Tasks;
|
||||||
using Microsoft.AspNet.Http;
|
using Microsoft.AspNet.Http;
|
||||||
using Microsoft.AspNet.Mvc.Abstractions;
|
using Microsoft.AspNet.Mvc.Abstractions;
|
||||||
using Microsoft.AspNet.Mvc.Internal;
|
using Microsoft.AspNet.Mvc.Internal;
|
||||||
using Microsoft.AspNet.Mvc.Routing;
|
|
||||||
using Microsoft.AspNet.Routing;
|
using Microsoft.AspNet.Routing;
|
||||||
|
using Microsoft.AspNet.Routing.Tree;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Logging.Testing;
|
using Microsoft.Extensions.Logging.Testing;
|
||||||
using Microsoft.Extensions.OptionsModel;
|
using Microsoft.Extensions.OptionsModel;
|
||||||
|
|
@ -141,7 +141,7 @@ namespace Microsoft.AspNet.Mvc.Infrastructure
|
||||||
var handler = new MvcRouteHandler();
|
var handler = new MvcRouteHandler();
|
||||||
|
|
||||||
var originalRouteData = context.RouteData;
|
var originalRouteData = context.RouteData;
|
||||||
originalRouteData.Values.Add(AttributeRouting.RouteGroupKey, "/Home/Test");
|
originalRouteData.Values.Add(TreeRouter.RouteGroupKey, "/Home/Test");
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
await handler.RouteAsync(context);
|
await handler.RouteAsync(context);
|
||||||
|
|
@ -152,7 +152,7 @@ namespace Microsoft.AspNet.Mvc.Infrastructure
|
||||||
Assert.Same(actionRouteData, context.RouteData);
|
Assert.Same(actionRouteData, context.RouteData);
|
||||||
|
|
||||||
// The new routedata is a copy
|
// The new routedata is a copy
|
||||||
Assert.False(context.RouteData.Values.ContainsKey(AttributeRouting.RouteGroupKey));
|
Assert.False(context.RouteData.Values.ContainsKey(TreeRouter.RouteGroupKey));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
|
||||||
|
|
@ -1,286 +0,0 @@
|
||||||
// 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 Microsoft.AspNet.Mvc.Routing;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNet.Mvc.Internal.DecisionTree
|
|
||||||
{
|
|
||||||
public class DecisionTreeBuilderTest
|
|
||||||
{
|
|
||||||
[Fact]
|
|
||||||
public void BuildTree_Empty()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var items = new List<Item>();
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var tree = DecisionTreeBuilder<Item>.GenerateTree(items, new ItemClassifier());
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.Empty(tree.Criteria);
|
|
||||||
Assert.Empty(tree.Matches);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void BuildTree_TrivialMatch()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var items = new List<Item>();
|
|
||||||
|
|
||||||
var item = new Item();
|
|
||||||
items.Add(item);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var tree = DecisionTreeBuilder<Item>.GenerateTree(items, new ItemClassifier());
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.Empty(tree.Criteria);
|
|
||||||
Assert.Same(item, Assert.Single(tree.Matches));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void BuildTree_WithMultipleCriteria()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var items = new List<Item>();
|
|
||||||
|
|
||||||
var item = new Item();
|
|
||||||
item.Criteria.Add("area", new DecisionCriterionValue(value: "Admin", isCatchAll: false));
|
|
||||||
item.Criteria.Add("controller", new DecisionCriterionValue(value: "Users", isCatchAll: false));
|
|
||||||
item.Criteria.Add("action", new DecisionCriterionValue(value: "AddUser", isCatchAll: false));
|
|
||||||
items.Add(item);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var tree = DecisionTreeBuilder<Item>.GenerateTree(items, new ItemClassifier());
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.Empty(tree.Matches);
|
|
||||||
|
|
||||||
var area = Assert.Single(tree.Criteria);
|
|
||||||
Assert.Equal("area", area.Key);
|
|
||||||
Assert.Null(area.Fallback);
|
|
||||||
|
|
||||||
var admin = Assert.Single(area.Branches);
|
|
||||||
Assert.Equal("Admin", admin.Key);
|
|
||||||
Assert.Empty(admin.Value.Matches);
|
|
||||||
|
|
||||||
var controller = Assert.Single(admin.Value.Criteria);
|
|
||||||
Assert.Equal("controller", controller.Key);
|
|
||||||
Assert.Null(controller.Fallback);
|
|
||||||
|
|
||||||
var users = Assert.Single(controller.Branches);
|
|
||||||
Assert.Equal("Users", users.Key);
|
|
||||||
Assert.Empty(users.Value.Matches);
|
|
||||||
|
|
||||||
var action = Assert.Single(users.Value.Criteria);
|
|
||||||
Assert.Equal("action", action.Key);
|
|
||||||
Assert.Null(action.Fallback);
|
|
||||||
|
|
||||||
var addUser = Assert.Single(action.Branches);
|
|
||||||
Assert.Equal("AddUser", addUser.Key);
|
|
||||||
Assert.Empty(addUser.Value.Criteria);
|
|
||||||
Assert.Same(item, Assert.Single(addUser.Value.Matches));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void BuildTree_WithMultipleItems()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var items = new List<Item>();
|
|
||||||
|
|
||||||
var item1 = new Item();
|
|
||||||
item1.Criteria.Add("controller", new DecisionCriterionValue(value: "Store", isCatchAll: false));
|
|
||||||
item1.Criteria.Add("action", new DecisionCriterionValue(value: "Buy", isCatchAll: false));
|
|
||||||
items.Add(item1);
|
|
||||||
|
|
||||||
var item2 = new Item();
|
|
||||||
item2.Criteria.Add("controller", new DecisionCriterionValue(value: "Store", isCatchAll: false));
|
|
||||||
item2.Criteria.Add("action", new DecisionCriterionValue(value: "Checkout", isCatchAll: false));
|
|
||||||
items.Add(item2);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var tree = DecisionTreeBuilder<Item>.GenerateTree(items, new ItemClassifier());
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.Empty(tree.Matches);
|
|
||||||
|
|
||||||
var action = Assert.Single(tree.Criteria);
|
|
||||||
Assert.Equal("action", action.Key);
|
|
||||||
Assert.Null(action.Fallback);
|
|
||||||
|
|
||||||
var buy = action.Branches["Buy"];
|
|
||||||
Assert.Empty(buy.Matches);
|
|
||||||
|
|
||||||
var controller = Assert.Single(buy.Criteria);
|
|
||||||
Assert.Equal("controller", controller.Key);
|
|
||||||
Assert.Null(controller.Fallback);
|
|
||||||
|
|
||||||
var store = Assert.Single(controller.Branches);
|
|
||||||
Assert.Equal("Store", store.Key);
|
|
||||||
Assert.Empty(store.Value.Criteria);
|
|
||||||
Assert.Same(item1, Assert.Single(store.Value.Matches));
|
|
||||||
|
|
||||||
var checkout = action.Branches["Checkout"];
|
|
||||||
Assert.Empty(checkout.Matches);
|
|
||||||
|
|
||||||
controller = Assert.Single(checkout.Criteria);
|
|
||||||
Assert.Equal("controller", controller.Key);
|
|
||||||
Assert.Null(controller.Fallback);
|
|
||||||
|
|
||||||
store = Assert.Single(controller.Branches);
|
|
||||||
Assert.Equal("Store", store.Key);
|
|
||||||
Assert.Empty(store.Value.Criteria);
|
|
||||||
Assert.Same(item2, Assert.Single(store.Value.Matches));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void BuildTree_WithInteriorMatch()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var items = new List<Item>();
|
|
||||||
|
|
||||||
var item1 = new Item();
|
|
||||||
item1.Criteria.Add("controller", new DecisionCriterionValue(value: "Store", isCatchAll: false));
|
|
||||||
item1.Criteria.Add("action", new DecisionCriterionValue(value: "Buy", isCatchAll: false));
|
|
||||||
items.Add(item1);
|
|
||||||
|
|
||||||
var item2 = new Item();
|
|
||||||
item2.Criteria.Add("controller", new DecisionCriterionValue(value: "Store", isCatchAll: false));
|
|
||||||
item2.Criteria.Add("action", new DecisionCriterionValue(value: "Checkout", isCatchAll: false));
|
|
||||||
items.Add(item2);
|
|
||||||
|
|
||||||
var item3 = new Item();
|
|
||||||
item3.Criteria.Add("action", new DecisionCriterionValue(value: "Buy", isCatchAll: false));
|
|
||||||
items.Add(item3);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var tree = DecisionTreeBuilder<Item>.GenerateTree(items, new ItemClassifier());
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.Empty(tree.Matches);
|
|
||||||
|
|
||||||
var action = Assert.Single(tree.Criteria);
|
|
||||||
Assert.Equal("action", action.Key);
|
|
||||||
Assert.Null(action.Fallback);
|
|
||||||
|
|
||||||
var buy = action.Branches["Buy"];
|
|
||||||
Assert.Same(item3, Assert.Single(buy.Matches));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void BuildTree_WithCatchAll()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var items = new List<Item>();
|
|
||||||
|
|
||||||
var item1 = new Item();
|
|
||||||
item1.Criteria.Add("country", new DecisionCriterionValue(value: "CA", isCatchAll: false));
|
|
||||||
item1.Criteria.Add("controller", new DecisionCriterionValue(value: "Store", isCatchAll: false));
|
|
||||||
item1.Criteria.Add("action", new DecisionCriterionValue(value: "Checkout", isCatchAll: false));
|
|
||||||
items.Add(item1);
|
|
||||||
|
|
||||||
var item2 = new Item();
|
|
||||||
item2.Criteria.Add("country", new DecisionCriterionValue(value: "US", isCatchAll: false));
|
|
||||||
item2.Criteria.Add("controller", new DecisionCriterionValue(value: "Store", isCatchAll: false));
|
|
||||||
item2.Criteria.Add("action", new DecisionCriterionValue(value: "Checkout", isCatchAll: false));
|
|
||||||
items.Add(item2);
|
|
||||||
|
|
||||||
var item3 = new Item();
|
|
||||||
item3.Criteria.Add("country", new DecisionCriterionValue(value: null, isCatchAll: true));
|
|
||||||
item3.Criteria.Add("controller", new DecisionCriterionValue(value: "Store", isCatchAll: false));
|
|
||||||
item3.Criteria.Add("action", new DecisionCriterionValue(value: "Checkout", isCatchAll: false));
|
|
||||||
items.Add(item3);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var tree = DecisionTreeBuilder<Item>.GenerateTree(items, new ItemClassifier());
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.Empty(tree.Matches);
|
|
||||||
|
|
||||||
var country = Assert.Single(tree.Criteria);
|
|
||||||
Assert.Equal("country", country.Key);
|
|
||||||
|
|
||||||
var fallback = country.Fallback;
|
|
||||||
Assert.NotNull(fallback);
|
|
||||||
|
|
||||||
var controller = Assert.Single(fallback.Criteria);
|
|
||||||
Assert.Equal("controller", controller.Key);
|
|
||||||
Assert.Null(controller.Fallback);
|
|
||||||
|
|
||||||
var store = Assert.Single(controller.Branches);
|
|
||||||
Assert.Equal("Store", store.Key);
|
|
||||||
Assert.Empty(store.Value.Matches);
|
|
||||||
|
|
||||||
var action = Assert.Single(store.Value.Criteria);
|
|
||||||
Assert.Equal("action", action.Key);
|
|
||||||
Assert.Null(action.Fallback);
|
|
||||||
|
|
||||||
var checkout = Assert.Single(action.Branches);
|
|
||||||
Assert.Equal("Checkout", checkout.Key);
|
|
||||||
Assert.Empty(checkout.Value.Criteria);
|
|
||||||
Assert.Same(item3, Assert.Single(checkout.Value.Matches));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void BuildTree_WithDivergentCriteria()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var items = new List<Item>();
|
|
||||||
|
|
||||||
var item1 = new Item();
|
|
||||||
item1.Criteria.Add("controller", new DecisionCriterionValue(value: "Store", isCatchAll: false));
|
|
||||||
item1.Criteria.Add("action", new DecisionCriterionValue(value: "Buy", isCatchAll: false));
|
|
||||||
items.Add(item1);
|
|
||||||
|
|
||||||
var item2 = new Item();
|
|
||||||
item2.Criteria.Add("controller", new DecisionCriterionValue(value: "Store", isCatchAll: false));
|
|
||||||
item2.Criteria.Add("action", new DecisionCriterionValue(value: "Checkout", isCatchAll: false));
|
|
||||||
items.Add(item2);
|
|
||||||
|
|
||||||
var item3 = new Item();
|
|
||||||
item3.Criteria.Add("stub", new DecisionCriterionValue(value: "Bleh", isCatchAll: false));
|
|
||||||
items.Add(item3);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var tree = DecisionTreeBuilder<Item>.GenerateTree(items, new ItemClassifier());
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.Empty(tree.Matches);
|
|
||||||
|
|
||||||
var action = tree.Criteria[0];
|
|
||||||
Assert.Equal("action", action.Key);
|
|
||||||
|
|
||||||
var stub = tree.Criteria[1];
|
|
||||||
Assert.Equal("stub", stub.Key);
|
|
||||||
}
|
|
||||||
|
|
||||||
private class Item
|
|
||||||
{
|
|
||||||
public Item()
|
|
||||||
{
|
|
||||||
Criteria = new Dictionary<string, DecisionCriterionValue>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Dictionary<string, DecisionCriterionValue> Criteria { get; private set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
private class ItemClassifier : IClassifier<Item>
|
|
||||||
{
|
|
||||||
public IEqualityComparer<object> ValueComparer
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return new RouteValueEqualityComparer();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public IDictionary<string, DecisionCriterionValue> GetCriteria(Item item)
|
|
||||||
{
|
|
||||||
return item.Criteria;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,338 +0,0 @@
|
||||||
// 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;
|
|
||||||
using System.Linq;
|
|
||||||
using Microsoft.AspNet.Http.Internal;
|
|
||||||
using Microsoft.AspNet.Mvc.Routing;
|
|
||||||
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<TreeRouteLinkGenerationEntry>();
|
|
||||||
|
|
||||||
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).Entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void SelectSingleEntry_MultipleCriteria()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var entries = new List<TreeRouteLinkGenerationEntry>();
|
|
||||||
|
|
||||||
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).Entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void SelectSingleEntry_MultipleCriteria_AmbientValues()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var entries = new List<TreeRouteLinkGenerationEntry>();
|
|
||||||
|
|
||||||
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
|
|
||||||
var match = Assert.Single(matches);
|
|
||||||
Assert.Same(entry, match.Entry);
|
|
||||||
Assert.False(match.IsFallbackMatch);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void SelectSingleEntry_MultipleCriteria_Replaced()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var entries = new List<TreeRouteLinkGenerationEntry>();
|
|
||||||
|
|
||||||
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
|
|
||||||
var match = Assert.Single(matches);
|
|
||||||
Assert.Same(entry, match.Entry);
|
|
||||||
Assert.False(match.IsFallbackMatch);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void SelectSingleEntry_MultipleCriteria_AmbientValue_Ignored()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var entries = new List<TreeRouteLinkGenerationEntry>();
|
|
||||||
|
|
||||||
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
|
|
||||||
var match = Assert.Single(matches);
|
|
||||||
Assert.Same(entry, match.Entry);
|
|
||||||
Assert.True(match.IsFallbackMatch);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void SelectSingleEntry_MultipleCriteria_NoMatch()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var entries = new List<TreeRouteLinkGenerationEntry>();
|
|
||||||
|
|
||||||
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<TreeRouteLinkGenerationEntry>();
|
|
||||||
|
|
||||||
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<TreeRouteLinkGenerationEntry>();
|
|
||||||
|
|
||||||
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).Entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void SelectMultipleEntries_BothMatch_CriteriaSubset()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var entries = new List<TreeRouteLinkGenerationEntry>();
|
|
||||||
|
|
||||||
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).Select(m => m.Entry).ToList();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.Equal(entries, matches);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void SelectMultipleEntries_BothMatch_NonOverlappingCriteria()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var entries = new List<TreeRouteLinkGenerationEntry>();
|
|
||||||
|
|
||||||
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).Select(m => m.Entry).ToList();
|
|
||||||
|
|
||||||
// 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<TreeRouteLinkGenerationEntry>();
|
|
||||||
|
|
||||||
var entry1 = CreateEntry(new { controller = "Store", action = "Buy" });
|
|
||||||
entry1.GenerationPrecedence = 0;
|
|
||||||
entries.Add(entry1);
|
|
||||||
|
|
||||||
var entry2 = CreateEntry(new { controller = "Store", action = "Buy" });
|
|
||||||
entry2.Order = 1;
|
|
||||||
entry2.GenerationPrecedence = 1;
|
|
||||||
entries.Add(entry2);
|
|
||||||
|
|
||||||
var tree = new LinkGenerationDecisionTree(entries);
|
|
||||||
|
|
||||||
var context = CreateContext(new { controller = "Store", action = "Buy" });
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var matches = tree.GetMatches(context).Select(m => m.Entry).ToList();
|
|
||||||
|
|
||||||
// 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<TreeRouteLinkGenerationEntry>();
|
|
||||||
|
|
||||||
var entry1 = CreateEntry(new { controller = "Store", action = "Buy" });
|
|
||||||
entry1.GenerationPrecedence = 1;
|
|
||||||
entries.Add(entry1);
|
|
||||||
|
|
||||||
var entry2 = CreateEntry(new { controller = "Store", action = "Buy" });
|
|
||||||
entry2.GenerationPrecedence = 0;
|
|
||||||
entries.Add(entry2);
|
|
||||||
|
|
||||||
var tree = new LinkGenerationDecisionTree(entries);
|
|
||||||
|
|
||||||
var context = CreateContext(new { controller = "Store", action = "Buy" });
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var matches = tree.GetMatches(context).Select(m => m.Entry).ToList();
|
|
||||||
|
|
||||||
// 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<TreeRouteLinkGenerationEntry>();
|
|
||||||
|
|
||||||
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).Select(m => m.Entry).ToList();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.Equal(entries, matches);
|
|
||||||
}
|
|
||||||
|
|
||||||
private TreeRouteLinkGenerationEntry CreateEntry(object requiredValues)
|
|
||||||
{
|
|
||||||
var entry = new TreeRouteLinkGenerationEntry();
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,125 +0,0 @@
|
||||||
// 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.
|
|
||||||
|
|
||||||
#if DNX451
|
|
||||||
using System;
|
|
||||||
using Microsoft.AspNet.Routing;
|
|
||||||
using Microsoft.AspNet.Routing.Template;
|
|
||||||
using Microsoft.Extensions.OptionsModel;
|
|
||||||
using Moq;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNet.Mvc.Routing
|
|
||||||
{
|
|
||||||
public class AttributeRoutePrecedenceTests
|
|
||||||
{
|
|
||||||
[Theory]
|
|
||||||
[InlineData("Employees/{id}", "Employees/{employeeId}")]
|
|
||||||
[InlineData("abc", "def")]
|
|
||||||
[InlineData("{x:alpha}", "{x:int}")]
|
|
||||||
public void ComputeMatched_IsEqual(string xTemplate, string yTemplate)
|
|
||||||
{
|
|
||||||
// Arrange & Act
|
|
||||||
var xPrededence = ComputeMatched(xTemplate);
|
|
||||||
var yPrededence = ComputeMatched(yTemplate);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.Equal(xPrededence, yPrededence);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[InlineData("Employees/{id}", "Employees/{employeeId}")]
|
|
||||||
[InlineData("abc", "def")]
|
|
||||||
[InlineData("{x:alpha}", "{x:int}")]
|
|
||||||
public void ComputeGenerated_IsEqual(string xTemplate, string yTemplate)
|
|
||||||
{
|
|
||||||
// Arrange & Act
|
|
||||||
var xPrededence = ComputeGenerated(xTemplate);
|
|
||||||
var yPrededence = ComputeGenerated(yTemplate);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.Equal(xPrededence, yPrededence);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[InlineData("abc", "a{x}")]
|
|
||||||
[InlineData("abc", "{x}c")]
|
|
||||||
[InlineData("abc", "{x:int}")]
|
|
||||||
[InlineData("abc", "{x}")]
|
|
||||||
[InlineData("abc", "{*x}")]
|
|
||||||
[InlineData("{x:int}", "{x}")]
|
|
||||||
[InlineData("{x:int}", "{*x}")]
|
|
||||||
[InlineData("a{x}", "{x}")]
|
|
||||||
[InlineData("{x}c", "{x}")]
|
|
||||||
[InlineData("a{x}", "{*x}")]
|
|
||||||
[InlineData("{x}c", "{*x}")]
|
|
||||||
[InlineData("{x}", "{*x}")]
|
|
||||||
[InlineData("{*x:maxlength(10)}", "{*x}")]
|
|
||||||
[InlineData("abc/def", "abc/{x:int}")]
|
|
||||||
[InlineData("abc/def", "abc/{x}")]
|
|
||||||
[InlineData("abc/def", "abc/{*x}")]
|
|
||||||
[InlineData("abc/{x:int}", "abc/{x}")]
|
|
||||||
[InlineData("abc/{x:int}", "abc/{*x}")]
|
|
||||||
[InlineData("abc/{x}", "abc/{*x}")]
|
|
||||||
[InlineData("{x}/{y:int}", "{x}/{y}")]
|
|
||||||
public void ComputeMatched_IsLessThan(string xTemplate, string yTemplate)
|
|
||||||
{
|
|
||||||
// Arrange & Act
|
|
||||||
var xPrededence = ComputeMatched(xTemplate);
|
|
||||||
var yPrededence = ComputeMatched(yTemplate);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.True(xPrededence < yPrededence);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[InlineData("abc", "a{x}")]
|
|
||||||
[InlineData("abc", "{x}c")]
|
|
||||||
[InlineData("abc", "{x:int}")]
|
|
||||||
[InlineData("abc", "{x}")]
|
|
||||||
[InlineData("abc", "{*x}")]
|
|
||||||
[InlineData("{x:int}", "{x}")]
|
|
||||||
[InlineData("{x:int}", "{*x}")]
|
|
||||||
[InlineData("a{x}", "{x}")]
|
|
||||||
[InlineData("{x}c", "{x}")]
|
|
||||||
[InlineData("a{x}", "{*x}")]
|
|
||||||
[InlineData("{x}c", "{*x}")]
|
|
||||||
[InlineData("{x}", "{*x}")]
|
|
||||||
[InlineData("{*x:maxlength(10)}", "{*x}")]
|
|
||||||
[InlineData("abc/def", "abc/{x:int}")]
|
|
||||||
[InlineData("abc/def", "abc/{x}")]
|
|
||||||
[InlineData("abc/def", "abc/{*x}")]
|
|
||||||
[InlineData("abc/{x:int}", "abc/{x}")]
|
|
||||||
[InlineData("abc/{x:int}", "abc/{*x}")]
|
|
||||||
[InlineData("abc/{x}", "abc/{*x}")]
|
|
||||||
[InlineData("{x}/{y:int}", "{x}/{y}")]
|
|
||||||
public void ComputeGenerated_IsGreaterThan(string xTemplate, string yTemplate)
|
|
||||||
{
|
|
||||||
// Arrange & Act
|
|
||||||
var xPrecedence = ComputeGenerated(xTemplate);
|
|
||||||
var yPrecedence = ComputeGenerated(yTemplate);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.True(xPrecedence > yPrecedence);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static decimal ComputeMatched(string template)
|
|
||||||
{
|
|
||||||
return Compute(template, AttributeRoutePrecedence.ComputeMatched);
|
|
||||||
}
|
|
||||||
private static decimal ComputeGenerated(string template)
|
|
||||||
{
|
|
||||||
return Compute(template, AttributeRoutePrecedence.ComputeGenerated);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static decimal Compute(string template, Func<RouteTemplate, decimal> func)
|
|
||||||
{
|
|
||||||
var options = new Mock<IOptions<RouteOptions>>();
|
|
||||||
options.SetupGet(o => o.Value).Returns(new RouteOptions());
|
|
||||||
|
|
||||||
var parsed = TemplateParser.Parse(template);
|
|
||||||
return func(parsed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
@ -10,6 +10,7 @@ using Microsoft.AspNet.Http.Internal;
|
||||||
using Microsoft.AspNet.Mvc.Abstractions;
|
using Microsoft.AspNet.Mvc.Abstractions;
|
||||||
using Microsoft.AspNet.Mvc.Infrastructure;
|
using Microsoft.AspNet.Mvc.Infrastructure;
|
||||||
using Microsoft.AspNet.Routing;
|
using Microsoft.AspNet.Routing;
|
||||||
|
using Microsoft.AspNet.Routing.Tree;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Logging.Testing;
|
using Microsoft.Extensions.Logging.Testing;
|
||||||
using Moq;
|
using Moq;
|
||||||
|
|
@ -43,7 +44,7 @@ namespace Microsoft.AspNet.Mvc.Routing
|
||||||
},
|
},
|
||||||
RouteConstraints = new List<RouteDataActionConstraint>()
|
RouteConstraints = new List<RouteDataActionConstraint>()
|
||||||
{
|
{
|
||||||
new RouteDataActionConstraint(AttributeRouting.RouteGroupKey, "1"),
|
new RouteDataActionConstraint(TreeRouter.RouteGroupKey, "1"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
new ActionDescriptor()
|
new ActionDescriptor()
|
||||||
|
|
@ -54,7 +55,7 @@ namespace Microsoft.AspNet.Mvc.Routing
|
||||||
},
|
},
|
||||||
RouteConstraints = new List<RouteDataActionConstraint>()
|
RouteConstraints = new List<RouteDataActionConstraint>()
|
||||||
{
|
{
|
||||||
new RouteDataActionConstraint(AttributeRouting.RouteGroupKey, "2"),
|
new RouteDataActionConstraint(TreeRouter.RouteGroupKey, "2"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -87,7 +88,7 @@ namespace Microsoft.AspNet.Mvc.Routing
|
||||||
// Assert 1
|
// Assert 1
|
||||||
Assert.True(context.IsHandled);
|
Assert.True(context.IsHandled);
|
||||||
Assert.Equal("5", context.RouteData.Values["id"]);
|
Assert.Equal("5", context.RouteData.Values["id"]);
|
||||||
Assert.Equal("2", context.RouteData.Values[AttributeRouting.RouteGroupKey]);
|
Assert.Equal("2", context.RouteData.Values[TreeRouter.RouteGroupKey]);
|
||||||
|
|
||||||
handler.Verify(h => h.RouteAsync(It.IsAny<RouteContext>()), Times.Once());
|
handler.Verify(h => h.RouteAsync(It.IsAny<RouteContext>()), Times.Once());
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ using Microsoft.AspNet.Mvc.Abstractions;
|
||||||
using Microsoft.AspNet.Mvc.Controllers;
|
using Microsoft.AspNet.Mvc.Controllers;
|
||||||
using Microsoft.AspNet.Mvc.Infrastructure;
|
using Microsoft.AspNet.Mvc.Infrastructure;
|
||||||
using Microsoft.AspNet.Routing;
|
using Microsoft.AspNet.Routing;
|
||||||
|
using Microsoft.AspNet.Routing.Tree;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Logging.Testing;
|
using Microsoft.Extensions.Logging.Testing;
|
||||||
using Microsoft.Extensions.OptionsModel;
|
using Microsoft.Extensions.OptionsModel;
|
||||||
|
|
@ -125,7 +126,7 @@ namespace Microsoft.AspNet.Mvc.Routing
|
||||||
action.MethodInfo = actionMethod;
|
action.MethodInfo = actionMethod;
|
||||||
action.RouteConstraints = new List<RouteDataActionConstraint>()
|
action.RouteConstraints = new List<RouteDataActionConstraint>()
|
||||||
{
|
{
|
||||||
new RouteDataActionConstraint(AttributeRouting.RouteGroupKey, "group"),
|
new RouteDataActionConstraint(TreeRouter.RouteGroupKey, "group"),
|
||||||
};
|
};
|
||||||
action.AttributeRouteInfo = new AttributeRouteInfo();
|
action.AttributeRouteInfo = new AttributeRouteInfo();
|
||||||
action.AttributeRouteInfo.Template = "{controller}/{action}";
|
action.AttributeRouteInfo.Template = "{controller}/{action}";
|
||||||
|
|
@ -161,7 +162,7 @@ namespace Microsoft.AspNet.Mvc.Routing
|
||||||
DisplayName = displayName,
|
DisplayName = displayName,
|
||||||
RouteConstraints = new List<RouteDataActionConstraint>()
|
RouteConstraints = new List<RouteDataActionConstraint>()
|
||||||
{
|
{
|
||||||
new RouteDataActionConstraint(AttributeRouting.RouteGroupKey, "whatever"),
|
new RouteDataActionConstraint(TreeRouter.RouteGroupKey, "whatever"),
|
||||||
},
|
},
|
||||||
AttributeRouteInfo = new AttributeRouteInfo { Template = template },
|
AttributeRouteInfo = new AttributeRouteInfo { Template = template },
|
||||||
};
|
};
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue