From 01102bba3f86665cf41d0baef7b1f6f446c5beb0 Mon Sep 17 00:00:00 2001 From: Ajay Bhargav Baaskaran Date: Fri, 13 Nov 2015 16:36:46 -0800 Subject: [PATCH] Moving Attribute Routing to Routing --- .../ControllerActionDescriptorBuilder.cs | 5 +- .../Infrastructure/MvcRouteHandler.cs | 4 +- .../DecisionTree/DecisionCriterion.cs | 16 - .../DecisionTree/DecisionCriterionValue.cs | 27 - .../DecisionCriterionValueEqualityComparer.cs | 34 - .../DecisionTree/DecisionTreeBuilder.cs | 234 -- .../Internal/DecisionTree/DecisionTreeNode.cs | 20 - .../Internal/DecisionTree/IClassifier.cs | 14 - .../Internal/DecisionTree/ItemDescriptor.cs | 16 - .../Routing/LinkGenerationDecisionTree.cs | 156 -- .../Internal/Routing/LinkGenerationMatch.cs | 23 - .../Logging/TreeRouterLoggerExtensions.cs | 29 - .../Properties/Resources.Designer.cs | 16 - src/Microsoft.AspNet.Mvc.Core/Resources.resx | 3 - .../Routing/ActionSelectionDecisionTree.cs | 3 +- .../Routing/AttributeRoute.cs | 19 +- .../Routing/AttributeRoutePrecedence.cs | 132 -- .../Routing/AttributeRouting.cs | 4 - .../Routing/RouteValueEqualityComparer.cs | 53 - .../Routing/Tree/TreeRouteBuilder.cs | 167 -- .../Tree/TreeRouteLinkGenerationEntry.cs | 66 - .../Routing/Tree/TreeRouteMatchingEntry.cs | 37 - .../Routing/Tree/TreeRouter.cs | 478 ---- .../Routing/Tree/UrlMatchingNode.cs | 34 - .../Routing/Tree/UrlMatchingTree.cs | 17 - src/Microsoft.AspNet.Mvc.Core/project.json | 8 +- ...ControllerActionDescriptorProviderTests.cs | 9 +- .../Infrastructure/MvcRouteHandlerTests.cs | 6 +- .../DecisionTree/DecisionTreeBuilderTest.cs | 286 --- .../Routing/LinkGenerationDecisionTreeTest.cs | 338 --- .../Routing/AttributeRoutePrecedenceTests.cs | 125 - .../Routing/AttributeRouteTest.cs | 7 +- .../Routing/AttributeRoutingTest.cs | 5 +- .../Routing/TreeRouterTest.cs | 2056 ----------------- 34 files changed, 34 insertions(+), 4413 deletions(-) delete mode 100644 src/Microsoft.AspNet.Mvc.Core/Internal/DecisionTree/DecisionCriterion.cs delete mode 100644 src/Microsoft.AspNet.Mvc.Core/Internal/DecisionTree/DecisionCriterionValue.cs delete mode 100644 src/Microsoft.AspNet.Mvc.Core/Internal/DecisionTree/DecisionCriterionValueEqualityComparer.cs delete mode 100644 src/Microsoft.AspNet.Mvc.Core/Internal/DecisionTree/DecisionTreeBuilder.cs delete mode 100644 src/Microsoft.AspNet.Mvc.Core/Internal/DecisionTree/DecisionTreeNode.cs delete mode 100644 src/Microsoft.AspNet.Mvc.Core/Internal/DecisionTree/IClassifier.cs delete mode 100644 src/Microsoft.AspNet.Mvc.Core/Internal/DecisionTree/ItemDescriptor.cs delete mode 100644 src/Microsoft.AspNet.Mvc.Core/Internal/Routing/LinkGenerationDecisionTree.cs delete mode 100644 src/Microsoft.AspNet.Mvc.Core/Internal/Routing/LinkGenerationMatch.cs delete mode 100644 src/Microsoft.AspNet.Mvc.Core/Logging/TreeRouterLoggerExtensions.cs delete mode 100644 src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRoutePrecedence.cs delete mode 100644 src/Microsoft.AspNet.Mvc.Core/Routing/RouteValueEqualityComparer.cs delete mode 100644 src/Microsoft.AspNet.Mvc.Core/Routing/Tree/TreeRouteBuilder.cs delete mode 100644 src/Microsoft.AspNet.Mvc.Core/Routing/Tree/TreeRouteLinkGenerationEntry.cs delete mode 100644 src/Microsoft.AspNet.Mvc.Core/Routing/Tree/TreeRouteMatchingEntry.cs delete mode 100644 src/Microsoft.AspNet.Mvc.Core/Routing/Tree/TreeRouter.cs delete mode 100644 src/Microsoft.AspNet.Mvc.Core/Routing/Tree/UrlMatchingNode.cs delete mode 100644 src/Microsoft.AspNet.Mvc.Core/Routing/Tree/UrlMatchingTree.cs delete mode 100644 test/Microsoft.AspNet.Mvc.Core.Test/Internal/DecisionTree/DecisionTreeBuilderTest.cs delete mode 100644 test/Microsoft.AspNet.Mvc.Core.Test/Internal/Routing/LinkGenerationDecisionTreeTest.cs delete mode 100644 test/Microsoft.AspNet.Mvc.Core.Test/Routing/AttributeRoutePrecedenceTests.cs delete mode 100644 test/Microsoft.AspNet.Mvc.Core.Test/Routing/TreeRouterTest.cs diff --git a/src/Microsoft.AspNet.Mvc.Core/Controllers/ControllerActionDescriptorBuilder.cs b/src/Microsoft.AspNet.Mvc.Core/Controllers/ControllerActionDescriptorBuilder.cs index 958b9173c9..40ab48a376 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Controllers/ControllerActionDescriptorBuilder.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Controllers/ControllerActionDescriptorBuilder.cs @@ -15,6 +15,7 @@ using Microsoft.AspNet.Mvc.Core; using Microsoft.AspNet.Mvc.Filters; using Microsoft.AspNet.Mvc.Infrastructure; using Microsoft.AspNet.Mvc.Routing; +using Microsoft.AspNet.Routing.Tree; namespace Microsoft.AspNet.Mvc.Controllers { @@ -120,7 +121,7 @@ namespace Microsoft.AspNet.Mvc.Controllers if (hasAttributeRoutes) { actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint( - AttributeRouting.RouteGroupKey, + TreeRouter.RouteGroupKey, string.Empty)); } @@ -534,7 +535,7 @@ namespace Microsoft.AspNet.Mvc.Controllers var routeConstraints = new List(); routeConstraints.Add(new RouteDataActionConstraint( - AttributeRouting.RouteGroupKey, + TreeRouter.RouteGroupKey, routeGroupValue)); actionDescriptor.RouteConstraints = routeConstraints; diff --git a/src/Microsoft.AspNet.Mvc.Core/Infrastructure/MvcRouteHandler.cs b/src/Microsoft.AspNet.Mvc.Core/Infrastructure/MvcRouteHandler.cs index 0ad69d3d2d..b813756d35 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Infrastructure/MvcRouteHandler.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Infrastructure/MvcRouteHandler.cs @@ -10,8 +10,8 @@ using Microsoft.AspNet.Mvc.Core; using Microsoft.AspNet.Mvc.Diagnostics; using Microsoft.AspNet.Mvc.Internal; using Microsoft.AspNet.Mvc.Logging; -using Microsoft.AspNet.Mvc.Routing; using Microsoft.AspNet.Routing; +using Microsoft.AspNet.Routing.Tree; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -80,7 +80,7 @@ namespace Microsoft.AspNet.Mvc.Infrastructure } // Removing RouteGroup from RouteValues to simulate the result of conventional routing - newRouteData.Values.Remove(AttributeRouting.RouteGroupKey); + newRouteData.Values.Remove(TreeRouter.RouteGroupKey); try { diff --git a/src/Microsoft.AspNet.Mvc.Core/Internal/DecisionTree/DecisionCriterion.cs b/src/Microsoft.AspNet.Mvc.Core/Internal/DecisionTree/DecisionCriterion.cs deleted file mode 100644 index 7f4dfef95c..0000000000 --- a/src/Microsoft.AspNet.Mvc.Core/Internal/DecisionTree/DecisionCriterion.cs +++ /dev/null @@ -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 - { - public string Key { get; set; } - - public Dictionary> Branches { get; set; } - - public DecisionTreeNode Fallback { get; set; } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/Internal/DecisionTree/DecisionCriterionValue.cs b/src/Microsoft.AspNet.Mvc.Core/Internal/DecisionTree/DecisionCriterionValue.cs deleted file mode 100644 index ddcebc923e..0000000000 --- a/src/Microsoft.AspNet.Mvc.Core/Internal/DecisionTree/DecisionCriterionValue.cs +++ /dev/null @@ -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; } - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/Internal/DecisionTree/DecisionCriterionValueEqualityComparer.cs b/src/Microsoft.AspNet.Mvc.Core/Internal/DecisionTree/DecisionCriterionValueEqualityComparer.cs deleted file mode 100644 index 48ae73d526..0000000000 --- a/src/Microsoft.AspNet.Mvc.Core/Internal/DecisionTree/DecisionCriterionValueEqualityComparer.cs +++ /dev/null @@ -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 - { - public DecisionCriterionValueEqualityComparer(IEqualityComparer innerComparer) - { - InnerComparer = innerComparer; - } - - public IEqualityComparer 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); - } - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/Internal/DecisionTree/DecisionTreeBuilder.cs b/src/Microsoft.AspNet.Mvc.Core/Internal/DecisionTree/DecisionTreeBuilder.cs deleted file mode 100644 index 19567579a5..0000000000 --- a/src/Microsoft.AspNet.Mvc.Core/Internal/DecisionTree/DecisionTreeBuilder.cs +++ /dev/null @@ -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 - { - public static DecisionTreeNode GenerateTree(IReadOnlyList items, IClassifier classifier) - { - var itemDescriptors = new List>(); - for (var i = 0; i < items.Count; i++) - { - itemDescriptors.Add(new ItemDescriptor() - { - 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 GenerateNode( - TreeBuilderContext context, - DecisionCriterionValueEqualityComparer comparer, - IList> 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(StringComparer.OrdinalIgnoreCase); - - // Matches are items that have no remaining criteria - at this point in the tree - // they are considered accepted. - var matches = new List(); - - // 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> branch; - if (!criterion.TryGetValue(kvp.Value, out branch)) - { - branch = new List>(); - 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>(); - foreach (var criterion in criteria.OrderByDescending(c => c.Value.Count)) - { - var reducedBranches = new Dictionary>(comparer.InnerComparer); - DecisionTreeNode fallback = null; - - foreach (var branch in criterion.Value) - { - var reducedItems = new List>(); - 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() - { - Key = criterion.Key, - Branches = reducedBranches, - Fallback = fallback, - }; - - reducedCriteria.Add(newCriterion); - } - } - - return new DecisionTreeNode() - { - Criteria = reducedCriteria.ToList(), - Matches = matches, - }; - } - - private class TreeBuilderContext - { - public TreeBuilderContext() - { - CurrentCriteria = new HashSet(StringComparer.OrdinalIgnoreCase); - MatchedItems = new HashSet>(); - } - - public TreeBuilderContext(TreeBuilderContext other) - { - CurrentCriteria = new HashSet(other.CurrentCriteria, StringComparer.OrdinalIgnoreCase); - MatchedItems = new HashSet>(); - } - - public HashSet CurrentCriteria { get; private set; } - - public HashSet> MatchedItems { get; private set; } - } - - // Subclass just to give a logical name to a mess of generics - private class Criterion : Dictionary>> - { - public Criterion(DecisionCriterionValueEqualityComparer comparer) - : base(comparer) - { - } - } - } -} diff --git a/src/Microsoft.AspNet.Mvc.Core/Internal/DecisionTree/DecisionTreeNode.cs b/src/Microsoft.AspNet.Mvc.Core/Internal/DecisionTree/DecisionTreeNode.cs deleted file mode 100644 index a117f04258..0000000000 --- a/src/Microsoft.AspNet.Mvc.Core/Internal/DecisionTree/DecisionTreeNode.cs +++ /dev/null @@ -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 - { - // 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 Matches { get; set; } - - // Additional criteria that further branch out from this node. Walk these to fine more items - // matching the input data. - public IList> Criteria { get; set; } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/Internal/DecisionTree/IClassifier.cs b/src/Microsoft.AspNet.Mvc.Core/Internal/DecisionTree/IClassifier.cs deleted file mode 100644 index 1d01d5f75a..0000000000 --- a/src/Microsoft.AspNet.Mvc.Core/Internal/DecisionTree/IClassifier.cs +++ /dev/null @@ -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 - { - IDictionary GetCriteria(TItem item); - - IEqualityComparer ValueComparer { get; } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/Internal/DecisionTree/ItemDescriptor.cs b/src/Microsoft.AspNet.Mvc.Core/Internal/DecisionTree/ItemDescriptor.cs deleted file mode 100644 index dfee1a0305..0000000000 --- a/src/Microsoft.AspNet.Mvc.Core/Internal/DecisionTree/ItemDescriptor.cs +++ /dev/null @@ -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 - { - public IDictionary Criteria { get; set; } - - public int Index { get; set; } - - public TItem Item { get; set; } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/Internal/Routing/LinkGenerationDecisionTree.cs b/src/Microsoft.AspNet.Mvc.Core/Internal/Routing/LinkGenerationDecisionTree.cs deleted file mode 100644 index 7c7b43f121..0000000000 --- a/src/Microsoft.AspNet.Mvc.Core/Internal/Routing/LinkGenerationDecisionTree.cs +++ /dev/null @@ -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 _root; - - public LinkGenerationDecisionTree(IReadOnlyList entries) - { - _root = DecisionTreeBuilder.GenerateTree( - entries, - new AttributeRouteLinkGenerationEntryClassifier()); - } - - public IList GetMatches(VirtualPathContext context) - { - var results = new List(); - 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 results, - VirtualPathContext context, - DecisionTreeNode 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 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 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 - { - public AttributeRouteLinkGenerationEntryClassifier() - { - ValueComparer = new RouteValueEqualityComparer(); - } - - public IEqualityComparer ValueComparer { get; private set; } - - public IDictionary GetCriteria(TreeRouteLinkGenerationEntry item) - { - var results = new Dictionary(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 - { - 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); - } - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/Internal/Routing/LinkGenerationMatch.cs b/src/Microsoft.AspNet.Mvc.Core/Internal/Routing/LinkGenerationMatch.cs deleted file mode 100644 index fa5ce76716..0000000000 --- a/src/Microsoft.AspNet.Mvc.Core/Internal/Routing/LinkGenerationMatch.cs +++ /dev/null @@ -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; } } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/Logging/TreeRouterLoggerExtensions.cs b/src/Microsoft.AspNet.Mvc.Core/Logging/TreeRouterLoggerExtensions.cs deleted file mode 100644 index 2bd5ea47b4..0000000000 --- a/src/Microsoft.AspNet.Mvc.Core/Logging/TreeRouterLoggerExtensions.cs +++ /dev/null @@ -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 _matchedRouteName; - - static TreeRouterLoggerExtensions() - { - _matchedRouteName = LoggerMessage.Define( - 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); - } - } -} diff --git a/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs index 597acc1cd5..777e416d2c 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs @@ -506,22 +506,6 @@ namespace Microsoft.AspNet.Mvc.Core return string.Format(CultureInfo.CurrentCulture, GetString("UnableToFindServices"), p0, p1, p2); } - /// - /// Two or more routes named '{0}' have different templates. - /// - internal static string AttributeRoute_DifferentLinkGenerationEntries_SameName - { - get { return GetString("AttributeRoute_DifferentLinkGenerationEntries_SameName"); } - } - - /// - /// Two or more routes named '{0}' have different templates. - /// - internal static string FormatAttributeRoute_DifferentLinkGenerationEntries_SameName(object p0) - { - return string.Format(CultureInfo.CurrentCulture, GetString("AttributeRoute_DifferentLinkGenerationEntries_SameName"), p0); - } - /// /// Action: '{0}' - Template: '{1}' /// diff --git a/src/Microsoft.AspNet.Mvc.Core/Resources.resx b/src/Microsoft.AspNet.Mvc.Core/Resources.resx index 839ec798a1..5a0b351d07 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Resources.resx +++ b/src/Microsoft.AspNet.Mvc.Core/Resources.resx @@ -213,9 +213,6 @@ 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. - - Two or more routes named '{0}' have different templates. - Action: '{0}' - Template: '{1}' Formats an action descriptor display name and it's associated template. diff --git a/src/Microsoft.AspNet.Mvc.Core/Routing/ActionSelectionDecisionTree.cs b/src/Microsoft.AspNet.Mvc.Core/Routing/ActionSelectionDecisionTree.cs index d5302c2b7f..1faac404d2 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Routing/ActionSelectionDecisionTree.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Routing/ActionSelectionDecisionTree.cs @@ -9,7 +9,8 @@ using System.ComponentModel; using System.Diagnostics; using Microsoft.AspNet.Mvc.Abstractions; 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 { diff --git a/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRoute.cs b/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRoute.cs index b5301071ae..7b9c7119f6 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRoute.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRoute.cs @@ -10,6 +10,7 @@ using Microsoft.AspNet.Mvc.Core; using Microsoft.AspNet.Mvc.Infrastructure; using Microsoft.AspNet.Routing; using Microsoft.AspNet.Routing.Template; +using Microsoft.AspNet.Routing.Tree; using Microsoft.Extensions.Logging; namespace Microsoft.AspNet.Mvc.Routing @@ -19,8 +20,7 @@ namespace Microsoft.AspNet.Mvc.Routing private readonly IRouter _target; private readonly IActionDescriptorsCollectionProvider _actionDescriptorsCollectionProvider; private readonly IInlineConstraintResolver _constraintResolver; - private readonly ILogger _routeLogger; - private readonly ILogger _constraintLogger; + private readonly ILoggerFactory _loggerFactory; private TreeRouter _router; @@ -53,9 +53,7 @@ namespace Microsoft.AspNet.Mvc.Routing _target = target; _actionDescriptorsCollectionProvider = actionDescriptorsCollectionProvider; _constraintResolver = constraintResolver; - - _routeLogger = loggerFactory.CreateLogger(); - _constraintLogger = loggerFactory.CreateLogger(typeof(RouteConstraintMatcher).FullName); + _loggerFactory = loggerFactory; } /// @@ -88,7 +86,7 @@ namespace Microsoft.AspNet.Mvc.Routing private TreeRouter BuildRoute(ActionDescriptorsCollection actions) { - var routeBuilder = new TreeRouteBuilder(_target, _routeLogger, _constraintLogger); + var routeBuilder = new TreeRouteBuilder(_target, _loggerFactory); var routeInfos = GetRouteInfos(_constraintResolver, actions.Items); // 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, RouteGroup = routeInfo.RouteGroup, Template = routeInfo.ParsedTemplate, - TemplateText = routeInfo.RouteTemplate, Name = routeInfo.Name, }); } @@ -127,7 +124,7 @@ namespace Microsoft.AspNet.Mvc.Routing routeInfo.ParsedTemplate, new Dictionary(StringComparer.OrdinalIgnoreCase) { - { AttributeRouting.RouteGroupKey, routeInfo.RouteGroup } + { TreeRouter.RouteGroupKey, routeInfo.RouteGroup } }), Constraints = routeInfo.Constraints }); @@ -203,7 +200,7 @@ namespace Microsoft.AspNet.Mvc.Routing ActionDescriptor action) { var constraint = action.RouteConstraints - .Where(c => c.RouteKey == AttributeRouting.RouteGroupKey) + .Where(c => c.RouteKey == TreeRouter.RouteGroupKey) .FirstOrDefault(); if (constraint == null || constraint.KeyHandling != RouteKeyHandling.RequireKey || @@ -260,8 +257,8 @@ namespace Microsoft.AspNet.Mvc.Routing routeInfo.Order = action.AttributeRouteInfo.Order; - routeInfo.MatchPrecedence = AttributeRoutePrecedence.ComputeMatched(routeInfo.ParsedTemplate); - routeInfo.GenerationPrecedence = AttributeRoutePrecedence.ComputeGenerated(routeInfo.ParsedTemplate); + routeInfo.MatchPrecedence = RoutePrecedence.ComputeMatched(routeInfo.ParsedTemplate); + routeInfo.GenerationPrecedence = RoutePrecedence.ComputeGenerated(routeInfo.ParsedTemplate); routeInfo.Name = action.AttributeRouteInfo.Name; diff --git a/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRoutePrecedence.cs b/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRoutePrecedence.cs deleted file mode 100644 index 2edde2f595..0000000000 --- a/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRoutePrecedence.cs +++ /dev/null @@ -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 -{ - /// - /// Computes precedence for an attribute route template. - /// - 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; - } - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRouting.cs b/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRouting.cs index d15a23104d..47100119e7 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRouting.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRouting.cs @@ -11,10 +11,6 @@ namespace Microsoft.AspNet.Mvc.Routing { 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"; - /// /// Creates an attribute route using the provided services and provided target router. /// diff --git a/src/Microsoft.AspNet.Mvc.Core/Routing/RouteValueEqualityComparer.cs b/src/Microsoft.AspNet.Mvc.Core/Routing/RouteValueEqualityComparer.cs deleted file mode 100644 index 2fbf090fbd..0000000000 --- a/src/Microsoft.AspNet.Mvc.Core/Routing/RouteValueEqualityComparer.cs +++ /dev/null @@ -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 -{ - /// - /// An implementation that compares objects as-if - /// they were route value strings. - /// - /// - /// Values that are are not strings are converted to strings using - /// Convert.ToString(x, CultureInfo.InvariantCulture). null values are converted - /// to the empty string. - /// - /// strings are compared using . - /// - public class RouteValueEqualityComparer : IEqualityComparer - { - /// - 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); - } - } - - /// - 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); - } - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/Routing/Tree/TreeRouteBuilder.cs b/src/Microsoft.AspNet.Mvc.Core/Routing/Tree/TreeRouteBuilder.cs deleted file mode 100644 index 821cde46b2..0000000000 --- a/src/Microsoft.AspNet.Mvc.Core/Routing/Tree/TreeRouteBuilder.cs +++ /dev/null @@ -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 _generatingEntries; - private readonly List _matchingEntries; - - private readonly ILogger _logger; - private readonly ILogger _constraintLogger; - - public TreeRouteBuilder(IRouter target, ILogger routeLogger, ILogger constraintLogger) - { - _target = target; - _generatingEntries = new List(); - _matchingEntries = new List(); - - _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(); - - 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; - }); - } - } -} diff --git a/src/Microsoft.AspNet.Mvc.Core/Routing/Tree/TreeRouteLinkGenerationEntry.cs b/src/Microsoft.AspNet.Mvc.Core/Routing/Tree/TreeRouteLinkGenerationEntry.cs deleted file mode 100644 index 2b032cdedb..0000000000 --- a/src/Microsoft.AspNet.Mvc.Core/Routing/Tree/TreeRouteLinkGenerationEntry.cs +++ /dev/null @@ -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 -{ - /// - /// Used to build a . Represents an individual URL-generating route that will be - /// aggregated into the . - /// - public class TreeRouteLinkGenerationEntry - { - /// - /// The . - /// - public TemplateBinder Binder { get; set; } - - /// - /// The route constraints. - /// - public IReadOnlyDictionary Constraints { get; set; } - - /// - /// The route defaults. - /// - public IReadOnlyDictionary Defaults { get; set; } - - /// - /// The order of the template. - /// - public int Order { get; set; } - - /// - /// The precedence of the template for link generation. Greater number means higher precedence. - /// - public decimal GenerationPrecedence { get; set; } - - /// - /// The name of the route. - /// - public string Name { get; set; } - - /// - /// The route group. - /// - public string RouteGroup { get; set; } - - /// - /// The set of values that must be present for link genration. - /// - public IDictionary RequiredLinkValues { get; set; } - - /// - /// The . - /// - public RouteTemplate Template { get; set; } - - /// - /// The original representing the route template. - /// - public string TemplateText { get; set; } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/Routing/Tree/TreeRouteMatchingEntry.cs b/src/Microsoft.AspNet.Mvc.Core/Routing/Tree/TreeRouteMatchingEntry.cs deleted file mode 100644 index 5b82149a8b..0000000000 --- a/src/Microsoft.AspNet.Mvc.Core/Routing/Tree/TreeRouteMatchingEntry.cs +++ /dev/null @@ -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 -{ - /// - /// Used to build an . Represents an individual URL-matching route that will be - /// aggregated into the . - /// - public class TreeRouteMatchingEntry - { - /// - /// The order of the template. - /// - public int Order { get; set; } - - /// - /// The precedence of the template. - /// - 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 Constraints { get; set; } - } -} diff --git a/src/Microsoft.AspNet.Mvc.Core/Routing/Tree/TreeRouter.cs b/src/Microsoft.AspNet.Mvc.Core/Routing/Tree/TreeRouter.cs deleted file mode 100644 index d46b82ed25..0000000000 --- a/src/Microsoft.AspNet.Mvc.Core/Routing/Tree/TreeRouter.cs +++ /dev/null @@ -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 -{ - /// - /// An implementation for attribute routing. - /// - public class TreeRouter : IRouter - { - private readonly IRouter _next; - private readonly LinkGenerationDecisionTree _linkGenerationTree; - private readonly UrlMatchingTree[] _trees; - private readonly IDictionary _namedEntries; - - private readonly ILogger _logger; - private readonly ILogger _constraintLogger; - - /// - /// Creates a new . - /// - /// The next router. Invoked when a route entry matches. - /// The list of that contains the route entries. - /// The set of . - /// The instance. - /// The instance used - /// in . - /// The version of this route. - public TreeRouter( - IRouter next, - UrlMatchingTree[] trees, - IEnumerable 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( - 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; - } - - /// - /// Gets the version of this route. This corresponds to the value of - /// when this route was created. - /// - public int Version { get; } - - /// - 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; - } - - /// - 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 - { - private readonly Stack _stack; - private readonly PathTokenizer _tokenizer; - - private int _segmentIndex; - - public TreeEnumerator(UrlMatchingNode root, PathTokenizer tokenizer) - { - _stack = new Stack(); - _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 destination, - IDictionary 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 - { - public TemplateMatch(TreeRouteMatchingEntry entry, IDictionary values) - { - Entry = entry; - Values = values; - } - - public TreeRouteMatchingEntry Entry { get; } - - public IDictionary 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(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( - 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); - } - } -} diff --git a/src/Microsoft.AspNet.Mvc.Core/Routing/Tree/UrlMatchingNode.cs b/src/Microsoft.AspNet.Mvc.Core/Routing/Tree/UrlMatchingNode.cs deleted file mode 100644 index e611df51a6..0000000000 --- a/src/Microsoft.AspNet.Mvc.Core/Routing/Tree/UrlMatchingNode.cs +++ /dev/null @@ -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(); - Literals = new Dictionary(StringComparer.OrdinalIgnoreCase); - } - - public int Length { get; } - - // These entries are sorted by precedence then template - public List Matches { get; } - - public Dictionary Literals { get; } - - public UrlMatchingNode ConstrainedParameters { get; set; } - - public UrlMatchingNode Parameters { get; set; } - - public UrlMatchingNode ConstrainedCatchAlls { get; set; } - - public UrlMatchingNode CatchAlls { get; set; } - } -} diff --git a/src/Microsoft.AspNet.Mvc.Core/Routing/Tree/UrlMatchingTree.cs b/src/Microsoft.AspNet.Mvc.Core/Routing/Tree/UrlMatchingTree.cs deleted file mode 100644 index 70c054201f..0000000000 --- a/src/Microsoft.AspNet.Mvc.Core/Routing/Tree/UrlMatchingTree.cs +++ /dev/null @@ -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); - } -} diff --git a/src/Microsoft.AspNet.Mvc.Core/project.json b/src/Microsoft.AspNet.Mvc.Core/project.json index 34b275be7c..613308f37b 100644 --- a/src/Microsoft.AspNet.Mvc.Core/project.json +++ b/src/Microsoft.AspNet.Mvc.Core/project.json @@ -14,15 +14,15 @@ "Microsoft.AspNet.FileProviders.Abstractions": "1.0.0-*", "Microsoft.AspNet.Hosting.Abstractions": "1.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.ClosedGenericMatcher.Sources": { "version": "1.0.0-*", "type": "build" }, - "Microsoft.Extensions.HashCodeCombiner.Sources": { - "type": "build", - "version": "1.0.0-*" - }, "Microsoft.Extensions.Logging.Abstractions": "1.0.0-*", "Microsoft.Extensions.MemoryPool": "1.0.0-*", "Microsoft.Extensions.PropertyActivator.Sources": { diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Controllers/ControllerActionDescriptorProviderTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Controllers/ControllerActionDescriptorProviderTests.cs index 23e45b12de..d2ef94671f 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/Controllers/ControllerActionDescriptorProviderTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Controllers/ControllerActionDescriptorProviderTests.cs @@ -13,6 +13,7 @@ using Microsoft.AspNet.Mvc.Filters; using Microsoft.AspNet.Mvc.Infrastructure; using Microsoft.AspNet.Mvc.ModelBinding; using Microsoft.AspNet.Mvc.Routing; +using Microsoft.AspNet.Routing.Tree; using Moq; using Xunit; @@ -250,7 +251,7 @@ namespace Microsoft.AspNet.Mvc.Controllers var routeconstraint = Assert.Single(action.RouteConstraints); Assert.Equal(RouteKeyHandling.RequireKey, routeconstraint.KeyHandling); - Assert.Equal(AttributeRouting.RouteGroupKey, routeconstraint.RouteKey); + Assert.Equal(TreeRouter.RouteGroupKey, routeconstraint.RouteKey); var controller = Assert.Single(action.RouteValueDefaults, rc => rc.Key.Equals("controller")); @@ -954,7 +955,7 @@ namespace Microsoft.AspNet.Mvc.Controllers { Assert.Equal(6, actionDescriptor.RouteConstraints.Count); var routeGroupConstraint = Assert.Single(actionDescriptor.RouteConstraints, - rc => rc.RouteKey.Equals(AttributeRouting.RouteGroupKey)); + rc => rc.RouteKey.Equals(TreeRouter.RouteGroupKey)); Assert.Equal(RouteKeyHandling.DenyKey, routeGroupConstraint.KeyHandling); } } @@ -978,7 +979,7 @@ namespace Microsoft.AspNet.Mvc.Controllers 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.NotNull(routeGroupConstraint.RouteValue); @@ -1027,7 +1028,7 @@ namespace Microsoft.AspNet.Mvc.Controllers var groupIds = actions.Select( a => a.RouteConstraints - .Where(rc => rc.RouteKey == AttributeRouting.RouteGroupKey) + .Where(rc => rc.RouteKey == TreeRouter.RouteGroupKey) .Select(rc => rc.RouteValue) .Single()) .ToArray(); diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Infrastructure/MvcRouteHandlerTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Infrastructure/MvcRouteHandlerTests.cs index 21e730a109..647dd020e6 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/Infrastructure/MvcRouteHandlerTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Infrastructure/MvcRouteHandlerTests.cs @@ -7,8 +7,8 @@ using System.Threading.Tasks; using Microsoft.AspNet.Http; using Microsoft.AspNet.Mvc.Abstractions; using Microsoft.AspNet.Mvc.Internal; -using Microsoft.AspNet.Mvc.Routing; using Microsoft.AspNet.Routing; +using Microsoft.AspNet.Routing.Tree; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Testing; using Microsoft.Extensions.OptionsModel; @@ -141,7 +141,7 @@ namespace Microsoft.AspNet.Mvc.Infrastructure var handler = new MvcRouteHandler(); var originalRouteData = context.RouteData; - originalRouteData.Values.Add(AttributeRouting.RouteGroupKey, "/Home/Test"); + originalRouteData.Values.Add(TreeRouter.RouteGroupKey, "/Home/Test"); // Act await handler.RouteAsync(context); @@ -152,7 +152,7 @@ namespace Microsoft.AspNet.Mvc.Infrastructure Assert.Same(actionRouteData, context.RouteData); // The new routedata is a copy - Assert.False(context.RouteData.Values.ContainsKey(AttributeRouting.RouteGroupKey)); + Assert.False(context.RouteData.Values.ContainsKey(TreeRouter.RouteGroupKey)); } [Fact] diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Internal/DecisionTree/DecisionTreeBuilderTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Internal/DecisionTree/DecisionTreeBuilderTest.cs deleted file mode 100644 index 1cf7b5f2da..0000000000 --- a/test/Microsoft.AspNet.Mvc.Core.Test/Internal/DecisionTree/DecisionTreeBuilderTest.cs +++ /dev/null @@ -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(); - - // Act - var tree = DecisionTreeBuilder.GenerateTree(items, new ItemClassifier()); - - // Assert - Assert.Empty(tree.Criteria); - Assert.Empty(tree.Matches); - } - - [Fact] - public void BuildTree_TrivialMatch() - { - // Arrange - var items = new List(); - - var item = new Item(); - items.Add(item); - - // Act - var tree = DecisionTreeBuilder.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(); - - 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.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(); - - 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.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(); - - 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.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(); - - 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.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(); - - 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.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(StringComparer.OrdinalIgnoreCase); - } - - public Dictionary Criteria { get; private set; } - } - - private class ItemClassifier : IClassifier - { - public IEqualityComparer ValueComparer - { - get - { - return new RouteValueEqualityComparer(); - } - } - - public IDictionary GetCriteria(Item item) - { - return item.Criteria; - } - } - } -} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Internal/Routing/LinkGenerationDecisionTreeTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Internal/Routing/LinkGenerationDecisionTreeTest.cs deleted file mode 100644 index 29e31e2573..0000000000 --- a/test/Microsoft.AspNet.Mvc.Core.Test/Internal/Routing/LinkGenerationDecisionTreeTest.cs +++ /dev/null @@ -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(); - - 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(); - - 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(); - - 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(); - - 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(); - - 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(); - - 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(); - - 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(); - - 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(); - - 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(); - - 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(); - - 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(); - - 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(); - - 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; - } - } -} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Routing/AttributeRoutePrecedenceTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Routing/AttributeRoutePrecedenceTests.cs deleted file mode 100644 index 047b03f170..0000000000 --- a/test/Microsoft.AspNet.Mvc.Core.Test/Routing/AttributeRoutePrecedenceTests.cs +++ /dev/null @@ -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 func) - { - var options = new Mock>(); - options.SetupGet(o => o.Value).Returns(new RouteOptions()); - - var parsed = TemplateParser.Parse(template); - return func(parsed); - } - } -} -#endif diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Routing/AttributeRouteTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Routing/AttributeRouteTest.cs index ff138e6683..d773225009 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/Routing/AttributeRouteTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Routing/AttributeRouteTest.cs @@ -10,6 +10,7 @@ using Microsoft.AspNet.Http.Internal; using Microsoft.AspNet.Mvc.Abstractions; using Microsoft.AspNet.Mvc.Infrastructure; using Microsoft.AspNet.Routing; +using Microsoft.AspNet.Routing.Tree; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Testing; using Moq; @@ -43,7 +44,7 @@ namespace Microsoft.AspNet.Mvc.Routing }, RouteConstraints = new List() { - new RouteDataActionConstraint(AttributeRouting.RouteGroupKey, "1"), + new RouteDataActionConstraint(TreeRouter.RouteGroupKey, "1"), }, }, new ActionDescriptor() @@ -54,7 +55,7 @@ namespace Microsoft.AspNet.Mvc.Routing }, RouteConstraints = new List() { - new RouteDataActionConstraint(AttributeRouting.RouteGroupKey, "2"), + new RouteDataActionConstraint(TreeRouter.RouteGroupKey, "2"), }, }, }; @@ -87,7 +88,7 @@ namespace Microsoft.AspNet.Mvc.Routing // Assert 1 Assert.True(context.IsHandled); 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()), Times.Once()); diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Routing/AttributeRoutingTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Routing/AttributeRoutingTest.cs index edc571a9f2..39e2e43e59 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/Routing/AttributeRoutingTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Routing/AttributeRoutingTest.cs @@ -10,6 +10,7 @@ using Microsoft.AspNet.Mvc.Abstractions; using Microsoft.AspNet.Mvc.Controllers; using Microsoft.AspNet.Mvc.Infrastructure; using Microsoft.AspNet.Routing; +using Microsoft.AspNet.Routing.Tree; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Testing; using Microsoft.Extensions.OptionsModel; @@ -125,7 +126,7 @@ namespace Microsoft.AspNet.Mvc.Routing action.MethodInfo = actionMethod; action.RouteConstraints = new List() { - new RouteDataActionConstraint(AttributeRouting.RouteGroupKey, "group"), + new RouteDataActionConstraint(TreeRouter.RouteGroupKey, "group"), }; action.AttributeRouteInfo = new AttributeRouteInfo(); action.AttributeRouteInfo.Template = "{controller}/{action}"; @@ -161,7 +162,7 @@ namespace Microsoft.AspNet.Mvc.Routing DisplayName = displayName, RouteConstraints = new List() { - new RouteDataActionConstraint(AttributeRouting.RouteGroupKey, "whatever"), + new RouteDataActionConstraint(TreeRouter.RouteGroupKey, "whatever"), }, AttributeRouteInfo = new AttributeRouteInfo { Template = template }, }; diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Routing/TreeRouterTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Routing/TreeRouterTest.cs deleted file mode 100644 index 8cc7c6977c..0000000000 --- a/test/Microsoft.AspNet.Mvc.Core.Test/Routing/TreeRouterTest.cs +++ /dev/null @@ -1,2056 +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; -using System.Threading.Tasks; -using Microsoft.AspNet.Http; -using Microsoft.AspNet.Routing; -using Microsoft.AspNet.Routing.Template; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Testing; -using Microsoft.Extensions.OptionsModel; -using Moq; -using Xunit; - -namespace Microsoft.AspNet.Mvc.Routing -{ - public class TreeRouterTest - { - [Theory] - [InlineData("template/5", "template/{parameter:int}")] - [InlineData("template/5", "template/{parameter}")] - [InlineData("template/5", "template/{*parameter:int}")] - [InlineData("template/5", "template/{*parameter}")] - [InlineData("template/{parameter:int}", "template/{parameter}")] - [InlineData("template/{parameter:int}", "template/{*parameter:int}")] - [InlineData("template/{parameter:int}", "template/{*parameter}")] - [InlineData("template/{parameter}", "template/{*parameter:int}")] - [InlineData("template/{parameter}", "template/{*parameter}")] - [InlineData("template/{*parameter:int}", "template/{*parameter}")] - public async Task AttributeRoute_RouteAsync_RespectsPrecedence( - string firstTemplate, - string secondTemplate) - { - // Arrange - var expectedRouteGroup = string.Format("{0}&&{1}", 0, firstTemplate); - - // We need to force the creation of a closure in order to avoid an issue with Moq and Roslyn. - var numberOfCalls = 0; - Action callBack = ctx => { ctx.IsHandled = true; numberOfCalls++; }; - - var next = new Mock(); - next.Setup(r => r.RouteAsync(It.IsAny())) - .Callback(callBack) - .Returns(Task.FromResult(true)) - .Verifiable(); - - var firstRoute = CreateMatchingEntry(next.Object, firstTemplate, order: 0); - var secondRoute = CreateMatchingEntry(next.Object, secondTemplate, order: 0); - - // We setup the route entries in reverse order of precedence to ensure that when we - // try to route the request, the route with a higher precedence gets tried first. - var matchingRoutes = new[] { secondRoute, firstRoute }; - - var linkGenerationEntries = Enumerable.Empty(); - - var route = CreateAttributeRoute(next.Object, matchingRoutes, linkGenerationEntries); - - var context = CreateRouteContext("/template/5"); - - // Act - await route.RouteAsync(context); - - // Assert - Assert.Equal(expectedRouteGroup, context.RouteData.Values["test_route_group"]); - } - - [Theory] - [InlineData("template/5", "template/{parameter:int}")] - [InlineData("template/5", "template/{parameter}")] - [InlineData("template/5", "template/{*parameter:int}")] - [InlineData("template/5", "template/{*parameter}")] - [InlineData("template/{parameter:int}", "template/{parameter}")] - [InlineData("template/{parameter:int}", "template/{*parameter:int}")] - [InlineData("template/{parameter:int}", "template/{*parameter}")] - [InlineData("template/{parameter}", "template/{*parameter:int}")] - [InlineData("template/{parameter}", "template/{*parameter}")] - [InlineData("template/{*parameter:int}", "template/{*parameter}")] - public async Task AttributeRoute_RouteAsync_RespectsOrderOverPrecedence( - string firstTemplate, - string secondTemplate) - { - // Arrange - var expectedRouteGroup = string.Format("{0}&&{1}", 0, secondTemplate); - - // We need to force the creation of a closure in order to avoid an issue with Moq and Roslyn. - var numberOfCalls = 0; - Action callBack = ctx => { ctx.IsHandled = true; numberOfCalls++; }; - - var next = new Mock(); - next.Setup(r => r.RouteAsync(It.IsAny())) - .Callback(callBack) - .Returns(Task.FromResult(true)) - .Verifiable(); - - var firstRoute = CreateMatchingEntry(next.Object, firstTemplate, order: 1); - var secondRoute = CreateMatchingEntry(next.Object, secondTemplate, order: 0); - - // We setup the route entries with a lower relative order and higher relative precedence - // first to ensure that when we try to route the request, the route with the higher - // relative order gets tried first. - var matchingRoutes = new[] { firstRoute, secondRoute }; - - var linkGenerationEntries = Enumerable.Empty(); - - var route = CreateAttributeRoute(next.Object, matchingRoutes, linkGenerationEntries); - - var context = CreateRouteContext("/template/5"); - - // Act - await route.RouteAsync(context); - - // Assert - Assert.Equal(expectedRouteGroup, context.RouteData.Values["test_route_group"]); - } - - [Theory] - [InlineData("template/5")] - [InlineData("template/{parameter:int}")] - [InlineData("template/{parameter}")] - [InlineData("template/{*parameter:int}")] - [InlineData("template/{*parameter}")] - public async Task AttributeRoute_RouteAsync_RespectsOrder(string template) - { - // Arrange - var expectedRouteGroup = string.Format("{0}&&{1}", 0, template); - - // We need to force the creation of a closure in order to avoid an issue with Moq and Roslyn. - var numberOfCalls = 0; - Action callBack = ctx => { ctx.IsHandled = true; numberOfCalls++; }; - - var next = new Mock(); - next.Setup(r => r.RouteAsync(It.IsAny())) - .Callback(callBack) - .Returns(Task.FromResult(true)) - .Verifiable(); - - var firstRoute = CreateMatchingEntry(next.Object, template, order: 1); - var secondRoute = CreateMatchingEntry(next.Object, template, order: 0); - - // We setup the route entries with a lower relative order first to ensure that when - // we try to route the request, the route with the higher relative order gets tried first. - var matchingRoutes = new[] { firstRoute, secondRoute }; - - var linkGenerationEntries = Enumerable.Empty(); - - var route = CreateAttributeRoute(next.Object, matchingRoutes, linkGenerationEntries); - - var context = CreateRouteContext("/template/5"); - - // Act - await route.RouteAsync(context); - - // Assert - Assert.Equal(expectedRouteGroup, context.RouteData.Values["test_route_group"]); - } - - [Theory] - [InlineData("template/{first:int}", "template/{second:int}")] - [InlineData("template/{first}", "template/{second}")] - [InlineData("template/{*first:int}", "template/{*second:int}")] - [InlineData("template/{*first}", "template/{*second}")] - public async Task AttributeRoute_RouteAsync_EnsuresStableOrdering(string first, string second) - { - // Arrange - var expectedRouteGroup = string.Format("{0}&&{1}", 0, first); - - // We need to force the creation of a closure in order to avoid an issue with Moq and Roslyn. - var numberOfCalls = 0; - Action callBack = ctx => { ctx.IsHandled = true; numberOfCalls++; }; - - var next = new Mock(); - next.Setup(r => r.RouteAsync(It.IsAny())) - .Callback(callBack) - .Returns(Task.FromResult(true)) - .Verifiable(); - - var secondRouter = new Mock(MockBehavior.Strict); - - var firstRoute = CreateMatchingEntry(next.Object, first, order: 0); - var secondRoute = CreateMatchingEntry(next.Object, second, order: 0); - - // We setup the route entries with a lower relative template order first to ensure that when - // we try to route the request, the route with the higher template order gets tried first. - var matchingRoutes = new[] { secondRoute, firstRoute }; - - var linkGenerationEntries = Enumerable.Empty(); - - var route = CreateAttributeRoute(next.Object, matchingRoutes, linkGenerationEntries); - - var context = CreateRouteContext("/template/5"); - - // Act - await route.RouteAsync(context); - - // Assert - Assert.Equal(expectedRouteGroup, context.RouteData.Values["test_route_group"]); - } - - [Theory] - [InlineData("template/{parameter:int}", "/template/5", true)] - [InlineData("template/{parameter:int?}", "/template/5", true)] - [InlineData("template/{parameter:int?}", "/template", true)] - [InlineData("template/{parameter:int?}", "/template/qwer", false)] - public async Task AttributeRoute_WithOptionalInlineConstraint( - string template, string request, bool expectedResult) - { - // Arrange - var expectedRouteGroup = string.Format("{0}&&{1}", 0, template); - - // We need to force the creation of a closure in order to avoid an issue with Moq and Roslyn. - var numberOfCalls = 0; - Action callBack = ctx => { ctx.IsHandled = true; numberOfCalls++; }; - - var next = new Mock(); - next.Setup(r => r.RouteAsync(It.IsAny())) - .Callback(callBack) - .Returns(Task.FromResult(true)) - .Verifiable(); - - var firstRoute = CreateMatchingEntry(next.Object, template, order: 0); - - // We setup the route entries in reverse order of precedence to ensure that when we - // try to route the request, the route with a higher precedence gets tried first. - var matchingRoutes = new[] { firstRoute }; - - var linkGenerationEntries = Enumerable.Empty(); - - var route = CreateAttributeRoute(next.Object, matchingRoutes, linkGenerationEntries); - - var context = CreateRouteContext(request); - - // Act - await route.RouteAsync(context); - - // Assert - if (expectedResult) - { - Assert.True(context.IsHandled); - Assert.Equal(expectedRouteGroup, context.RouteData.Values["test_route_group"]); - } - else - { - Assert.False(context.IsHandled); - } - } - - [Theory] - [InlineData("moo/{p1}.{p2?}", "/moo/foo.bar", "foo", "bar", null)] - [InlineData("moo/{p1?}", "/moo/foo", "foo", null, null)] - [InlineData("moo/{p1?}", "/moo", null, null, null)] - [InlineData("moo/{p1}.{p2?}", "/moo/foo", "foo", null, null)] - [InlineData("moo/{p1}.{p2?}", "/moo/foo..bar", "foo.", "bar", null)] - [InlineData("moo/{p1}.{p2?}", "/moo/foo.moo.bar", "foo.moo", "bar", null)] - [InlineData("moo/{p1}.{p2}", "/moo/foo.bar", "foo", "bar", null)] - [InlineData("moo/foo.{p1}.{p2?}", "/moo/foo.moo.bar", "moo", "bar", null)] - [InlineData("moo/foo.{p1}.{p2?}", "/moo/foo.moo", "moo", null, null)] - [InlineData("moo/.{p2?}", "/moo/.foo", null, "foo", null)] - [InlineData("moo/{p1}.{p2?}", "/moo/....", "..", ".", null)] - [InlineData("moo/{p1}.{p2?}", "/moo/.bar", ".bar", null, null)] - [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo.moo.bar", "foo", "moo", "bar")] - [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo.moo", "foo", "moo", null)] - [InlineData("moo/{p1}.{p2}.{p3}.{p4?}", "/moo/foo.moo.bar", "foo", "moo", "bar")] - [InlineData("{p1}.{p2?}/{p3}", "/foo.moo/bar", "foo", "moo", "bar")] - [InlineData("{p1}.{p2?}/{p3}", "/foo/bar", "foo", null, "bar")] - [InlineData("{p1}.{p2?}/{p3}", "/.foo/bar", ".foo", null, "bar")] - public async Task AttributeRoute_WithOptionalCompositeParameter_Valid( - string template, - string request, - string p1, - string p2, - string p3) - { - // Arrange - var expectedRouteGroup = string.Format("{0}&&{1}", 0, template); - - // We need to force the creation of a closure in order to avoid an issue with Moq and Roslyn. - var numberOfCalls = 0; - Action callBack = ctx => { ctx.IsHandled = true; numberOfCalls++; }; - - var next = new Mock(); - next.Setup(r => r.RouteAsync(It.IsAny())) - .Callback(callBack) - .Returns(Task.FromResult(true)) - .Verifiable(); - - var firstRoute = CreateMatchingEntry(next.Object, template, order: 0); - - // We setup the route entries in reverse order of precedence to ensure that when we - // try to route the request, the route with a higher precedence gets tried first. - var matchingEntries = new[] { firstRoute }; - var linkGenerationEntries = Enumerable.Empty(); - var route = CreateAttributeRoute(next.Object, matchingEntries, linkGenerationEntries); - var context = CreateRouteContext(request); - - // Act - await route.RouteAsync(context); - - // Assert - Assert.True(context.IsHandled); - if (p1 != null) - { - Assert.Equal(p1, context.RouteData.Values["p1"]); - } - if (p2 != null) - { - Assert.Equal(p2, context.RouteData.Values["p2"]); - } - if (p3 != null) - { - Assert.Equal(p3, context.RouteData.Values["p3"]); - } - } - - [Theory] - [InlineData("moo/{p1}.{p2?}", "/moo/foo.")] - [InlineData("moo/{p1}.{p2?}", "/moo/.")] - [InlineData("moo/{p1}.{p2}", "/foo.")] - [InlineData("moo/{p1}.{p2}", "/foo")] - [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo.moo.")] - [InlineData("moo/foo.{p2}.{p3?}", "/moo/bar.foo.moo")] - [InlineData("moo/foo.{p2}.{p3?}", "/moo/kungfoo.moo.bar")] - [InlineData("moo/foo.{p2}.{p3?}", "/moo/kungfoo.moo")] - [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo")] - [InlineData("{p1}.{p2?}/{p3}", "/foo./bar")] - [InlineData("moo/.{p2?}", "/moo/.")] - [InlineData("{p1}.{p2}/{p3}", "/.foo/bar")] - public async Task AttributeRoute_WithOptionalCompositeParameter_Invalid( - string template, - string request) - { - // Arrange - var expectedRouteGroup = string.Format("{0}&&{1}", 0, template); - - // We need to force the creation of a closure in order to avoid an issue with Moq and Roslyn. - var numberOfCalls = 0; - Action callBack = ctx => { ctx.IsHandled = true; numberOfCalls++; }; - - var next = new Mock(); - next.Setup(r => r.RouteAsync(It.IsAny())) - .Callback(callBack) - .Returns(Task.FromResult(true)) - .Verifiable(); - - var firstRoute = CreateMatchingEntry(next.Object, template, order: 0); - - // We setup the route entries in reverse order of precedence to ensure that when we - // try to route the request, the route with a higher precedence gets tried first. - var matchingEntries = new[] { firstRoute }; - var linkGenerationEntries = Enumerable.Empty(); - var route = CreateAttributeRoute(next.Object, matchingEntries, linkGenerationEntries); - - var context = CreateRouteContext(request); - - // Act - await route.RouteAsync(context); - - // Assert - Assert.False(context.IsHandled); - } - - [Theory] - [InlineData("template", "{*url:alpha}", "/template?url=dingo&id=5")] - [InlineData("{*url:alpha}", "{*url}", "/dingo?id=5")] - [InlineData("{id}", "{*url}", "/5?url=dingo")] - [InlineData("{id}", "{*url:alpha}", "/5?url=dingo")] - [InlineData("{id:int}", "{id}", "/5?url=dingo")] - [InlineData("template/api/{*url}", "template/api", "/template/api/dingo?id=5")] - [InlineData("template/api", "template/{*url}", "/template/api?url=dingo&id=5")] - [InlineData("template/api", "template/api{id}location", "/template/api?url=dingo&id=5")] - [InlineData("template/api{id}location", "template/{id:int}", "/template/api5location?url=dingo")] - public void AttributeRoute_GenerateLink(string firstTemplate, string secondTemplate, string expectedPath) - { - // Arrange - var expectedGroup = CreateRouteGroup(0, firstTemplate); - - string selectedGroup = null; - Action callback = ctx => - { - selectedGroup = (string)ctx.ProvidedValues[AttributeRouting.RouteGroupKey]; - ctx.IsBound = true; - }; - - var values = new Dictionary - { - {"url", "dingo" }, - {"id", 5 } - }; - - var route = CreateAttributeRoute(callback, firstTemplate, secondTemplate); - var context = CreateVirtualPathContext( - values: values, - ambientValues: null); - - // Act - var result = route.GetVirtualPath(context); - - // Assert - Assert.NotNull(result); - Assert.Equal(new PathString(expectedPath), result.VirtualPath); - Assert.Same(route, result.Router); - Assert.Empty(result.DataTokens); - Assert.Equal(expectedGroup, selectedGroup); - } - - [Fact] - public void AttributeRoute_GenerateLink_LongerTemplateWithDefaultIsMoreSpecific() - { - // Arrange - var firstTemplate = "template"; - var secondTemplate = "template/{parameter:int=1003}"; - - var expectedGroup = CreateRouteGroup(0, secondTemplate); - - string selectedGroup = null; - Action callback = ctx => - { - selectedGroup = (string)ctx.ProvidedValues[AttributeRouting.RouteGroupKey]; - ctx.IsBound = true; - }; - - var route = CreateAttributeRoute(callback, firstTemplate, secondTemplate); - var context = CreateVirtualPathContext( - values: null, - ambientValues: null); - - // Act - var result = route.GetVirtualPath(context); - - // Assert - Assert.NotNull(result); - // The Binder binds to /template - Assert.Equal(new PathString($"/template"), result.VirtualPath); - Assert.Same(route, result.Router); - Assert.Empty(result.DataTokens); - // Even though the path was /template, the group generated from was /template/{paramter:int=1003} - Assert.Equal(expectedGroup, selectedGroup); - } - - [Theory] - [InlineData("template/{parameter:int=5}", "template", "/template/5")] - [InlineData("template/{parameter}", "template", "/template/5")] - [InlineData("template/{parameter}/{id}", "template/{parameter}", "/template/5/1234")] - public void AttributeRoute_GenerateLink_OrderingAgnostic( - string firstTemplate, - string secondTemplate, - string expectedPath) - { - var expectedGroup = CreateRouteGroup(0, firstTemplate); - - string selectedGroup = null; - Action callback = ctx => - { - selectedGroup = (string)ctx.ProvidedValues[AttributeRouting.RouteGroupKey]; - ctx.IsBound = true; - }; - - var route = CreateAttributeRoute(callback, firstTemplate, secondTemplate); - var parameter = 5; - var id = 1234; - var values = new Dictionary - { - { nameof(parameter) , parameter}, - { nameof(id), id } - }; - var context = CreateVirtualPathContext( - values: null, - ambientValues: values); - - // Act - var result = route.GetVirtualPath(context); - - // Assert - Assert.NotNull(result); - Assert.Equal(new PathString(expectedPath), result.VirtualPath); - Assert.Same(route, result.Router); - Assert.Empty(result.DataTokens); - Assert.Equal(expectedGroup, selectedGroup); - } - - [Theory] - [InlineData("template", "template/{parameter}", "/template/5")] - [InlineData("template/{parameter}", "template/{parameter}/{id}", "/template/5/1234")] - [InlineData("template", "template/{parameter:int=5}", "/template/5")] - public void AttributeRoute_GenerateLink_UseAvailableVariables( - string firstTemplate, - string secondTemplate, - string expectedPath) - { - // Arrange - var expectedGroup = CreateRouteGroup(0, secondTemplate); - - string selectedGroup = null; - Action callback = ctx => - { - selectedGroup = (string)ctx.ProvidedValues[AttributeRouting.RouteGroupKey]; - ctx.IsBound = true; - }; - - var route = CreateAttributeRoute(callback, firstTemplate, secondTemplate); - var parameter = 5; - var id = 1234; - var values = new Dictionary - { - { nameof(parameter) , parameter}, - { nameof(id), id } - }; - var context = CreateVirtualPathContext( - values: null, - ambientValues: values); - - // Act - var result = route.GetVirtualPath(context); - - // Assert - Assert.NotNull(result); - Assert.Equal(new PathString(expectedPath), result.VirtualPath); - Assert.Same(route, result.Router); - Assert.Empty(result.DataTokens); - Assert.Equal(expectedGroup, selectedGroup); - } - - [Theory] - [InlineData("template/5", "template/{parameter:int}")] - [InlineData("template/5", "template/{parameter}")] - [InlineData("template/5", "template/{*parameter:int}")] - [InlineData("template/5", "template/{*parameter}")] - [InlineData("template/{parameter:int}", "template/{parameter}")] - [InlineData("template/{parameter:int}", "template/{*parameter:int}")] - [InlineData("template/{parameter:int}", "template/{*parameter}")] - [InlineData("template/{parameter}", "template/{*parameter:int}")] - [InlineData("template/{parameter}", "template/{*parameter}")] - [InlineData("template/{*parameter:int}", "template/{*parameter}")] - public void AttributeRoute_GenerateLink_RespectsPrecedence(string firstTemplate, string secondTemplate) - { - // Arrange - var expectedGroup = CreateRouteGroup(0, firstTemplate); - - string selectedGroup = null; - - var next = new Mock(); - next.Setup(n => n.GetVirtualPath(It.IsAny())).Callback(ctx => - { - selectedGroup = (string)ctx.ProvidedValues[AttributeRouting.RouteGroupKey]; - ctx.IsBound = true; - }) - .Returns((VirtualPathData)null); - - var matchingRoutes = Enumerable.Empty(); - - var firstEntry = CreateGenerationEntry(firstTemplate, requiredValues: null); - var secondEntry = CreateGenerationEntry(secondTemplate, requiredValues: null, order: 0); - - // We setup the route entries in reverse order of precedence to ensure that when we - // try to generate a link, the route with a higher precedence gets tried first. - var linkGenerationEntries = new[] { secondEntry, firstEntry }; - - var route = CreateAttributeRoute(next.Object, matchingRoutes, linkGenerationEntries); - - var context = CreateVirtualPathContext(values: null, ambientValues: new { parameter = 5 }); - - // Act - var result = route.GetVirtualPath(context); - - // Assert - Assert.NotNull(result); - Assert.Equal(new PathString("/template/5"), result.VirtualPath); - Assert.Same(route, result.Router); - Assert.Empty(result.DataTokens); - - Assert.Equal(expectedGroup, selectedGroup); - } - - [Theory] - [InlineData("template/{parameter:int}", "/template/5", 5)] - [InlineData("template/{parameter:int?}", "/template/5", 5)] - [InlineData("template/{parameter:int?}", "/template", null)] - [InlineData("template/{parameter:int?}", null, "asdf")] - [InlineData("template/{parameter:alpha?}", "/template/asdf", "asdf")] - [InlineData("template/{parameter:alpha?}", "/template", null)] - [InlineData("template/{parameter:int:range(1,20)?}", "/template", null)] - [InlineData("template/{parameter:int:range(1,20)?}", "/template/5", 5)] - [InlineData("template/{parameter:int:range(1,20)?}", null, 21)] - public void AttributeRoute_GenerateLink_OptionalInlineParameter - (string template, string expectedPath, object parameter) - { - // Arrange - var expectedGroup = CreateRouteGroup(0, template); - - string selectedGroup = null; - - var next = new Mock(); - next.Setup(n => n.GetVirtualPath(It.IsAny())).Callback(ctx => - { - selectedGroup = (string)ctx.ProvidedValues[AttributeRouting.RouteGroupKey]; - ctx.IsBound = true; - }) - .Returns((VirtualPathData)null); - - var matchingRoutes = Enumerable.Empty(); - - var entry = CreateGenerationEntry(template, requiredValues: null); - - var linkGenerationEntries = new[] { entry }; - - var route = CreateAttributeRoute(next.Object, matchingRoutes, linkGenerationEntries); - - VirtualPathContext context; - if (parameter != null) - { - context = CreateVirtualPathContext(values: null, ambientValues: new { parameter = parameter }); - } - else - { - context = CreateVirtualPathContext(values: null, ambientValues: null); - } - - // Act - var result = route.GetVirtualPath(context); - - // Assert - if (expectedPath == null) - { - Assert.Null(result); - } - else - { - Assert.NotNull(result); - Assert.Equal(new PathString(expectedPath), result.VirtualPath); - Assert.Same(route, result.Router); - Assert.Empty(result.DataTokens); - } - } - - [Theory] - [InlineData("template/5", "template/{parameter:int}")] - [InlineData("template/5", "template/{parameter}")] - [InlineData("template/5", "template/{*parameter:int}")] - [InlineData("template/5", "template/{*parameter}")] - [InlineData("template/{parameter:int}", "template/{parameter}")] - [InlineData("template/{parameter:int}", "template/{*parameter:int}")] - [InlineData("template/{parameter:int}", "template/{*parameter}")] - [InlineData("template/{parameter}", "template/{*parameter:int}")] - [InlineData("template/{parameter}", "template/{*parameter}")] - [InlineData("template/{*parameter:int}", "template/{*parameter}")] - public void AttributeRoute_GenerateLink_RespectsOrderOverPrecedence(string firstTemplate, string secondTemplate) - { - // Arrange - var selectedGroup = CreateRouteGroup(0, secondTemplate); - - string firstRouteGroupSelected = null; - var next = new Mock(); - next.Setup(n => n.GetVirtualPath(It.IsAny())).Callback(ctx => - { - firstRouteGroupSelected = (string)ctx.ProvidedValues[AttributeRouting.RouteGroupKey]; - ctx.IsBound = true; - }) - .Returns((VirtualPathData)null); - - var matchingRoutes = Enumerable.Empty(); - - var firstRoute = CreateGenerationEntry(firstTemplate, requiredValues: null, order: 1); - var secondRoute = CreateGenerationEntry(secondTemplate, requiredValues: null, order: 0); - - // We setup the route entries with a lower relative order and higher relative precedence - // first to ensure that when we try to generate a link, the route with the higher - // relative order gets tried first. - var linkGenerationEntries = new[] { firstRoute, secondRoute }; - - var route = CreateAttributeRoute(next.Object, matchingRoutes, linkGenerationEntries); - - var context = CreateVirtualPathContext(null, ambientValues: new { parameter = 5 }); - - // Act - var result = route.GetVirtualPath(context); - - // Assert - Assert.NotNull(result); - Assert.Equal(new PathString("/template/5"), result.VirtualPath); - Assert.Same(route, result.Router); - Assert.Empty(result.DataTokens); - - Assert.Equal(selectedGroup, firstRouteGroupSelected); - } - - [Theory] - [InlineData("template/5", "template/5")] - [InlineData("template/{first:int}", "template/{second:int}")] - [InlineData("template/{first}", "template/{second}")] - [InlineData("template/{*first:int}", "template/{*second:int}")] - [InlineData("template/{*first}", "template/{*second}")] - public void AttributeRoute_GenerateLink_RespectsOrder(string firstTemplate, string secondTemplate) - { - // Arrange - var expectedGroup = CreateRouteGroup(0, secondTemplate); - - var next = new Mock(); - string selectedGroup = null; - next.Setup(n => n.GetVirtualPath(It.IsAny())).Callback(ctx => - { - selectedGroup = (string)ctx.ProvidedValues[AttributeRouting.RouteGroupKey]; - ctx.IsBound = true; - }) - .Returns((VirtualPathData)null); - - var matchingRoutes = Enumerable.Empty(); - - var firstRoute = CreateGenerationEntry(firstTemplate, requiredValues: null, order: 1); - var secondRoute = CreateGenerationEntry(secondTemplate, requiredValues: null, order: 0); - - // We setup the route entries with a lower relative order first to ensure that when - // we try to generate a link, the route with the higher relative order gets tried first. - var linkGenerationEntries = new[] { firstRoute, secondRoute }; - - var route = CreateAttributeRoute(next.Object, matchingRoutes, linkGenerationEntries); - - var context = CreateVirtualPathContext(values: null, ambientValues: new { first = 5, second = 5 }); - - // Act - var result = route.GetVirtualPath(context); - - // Assert - Assert.NotNull(result); - Assert.Equal(new PathString("/template/5"), result.VirtualPath); - Assert.Same(route, result.Router); - Assert.Empty(result.DataTokens); - - Assert.Equal(expectedGroup, selectedGroup); - } - - [Theory] - [InlineData("first/5", "second/5")] - [InlineData("first/{first:int}", "second/{second:int}")] - [InlineData("first/{first}", "second/{second}")] - [InlineData("first/{*first:int}", "second/{*second:int}")] - [InlineData("first/{*first}", "second/{*second}")] - public void AttributeRoute_GenerateLink_EnsuresStableOrder(string firstTemplate, string secondTemplate) - { - // Arrange - var expectedGroup = CreateRouteGroup(0, firstTemplate); - - var next = new Mock(); - string selectedGroup = null; - next.Setup(n => n.GetVirtualPath(It.IsAny())).Callback(ctx => - { - selectedGroup = (string)ctx.ProvidedValues[AttributeRouting.RouteGroupKey]; - ctx.IsBound = true; - }) - .Returns((VirtualPathData)null); - - var matchingRoutes = Enumerable.Empty(); - - var firstRoute = CreateGenerationEntry(firstTemplate, requiredValues: null, order: 0); - var secondRoute = CreateGenerationEntry(secondTemplate, requiredValues: null, order: 0); - - // We setup the route entries with a lower relative template order first to ensure that when - // we try to generate a link, the route with the higher template order gets tried first. - var linkGenerationEntries = new[] { secondRoute, firstRoute }; - - var route = CreateAttributeRoute(next.Object, matchingRoutes, linkGenerationEntries); - - var context = CreateVirtualPathContext(values: null, ambientValues: new { first = 5, second = 5 }); - - // Act - var result = route.GetVirtualPath(context); - - // Assert - Assert.NotNull(result); - Assert.Equal(new PathString("/first/5"), result.VirtualPath); - Assert.Same(route, result.Router); - Assert.Empty(result.DataTokens); - - Assert.Equal(expectedGroup, selectedGroup); - } - - public static IEnumerable NamedEntriesWithDifferentTemplates - { - get - { - var data = new TheoryData>(); - data.Add(new[] - { - CreateGenerationEntry("template", null, 0, "NamedEntry"), - CreateGenerationEntry("otherTemplate", null, 0, "NamedEntry"), - CreateGenerationEntry("anotherTemplate", null, 0, "NamedEntry") - }); - - // Default values for parameters are taken into account by comparing the templates. - data.Add(new[] - { - CreateGenerationEntry("template/{parameter=0}", null, 0, "NamedEntry"), - CreateGenerationEntry("template/{parameter=1}", null, 0, "NamedEntry"), - CreateGenerationEntry("template/{parameter=2}", null, 0, "NamedEntry") - }); - - // Names for entries are compared ignoring casing. - data.Add(new[] - { - CreateGenerationEntry("template/{*parameter:int=0}", null, 0, "NamedEntry"), - CreateGenerationEntry("template/{*parameter:int=1}", null, 0, "NAMEDENTRY"), - CreateGenerationEntry("template/{*parameter:int=2}", null, 0, "namedentry") - }); - return data; - } - } - - [Theory] - [MemberData(nameof(TreeRouterTest.NamedEntriesWithDifferentTemplates))] - public void AttributeRoute_CreateAttributeRoute_ThrowsIfDifferentEntriesHaveTheSameName( - IEnumerable namedEntries) - { - // Arrange - string expectedExceptionMessage = "Two or more routes named 'NamedEntry' have different templates." + - Environment.NewLine + - "Parameter name: linkGenerationEntries"; - - var next = new Mock().Object; - - // Act - var builder = new TreeRouteBuilder(next, NullLogger.Instance, NullLogger.Instance); - var exception = Assert.Throws( - "linkGenerationEntries", - () => - { - foreach (var entry in namedEntries) - { - builder.Add(entry); - } - - return builder.Build(version: 1); - }); - - Assert.Equal(expectedExceptionMessage, exception.Message, StringComparer.OrdinalIgnoreCase); - } - - public static IEnumerable NamedEntriesWithTheSameTemplate - { - get - { - var data = new TheoryData>(); - - data.Add(new[] - { - CreateGenerationEntry("template", null, 0, "NamedEntry"), - CreateGenerationEntry("template", null, 1, "NamedEntry"), - CreateGenerationEntry("template", null, 2, "NamedEntry") - }); - - // Templates are compared ignoring casing. - data.Add(new[] - { - CreateGenerationEntry("template", null, 0, "NamedEntry"), - CreateGenerationEntry("Template", null, 1, "NamedEntry"), - CreateGenerationEntry("TEMPLATE", null, 2, "NamedEntry") - }); - - data.Add(new[] - { - CreateGenerationEntry("template/{parameter=0}", null, 0, "NamedEntry"), - CreateGenerationEntry("template/{parameter=0}", null, 1, "NamedEntry"), - CreateGenerationEntry("template/{parameter=0}", null, 2, "NamedEntry") - }); - - return data; - } - } - - [Theory] - [MemberData(nameof(TreeRouterTest.NamedEntriesWithTheSameTemplate))] - public void AttributeRoute_GeneratesLink_ForMultipleNamedEntriesWithTheSameTemplate( - IEnumerable namedEntries) - { - // Arrange - var expectedLink = new PathString( - namedEntries.First().Template.Parameters.Any() ? "/template/5" : "/template"); - - var expectedGroup = "0&" + namedEntries.First().TemplateText; - string selectedGroup = null; - var next = new Mock(); - next.Setup(s => s.GetVirtualPath(It.IsAny())) - .Callback(vpc => - { - vpc.IsBound = true; - selectedGroup = (string)vpc.ProvidedValues[AttributeRouting.RouteGroupKey]; - }) - .Returns((VirtualPathData)null); - - var matchingEntries = Enumerable.Empty(); - - var route = CreateAttributeRoute(next.Object, matchingEntries, namedEntries); - - var ambientValues = namedEntries.First().Template.Parameters.Any() ? new { parameter = 5 } : null; - - var context = CreateVirtualPathContext(values: null, ambientValues: ambientValues, name: "NamedEntry"); - - // Act - var result = route.GetVirtualPath(context); - - // Assert - Assert.NotNull(result); - Assert.Equal(expectedLink, result.VirtualPath); - Assert.Same(route, result.Router); - Assert.Empty(result.DataTokens); - - Assert.Equal(expectedGroup, selectedGroup); - } - - [Fact] - public void AttributeRoute_GenerateLink_WithName() - { - // Arrange - string selectedGroup = null; - var next = new Mock(); - next.Setup(s => s.GetVirtualPath(It.IsAny())) - .Callback(vpc => - { - vpc.IsBound = true; - selectedGroup = (string)vpc.ProvidedValues[AttributeRouting.RouteGroupKey]; - }) - .Returns((VirtualPathData)null); - - var namedEntry = CreateGenerationEntry("named", requiredValues: null, order: 1, name: "NamedRoute"); - var unnamedEntry = CreateGenerationEntry("unnamed", requiredValues: null, order: 0); - - // The named route has a lower order which will ensure that we aren't trying the route as - // if it were an unnamed route. - var linkGenerationEntries = new[] { namedEntry, unnamedEntry }; - - var matchingEntries = Enumerable.Empty(); - - var route = CreateAttributeRoute(next.Object, matchingEntries, linkGenerationEntries); - - var context = CreateVirtualPathContext(values: null, ambientValues: null, name: "NamedRoute"); - - // Act - var result = route.GetVirtualPath(context); - - // Assert - Assert.NotNull(result); - Assert.Equal(new PathString("/named"), result.VirtualPath); - Assert.Same(route, result.Router); - Assert.Empty(result.DataTokens); - - Assert.Equal("1&named", selectedGroup); - } - - [Fact] - public void AttributeRoute_DoesNotGenerateLink_IfThereIsNoRouteForAGivenName() - { - // Arrange - string selectedGroup = null; - var next = new Mock(); - next.Setup(s => s.GetVirtualPath(It.IsAny())) - .Callback(vpc => - { - vpc.IsBound = true; - selectedGroup = (string)vpc.ProvidedValues[AttributeRouting.RouteGroupKey]; - }); - - var namedEntry = CreateGenerationEntry("named", requiredValues: null, order: 1, name: "NamedRoute"); - - // Add an unnamed entry to ensure we don't fall back to generating a link for an unnamed route. - var unnamedEntry = CreateGenerationEntry("unnamed", requiredValues: null, order: 0); - - // The named route has a lower order which will ensure that we aren't trying the route as - // if it were an unnamed route. - var linkGenerationEntries = new[] { namedEntry, unnamedEntry }; - - var matchingEntries = Enumerable.Empty(); - - var route = CreateAttributeRoute(next.Object, matchingEntries, linkGenerationEntries); - - var context = CreateVirtualPathContext(values: null, ambientValues: null, name: "NonExistingNamedRoute"); - - // Act - var result = route.GetVirtualPath(context); - - // Assert - Assert.Null(result); - } - - [Theory] - [InlineData("template/{parameter:int}", null)] - [InlineData("template/{parameter:int}", "NaN")] - [InlineData("template/{parameter}", null)] - [InlineData("template/{*parameter:int}", null)] - [InlineData("template/{*parameter:int}", "NaN")] - public void AttributeRoute_DoesNotGenerateLink_IfValuesDoNotMatchNamedEntry(string template, string value) - { - // Arrange - string selectedGroup = null; - var next = new Mock(); - next.Setup(s => s.GetVirtualPath(It.IsAny())) - .Callback(vpc => - { - vpc.IsBound = true; - selectedGroup = (string)vpc.ProvidedValues[AttributeRouting.RouteGroupKey]; - }); - - var namedEntry = CreateGenerationEntry(template, requiredValues: null, order: 1, name: "NamedRoute"); - - // Add an unnamed entry to ensure we don't fall back to generating a link for an unnamed route. - var unnamedEntry = CreateGenerationEntry("unnamed", requiredValues: null, order: 0); - - // The named route has a lower order which will ensure that we aren't trying the route as - // if it were an unnamed route. - var linkGenerationEntries = new[] { namedEntry, unnamedEntry }; - - var matchingEntries = Enumerable.Empty(); - - var route = CreateAttributeRoute(next.Object, matchingEntries, linkGenerationEntries); - - var ambientValues = value == null ? null : new { parameter = value }; - - var context = CreateVirtualPathContext(values: null, ambientValues: ambientValues, name: "NamedRoute"); - - // Act - var result = route.GetVirtualPath(context); - - // Assert - Assert.Null(result); - } - - [Theory] - [InlineData("template/{parameter:int}", "5")] - [InlineData("template/{parameter}", "5")] - [InlineData("template/{*parameter:int}", "5")] - [InlineData("template/{*parameter}", "5")] - public void AttributeRoute_GeneratesLink_IfValuesMatchNamedEntry(string template, string value) - { - // Arrange - string selectedGroup = null; - var next = new Mock(); - next.Setup(s => s.GetVirtualPath(It.IsAny())) - .Callback(vpc => - { - vpc.IsBound = true; - selectedGroup = (string)vpc.ProvidedValues[AttributeRouting.RouteGroupKey]; - }) - .Returns((VirtualPathData)null); - - var namedEntry = CreateGenerationEntry(template, requiredValues: null, order: 1, name: "NamedRoute"); - - // Add an unnamed entry to ensure we don't fall back to generating a link for an unnamed route. - var unnamedEntry = CreateGenerationEntry("unnamed", requiredValues: null, order: 0); - - // The named route has a lower order which will ensure that we aren't trying the route as - // if it were an unnamed route. - var linkGenerationEntries = new[] { namedEntry, unnamedEntry }; - - var matchingEntries = Enumerable.Empty(); - - var route = CreateAttributeRoute(next.Object, matchingEntries, linkGenerationEntries); - - var ambientValues = value == null ? null : new { parameter = value }; - - var context = CreateVirtualPathContext(values: null, ambientValues: ambientValues, name: "NamedRoute"); - - // Act - var result = route.GetVirtualPath(context); - - // Assert - Assert.NotNull(result); - Assert.Equal(new PathString("/template/5"), result.VirtualPath); - Assert.Same(route, result.Router); - Assert.Empty(result.DataTokens); - - Assert.Equal(string.Format("1&{0}", template), selectedGroup); - } - - [Fact] - public void AttributeRoute_GenerateLink_NoRequiredValues() - { - // Arrange - var entry = CreateGenerationEntry("api/Store", new { }); - var route = CreateAttributeRoute(entry); - - var context = CreateVirtualPathContext(new { }); - - // Act - var pathData = route.GetVirtualPath(context); - - // Assert - Assert.NotNull(pathData); - Assert.Equal(new PathString("/api/Store"), pathData.VirtualPath); - Assert.Same(route, pathData.Router); - Assert.Empty(pathData.DataTokens); - } - - [Fact] - public void AttributeRoute_GenerateLink_Match() - { - // Arrange - var entry = CreateGenerationEntry("api/Store", new { action = "Index", controller = "Store" }); - var route = CreateAttributeRoute(entry); - - var context = CreateVirtualPathContext(new { action = "Index", controller = "Store" }); - - // Act - var pathData = route.GetVirtualPath(context); - - // Assert - Assert.NotNull(pathData); - Assert.Equal(new PathString("/api/Store"), pathData.VirtualPath); - Assert.Same(route, pathData.Router); - Assert.Empty(pathData.DataTokens); - } - - [Fact] - public void AttributeRoute_GenerateLink_NoMatch() - { - // Arrange - var entry = CreateGenerationEntry("api/Store", new { action = "Details", controller = "Store" }); - var route = CreateAttributeRoute(entry); - - var context = CreateVirtualPathContext(new { action = "Index", controller = "Store" }); - - // Act - var path = route.GetVirtualPath(context); - - // Assert - Assert.Null(path); - } - - [Fact] - public void AttributeRoute_GenerateLink_Match_WithAmbientValues() - { - // Arrange - var entry = CreateGenerationEntry("api/Store", new { action = "Index", controller = "Store" }); - var route = CreateAttributeRoute(entry); - - var context = CreateVirtualPathContext(new { }, new { action = "Index", controller = "Store" }); - - // Act - var pathData = route.GetVirtualPath(context); - - // Assert - Assert.NotNull(pathData); - Assert.Equal(new PathString("/api/Store"), pathData.VirtualPath); - Assert.Same(route, pathData.Router); - Assert.Empty(pathData.DataTokens); - } - - [Fact] - public void AttributeRoute_GenerateLink_Match_WithParameters() - { - // Arrange - var entry = CreateGenerationEntry("api/Store/{action}", new { action = "Index", controller = "Store" }); - var route = CreateAttributeRoute(entry); - - var context = CreateVirtualPathContext(new { action = "Index", controller = "Store" }); - - // Act - var pathData = route.GetVirtualPath(context); - - // Assert - Assert.NotNull(pathData); - Assert.Equal(new PathString("/api/Store/Index"), pathData.VirtualPath); - Assert.Same(route, pathData.Router); - Assert.Empty(pathData.DataTokens); - } - - [Fact] - public void AttributeRoute_GenerateLink_Match_WithMoreParameters() - { - // Arrange - var entry = CreateGenerationEntry( - "api/{area}/dosomething/{controller}/{action}", - new { action = "Index", controller = "Store", area = "AwesomeCo" }); - - var expectedValues = new Dictionary(StringComparer.OrdinalIgnoreCase) - { - { "area", "AwesomeCo" }, - { "controller", "Store" }, - { "action", "Index" }, - { AttributeRouting.RouteGroupKey, entry.RouteGroup }, - }; - - var next = new StubRouter(); - var route = CreateAttributeRoute(next, entry); - - var context = CreateVirtualPathContext( - new { action = "Index", controller = "Store" }, - new { area = "AwesomeCo" }); - - // Act - var pathData = route.GetVirtualPath(context); - - // Assert - Assert.NotNull(pathData); - Assert.Equal(new PathString("/api/AwesomeCo/dosomething/Store/Index"), pathData.VirtualPath); - Assert.Same(route, pathData.Router); - Assert.Empty(pathData.DataTokens); - - Assert.Equal(expectedValues, next.GenerationContext.ProvidedValues); - } - - [Fact] - public void AttributeRoute_GenerateLink_Match_WithDefault() - { - // Arrange - var entry = CreateGenerationEntry("api/Store/{action=Index}", new { action = "Index", controller = "Store" }); - var route = CreateAttributeRoute(entry); - - var context = CreateVirtualPathContext(new { action = "Index", controller = "Store" }); - - // Act - var pathData = route.GetVirtualPath(context); - - // Assert - Assert.NotNull(pathData); - Assert.Equal(new PathString("/api/Store"), pathData.VirtualPath); - Assert.Same(route, pathData.Router); - Assert.Empty(pathData.DataTokens); - } - - [Fact] - public void AttributeRoute_GenerateLink_Match_WithConstraint() - { - // Arrange - var entry = CreateGenerationEntry("api/Store/{action}/{id:int}", new { action = "Index", controller = "Store" }); - - var expectedValues = new Dictionary(StringComparer.OrdinalIgnoreCase) - { - { "action", "Index" }, - { "id", 5 }, - { AttributeRouting.RouteGroupKey, entry.RouteGroup }, - }; - - var next = new StubRouter(); - var route = CreateAttributeRoute(next, entry); - - var context = CreateVirtualPathContext(new { action = "Index", controller = "Store", id = 5 }); - - // Act - var pathData = route.GetVirtualPath(context); - - // Assert - Assert.NotNull(pathData); - Assert.Equal(new PathString("/api/Store/Index/5"), pathData.VirtualPath); - Assert.Same(route, pathData.Router); - Assert.Empty(pathData.DataTokens); - - Assert.Equal(expectedValues, next.GenerationContext.ProvidedValues); - } - - [Fact] - public void AttributeRoute_GenerateLink_NoMatch_WithConstraint() - { - // Arrange - var entry = CreateGenerationEntry("api/Store/{action}/{id:int}", new { action = "Index", controller = "Store" }); - var route = CreateAttributeRoute(entry); - - var expectedValues = new Dictionary(StringComparer.OrdinalIgnoreCase) - { - { "id", "5" }, - { AttributeRouting.RouteGroupKey, entry.RouteGroup }, - }; - - var next = new StubRouter(); - var context = CreateVirtualPathContext(new { action = "Index", controller = "Store", id = "heyyyy" }); - - // Act - var path = route.GetVirtualPath(context); - - // Assert - Assert.Null(path); - } - - [Fact] - public void AttributeRoute_GenerateLink_Match_WithMixedAmbientValues() - { - // Arrange - var entry = CreateGenerationEntry("api/Store", new { action = "Index", controller = "Store" }); - var route = CreateAttributeRoute(entry); - - var context = CreateVirtualPathContext(new { action = "Index" }, new { controller = "Store" }); - - // Act - var pathData = route.GetVirtualPath(context); - - // Assert - Assert.NotNull(pathData); - Assert.Equal(new PathString("/api/Store"), pathData.VirtualPath); - Assert.Same(route, pathData.Router); - Assert.Empty(pathData.DataTokens); - } - - [Fact] - public void AttributeRoute_GenerateLink_Match_WithQueryString() - { - // Arrange - var entry = CreateGenerationEntry("api/Store", new { action = "Index", controller = "Store" }); - var route = CreateAttributeRoute(entry); - - var context = CreateVirtualPathContext(new { action = "Index", id = 5 }, new { controller = "Store" }); - - // Act - var pathData = route.GetVirtualPath(context); - - // Assert - Assert.NotNull(pathData); - Assert.Equal(new PathString("/api/Store?id=5"), pathData.VirtualPath); - Assert.Same(route, pathData.Router); - Assert.Empty(pathData.DataTokens); - } - - [Fact] - public void AttributeRoute_GenerateLink_ForwardsRouteGroup() - { - // Arrange - var entry = CreateGenerationEntry("api/Store", new { action = "Index", controller = "Store" }); - - var expectedValues = new Dictionary(StringComparer.OrdinalIgnoreCase) - { - { AttributeRouting.RouteGroupKey, entry.RouteGroup }, - }; - - var next = new StubRouter(); - var route = CreateAttributeRoute(next, entry); - - var context = CreateVirtualPathContext(new { action = "Index", controller = "Store" }); - - // Act - var path = route.GetVirtualPath(context); - - // Assert - Assert.Equal(expectedValues, next.GenerationContext.ProvidedValues); - } - - [Fact] - public void AttributeRoute_GenerateLink_RejectedByFirstRoute() - { - // Arrange - var entry1 = CreateGenerationEntry("api/Store", new { action = "Index", controller = "Store" }); - var entry2 = CreateGenerationEntry("api2/{controller}", new { action = "Index", controller = "Blog" }); - - var route = CreateAttributeRoute(entry1, entry2); - - var context = CreateVirtualPathContext(new { action = "Index", controller = "Blog" }); - - // Act - var pathData = route.GetVirtualPath(context); - - // Assert - Assert.NotNull(pathData); - Assert.Equal(new PathString("/api2/Blog"), pathData.VirtualPath); - Assert.Same(route, pathData.Router); - Assert.Empty(pathData.DataTokens); - } - - [Fact] - public void AttributeRoute_GenerateLink_RejectedByHandler() - { - // Arrange - var entry1 = CreateGenerationEntry("api/Store", new { action = "Edit", controller = "Store" }); - var entry2 = CreateGenerationEntry("api2/{controller}", new { action = "Edit", controller = "Store" }); - - var next = new StubRouter(); - - var callCount = 0; - next.GenerationDelegate = (VirtualPathContext c) => - { - // Reject entry 1. - callCount++; - return !c.ProvidedValues.Contains(new KeyValuePair( - AttributeRouting.RouteGroupKey, - entry1.RouteGroup)); - }; - - var route = CreateAttributeRoute(next, entry1, entry2); - - var context = CreateVirtualPathContext(new { action = "Edit", controller = "Store" }); - - // Act - var pathData = route.GetVirtualPath(context); - - // Assert - Assert.NotNull(pathData); - Assert.Equal(new PathString("/api2/Store"), pathData.VirtualPath); - Assert.Same(route, pathData.Router); - Assert.Empty(pathData.DataTokens); - - Assert.Equal(2, callCount); - } - - [Fact] - public void AttributeRoute_GenerateLink_ToArea() - { - // Arrange - var entry1 = CreateGenerationEntry("Help/Store", new { area = "Help", action = "Edit", controller = "Store" }); - entry1.GenerationPrecedence = 2; - - var entry2 = CreateGenerationEntry("Store", new { area = (string)null, action = "Edit", controller = "Store" }); - entry2.GenerationPrecedence = 1; - - var next = new StubRouter(); - - var route = CreateAttributeRoute(next, entry1, entry2); - - var context = CreateVirtualPathContext(new { area = "Help", action = "Edit", controller = "Store" }); - - // Act - var pathData = route.GetVirtualPath(context); - - // Assert - Assert.NotNull(pathData); - Assert.Equal(new PathString("/Help/Store"), pathData.VirtualPath); - Assert.Same(route, pathData.Router); - Assert.Empty(pathData.DataTokens); - } - - [Fact] - public void AttributeRoute_GenerateLink_ToArea_PredecedenceReversed() - { - // Arrange - var entry1 = CreateGenerationEntry("Help/Store", new { area = "Help", action = "Edit", controller = "Store" }); - entry1.GenerationPrecedence = 1; - - var entry2 = CreateGenerationEntry("Store", new { area = (string)null, action = "Edit", controller = "Store" }); - entry2.GenerationPrecedence = 2; - - var next = new StubRouter(); - - var route = CreateAttributeRoute(next, entry1, entry2); - - var context = CreateVirtualPathContext(new { area = "Help", action = "Edit", controller = "Store" }); - - // Act - var pathData = route.GetVirtualPath(context); - - // Assert - Assert.NotNull(pathData); - Assert.Equal(new PathString("/Help/Store"), pathData.VirtualPath); - Assert.Same(route, pathData.Router); - Assert.Empty(pathData.DataTokens); - } - - [Fact] - public void AttributeRoute_GenerateLink_ToArea_WithAmbientValues() - { - // Arrange - var entry1 = CreateGenerationEntry("Help/Store", new { area = "Help", action = "Edit", controller = "Store" }); - entry1.GenerationPrecedence = 2; - - var entry2 = CreateGenerationEntry("Store", new { area = (string)null, action = "Edit", controller = "Store" }); - entry2.GenerationPrecedence = 1; - - var next = new StubRouter(); - - var route = CreateAttributeRoute(next, entry1, entry2); - - var context = CreateVirtualPathContext( - values: new { action = "Edit", controller = "Store" }, - ambientValues: new { area = "Help" }); - - // Act - var pathData = route.GetVirtualPath(context); - - // Assert - Assert.NotNull(pathData); - Assert.Equal(new PathString("/Help/Store"), pathData.VirtualPath); - Assert.Same(route, pathData.Router); - Assert.Empty(pathData.DataTokens); - } - - [Fact] - public void AttributeRoute_GenerateLink_OutOfArea_IgnoresAmbientValue() - { - // Arrange - var entry1 = CreateGenerationEntry("Help/Store", new { area = "Help", action = "Edit", controller = "Store" }); - entry1.GenerationPrecedence = 2; - - var entry2 = CreateGenerationEntry("Store", new { area = (string)null, action = "Edit", controller = "Store" }); - entry2.GenerationPrecedence = 1; - - var next = new StubRouter(); - - var route = CreateAttributeRoute(next, entry1, entry2); - - var context = CreateVirtualPathContext( - values: new { action = "Edit", controller = "Store" }, - ambientValues: new { area = "Blog" }); - - // Act - var pathData = route.GetVirtualPath(context); - - // Assert - Assert.NotNull(pathData); - Assert.Equal(new PathString("/Store"), pathData.VirtualPath); - Assert.Same(route, pathData.Router); - Assert.Empty(pathData.DataTokens); - } - - public static IEnumerable OptionalParamValues - { - get - { - return new object[][] - { - // defaults - // ambient values - // values - new object[] - { - "Test/{val1}/{val2}.{val3?}", - new {val1 = "someval1", val2 = "someval2", val3 = "someval3a"}, - new {val3 = "someval3v"}, - "/Test/someval1/someval2.someval3v", - }, - new object[] - { - "Test/{val1}/{val2}.{val3?}", - new {val3 = "someval3a"}, - new {val1 = "someval1", val2 = "someval2", val3 = "someval3v" }, - "/Test/someval1/someval2.someval3v", - }, - new object[] - { - "Test/{val1}/{val2}.{val3?}", - null, - new {val1 = "someval1", val2 = "someval2" }, - "/Test/someval1/someval2", - }, - new object[] - { - "Test/{val1}.{val2}.{val3}.{val4?}", - new {val1 = "someval1", val2 = "someval2" }, - new {val4 = "someval4", val3 = "someval3" }, - "/Test/someval1.someval2.someval3.someval4", - }, - new object[] - { - "Test/{val1}.{val2}.{val3}.{val4?}", - new {val1 = "someval1", val2 = "someval2" }, - new {val3 = "someval3" }, - "/Test/someval1.someval2.someval3", - }, - new object[] - { - "Test/.{val2?}", - null, - new {val2 = "someval2" }, - "/Test/.someval2", - }, - new object[] - { - "Test/.{val2?}", - null, - null, - "/Test/", - }, - new object[] - { - "Test/{val1}.{val2}", - new {val1 = "someval1", val2 = "someval2" }, - new {val3 = "someval3" }, - "/Test/someval1.someval2?val3=someval3", - }, - }; - } - } - - [Theory] - [MemberData("OptionalParamValues")] - public void AttributeRoute_GenerateLink_Match_WithOptionalParameters( - string template, - object ambientValues, - object values, - string expected) - { - // Arrange - var entry = CreateGenerationEntry(template, null); - var route = CreateAttributeRoute(entry); - - var context = CreateVirtualPathContext(values, ambientValues); - - // Act - var pathData = route.GetVirtualPath(context); - - // Assert - Assert.NotNull(pathData); - Assert.Equal(new PathString(expected), pathData.VirtualPath); - Assert.Same(route, pathData.Router); - Assert.Empty(pathData.DataTokens); - } - - [Fact] - public async Task AttributeRoute_CreatesNewRouteData() - { - // Arrange - RouteData nestedRouteData = null; - var next = new Mock(); - next - .Setup(r => r.RouteAsync(It.IsAny())) - .Callback((c) => - { - nestedRouteData = c.RouteData; - c.IsHandled = true; - }) - .Returns(Task.FromResult(true)); - - var entry = CreateMatchingEntry(next.Object, "api/Store", order: 0); - var route = CreateAttributeRoute(next.Object, entry); - - var context = CreateRouteContext("/api/Store"); - - var originalRouteData = context.RouteData; - originalRouteData.Values.Add("action", "Index"); - - // Act - await route.RouteAsync(context); - - // Assert - Assert.NotSame(originalRouteData, context.RouteData); - Assert.NotSame(originalRouteData, nestedRouteData); - Assert.Same(nestedRouteData, context.RouteData); - - // The new routedata is a copy - Assert.Equal("Index", context.RouteData.Values["action"]); - Assert.Single(context.RouteData.Values, kvp => kvp.Key == "test_route_group"); - - Assert.Equal(1, context.RouteData.Routers.Count); - Assert.Equal(next.Object.GetType(), context.RouteData.Routers[0].GetType()); - } - - [Fact] - public async Task AttributeRoute_ReplacesExistingRouteValues_IfNotNull() - { - // Arrange - var router = new Mock(); - router - .Setup(r => r.RouteAsync(It.IsAny())) - .Callback((c) => - { - c.IsHandled = true; - }) - .Returns(Task.FromResult(true)); - - var entry = CreateMatchingEntry(router.Object, "Foo/{*path}", order: 0); - var route = CreateAttributeRoute(router.Object, entry); - - var context = CreateRouteContext("/Foo/Bar"); - - var originalRouteData = context.RouteData; - originalRouteData.Values.Add("path", "default"); - - // Act - await route.RouteAsync(context); - - // Assert - Assert.Equal("Bar", context.RouteData.Values["path"]); - } - - [Fact] - public async Task AttributeRoute_DoesNotReplaceExistingRouteValues_IfNull() - { - // Arrange - var router = new Mock(); - router - .Setup(r => r.RouteAsync(It.IsAny())) - .Callback((c) => - { - c.IsHandled = true; - }) - .Returns(Task.FromResult(true)); - - var entry = CreateMatchingEntry(router.Object, "Foo/{*path}", order: 0); - var route = CreateAttributeRoute(router.Object, entry); - - var context = CreateRouteContext("/Foo/"); - - var originalRouteData = context.RouteData; - originalRouteData.Values.Add("path", "default"); - - // Act - await route.RouteAsync(context); - - // Assert - Assert.Equal("default", context.RouteData.Values["path"]); - } - - [Fact] - public async Task AttributeRoute_CreatesNewRouteData_ResetsWhenNotMatched() - { - // Arrange - RouteData nestedRouteData = null; - var next = new Mock(); - next - .Setup(r => r.RouteAsync(It.IsAny())) - .Callback((c) => - { - nestedRouteData = c.RouteData; - c.IsHandled = false; - }) - .Returns(Task.FromResult(true)); - - var entry = CreateMatchingEntry(next.Object, "api/Store", order: 0); - var route = CreateAttributeRoute(next.Object, entry); - - var context = CreateRouteContext("/api/Store"); - - var originalRouteData = context.RouteData; - originalRouteData.Values.Add("action", "Index"); - - // Act - await route.RouteAsync(context); - - // Assert - Assert.Same(originalRouteData, context.RouteData); - Assert.NotSame(originalRouteData, nestedRouteData); - Assert.NotSame(nestedRouteData, context.RouteData); - - // The new routedata is a copy - Assert.Equal("Index", context.RouteData.Values["action"]); - Assert.Equal("Index", nestedRouteData.Values["action"]); - Assert.DoesNotContain(context.RouteData.Values, kvp => kvp.Key == "test_route_group"); - Assert.Single(nestedRouteData.Values, kvp => kvp.Key == "test_route_group"); - - Assert.Empty(context.RouteData.Routers); - - Assert.Equal(1, nestedRouteData.Routers.Count); - Assert.Equal(next.Object.GetType(), nestedRouteData.Routers[0].GetType()); - } - - [Fact] - public async Task AttributeRoute_CreatesNewRouteData_ResetsWhenThrows() - { - // Arrange - RouteData nestedRouteData = null; - var next = new Mock(); - next - .Setup(r => r.RouteAsync(It.IsAny())) - .Callback((c) => - { - nestedRouteData = c.RouteData; - c.IsHandled = false; - }) - .Throws(new Exception()); - - var entry = CreateMatchingEntry(next.Object, "api/Store", order: 0); - var route = CreateAttributeRoute(next.Object, entry); - - var context = CreateRouteContext("/api/Store"); - - var originalRouteData = context.RouteData; - originalRouteData.Values.Add("action", "Index"); - - // Act - await Assert.ThrowsAsync(() => route.RouteAsync(context)); - - // Assert - Assert.Same(originalRouteData, context.RouteData); - Assert.NotSame(originalRouteData, nestedRouteData); - Assert.NotSame(nestedRouteData, context.RouteData); - - // The new routedata is a copy - Assert.Equal("Index", context.RouteData.Values["action"]); - Assert.Equal("Index", nestedRouteData.Values["action"]); - Assert.DoesNotContain(context.RouteData.Values, kvp => kvp.Key == "test_route_group"); - Assert.Single(nestedRouteData.Values, kvp => kvp.Key == "test_route_group"); - - Assert.Empty(context.RouteData.Routers); - - Assert.Equal(1, nestedRouteData.Routers.Count); - Assert.Equal(next.Object.GetType(), nestedRouteData.Routers[0].GetType()); - } - - private static RouteContext CreateRouteContext(string requestPath) - { - var request = new Mock(MockBehavior.Strict); - request.SetupGet(r => r.Path).Returns(new PathString(requestPath)); - - var context = new Mock(MockBehavior.Strict); - context.Setup(m => m.RequestServices.GetService(typeof(ILoggerFactory))) - .Returns(NullLoggerFactory.Instance); - - context.SetupGet(c => c.Request).Returns(request.Object); - - return new RouteContext(context.Object); - } - - private static VirtualPathContext CreateVirtualPathContext( - object values, - object ambientValues = null, - string name = null) - { - var mockHttpContext = new Mock(); - mockHttpContext.Setup(h => h.RequestServices.GetService(typeof(ILoggerFactory))) - .Returns(NullLoggerFactory.Instance); - - return new VirtualPathContext( - mockHttpContext.Object, - new RouteValueDictionary(ambientValues), - new RouteValueDictionary(values), - name); - } - - private static TreeRouteMatchingEntry CreateMatchingEntry(IRouter router, string template, int order) - { - var routeGroup = string.Format("{0}&&{1}", order, template); - var entry = new TreeRouteMatchingEntry(); - entry.Target = router; - entry.RouteTemplate = TemplateParser.Parse(template); - var parsedRouteTemplate = TemplateParser.Parse(template); - entry.TemplateMatcher = new TemplateMatcher( - parsedRouteTemplate, - new RouteValueDictionary(new { test_route_group = routeGroup })); - entry.Precedence = AttributeRoutePrecedence.ComputeMatched(parsedRouteTemplate); - entry.Order = order; - entry.Constraints = GetRouteConstriants(CreateConstraintResolver(), template, parsedRouteTemplate); - return entry; - } - - private static TreeRouteLinkGenerationEntry CreateGenerationEntry( - string template, - object requiredValues, - int order = 0, - string name = null) - { - var constraintResolver = CreateConstraintResolver(); - - var entry = new TreeRouteLinkGenerationEntry(); - entry.TemplateText = template; - entry.Template = TemplateParser.Parse(template); - - var defaults = entry.Template.Parameters - .Where(p => p.DefaultValue != null) - .ToDictionary(p => p.Name, p => p.DefaultValue); - - var constraintBuilder = new RouteConstraintBuilder(CreateConstraintResolver(), template); - foreach (var parameter in entry.Template.Parameters) - { - if (parameter.InlineConstraints != null) - { - if (parameter.IsOptional) - { - constraintBuilder.SetOptional(parameter.Name); - } - - foreach (var constraint in parameter.InlineConstraints) - { - constraintBuilder.AddResolvedConstraint(parameter.Name, constraint.Constraint); - } - } - } - - var constraints = constraintBuilder.Build(); - - entry.Constraints = constraints; - entry.Defaults = defaults; - entry.Binder = new TemplateBinder(entry.Template, defaults); - entry.Order = order; - entry.GenerationPrecedence = AttributeRoutePrecedence.ComputeGenerated(entry.Template); - entry.RequiredLinkValues = new RouteValueDictionary(requiredValues); - entry.RouteGroup = CreateRouteGroup(order, template); - entry.Name = name; - return entry; - } - - private TreeRouteMatchingEntry CreateMatchingEntry(string template) - { - var mockConstraint = new Mock(); - mockConstraint.Setup(c => c.Match( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny>(), - It.IsAny())) - .Returns(true); - - var mockConstraintResolver = new Mock(); - mockConstraintResolver.Setup(r => r.ResolveConstraint( - It.IsAny())) - .Returns(mockConstraint.Object); - - var entry = new TreeRouteMatchingEntry(); - entry.Target = new StubRouter(); - entry.RouteTemplate = TemplateParser.Parse(template); - - return entry; - } - - private static string CreateRouteGroup(int order, string template) - { - return string.Format("{0}&{1}", order, template); - } - - private static DefaultInlineConstraintResolver CreateConstraintResolver() - { - var options = new RouteOptions(); - var optionsMock = new Mock>(); - optionsMock.SetupGet(o => o.Value).Returns(options); - - return new DefaultInlineConstraintResolver(optionsMock.Object); - } - - private static TreeRouter CreateAttributeRoute(TreeRouteLinkGenerationEntry entry) - { - return CreateAttributeRoute(new StubRouter(), entry); - } - - private static TreeRouter CreateAttributeRoute(IRouter next, TreeRouteLinkGenerationEntry entry) - { - return CreateAttributeRoute(next, new[] { entry }); - } - - private static TreeRouter CreateAttributeRoute(params TreeRouteLinkGenerationEntry[] entries) - { - return CreateAttributeRoute(new StubRouter(), entries); - } - - private static TreeRouter CreateAttributeRoute(IRouter next, params TreeRouteLinkGenerationEntry[] entries) - { - return CreateAttributeRoute( - next, - Enumerable.Empty(), - entries); - } - - private static TreeRouter CreateAttributeRoute(IRouter next, params TreeRouteMatchingEntry[] entries) - { - return CreateAttributeRoute( - next, - entries, - Enumerable.Empty()); - } - - private static TreeRouter CreateAttributeRoute( - IRouter next, - IEnumerable matchingEntries, - IEnumerable generationEntries) - { - var builder = new TreeRouteBuilder( - next, - NullLogger.Instance, - NullLogger.Instance); - - foreach (var entry in matchingEntries) - { - builder.Add(entry); - } - - foreach (var entry in generationEntries) - { - builder.Add(entry); - } - - return builder.Build(version: 1); - } - - private static TreeRouter CreateAttributeRoute( - Action virtualPathCallback, - string firstTemplate, - string secondTemplate) - { - var next = new Mock(); - next.Setup(n => n.GetVirtualPath(It.IsAny())).Callback(virtualPathCallback) - .Returns((VirtualPathData)null); - - var matchingRoutes = Enumerable.Empty(); - var firstEntry = CreateGenerationEntry(firstTemplate, requiredValues: null); - var secondEntry = CreateGenerationEntry(secondTemplate, requiredValues: null); - - return CreateAttributeRoute( - next.Object, - matchingRoutes, - new[] { secondEntry, firstEntry }); - } - - private static TreeRouter CreateRoutingAttributeRoute( - ILoggerFactory loggerFactory = null, - params TreeRouteMatchingEntry[] entries) - { - loggerFactory = loggerFactory ?? NullLoggerFactory.Instance; - - var builder = new TreeRouteBuilder( - new StubRouter(), - loggerFactory.CreateLogger(), - loggerFactory.CreateLogger(typeof(RouteConstraintMatcher).FullName)); - - foreach (var entry in entries) - { - builder.Add(entry); - } - - return builder.Build(version: 1); - } - - private static IReadOnlyDictionary GetRouteConstriants( - IInlineConstraintResolver inlineConstraintResolver, - string template, - RouteTemplate parsedRouteTemplate) - { - var constraintBuilder = new RouteConstraintBuilder(inlineConstraintResolver, template); - foreach (var parameter in parsedRouteTemplate.Parameters) - { - if (parameter.InlineConstraints != null) - { - if (parameter.IsOptional) - { - constraintBuilder.SetOptional(parameter.Name); - } - - foreach (var constraint in parameter.InlineConstraints) - { - constraintBuilder.AddResolvedConstraint(parameter.Name, constraint.Constraint); - } - } - } - - return constraintBuilder.Build(); - } - private class StubRouter : IRouter - { - public VirtualPathContext GenerationContext { get; set; } - - public Func GenerationDelegate { get; set; } - - public RouteContext MatchingContext { get; set; } - - public Func MatchingDelegate { get; set; } - - public VirtualPathData GetVirtualPath(VirtualPathContext context) - { - GenerationContext = context; - - if (GenerationDelegate == null) - { - context.IsBound = true; - } - else - { - context.IsBound = GenerationDelegate(context); - } - - return null; - } - - public Task RouteAsync(RouteContext context) - { - if (MatchingDelegate == null) - { - context.IsHandled = true; - } - else - { - context.IsHandled = MatchingDelegate(context); - } - - return Task.FromResult(true); - } - } - } -} \ No newline at end of file