From e8ce0e752360a98871a4f4c08df8213b2e2a2e01 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Tue, 19 Apr 2016 12:26:09 -0700 Subject: [PATCH] TreeRouter cleanup --- .../RoutingServiceCollectionExtensions.cs | 5 + .../Internal/LinkGenerationDecisionTree.cs | 50 +- .../Internal/LinkGenerationMatch.cs | 23 - .../Internal/OutboundMatchResult.cs | 20 + .../Template/RoutePrecedence.cs | 14 +- .../Tree/InboundMatch.cs | 14 + .../Tree/InboundRouteEntry.cs | 56 ++ .../Tree/OutboundMatch.cs | 14 + ...nerationEntry.cs => OutboundRouteEntry.cs} | 43 +- .../Tree/TreeRouteBuilder.cs | 195 +++- .../Tree/TreeRouteMatchingEntry.cs | 50 - .../Tree/TreeRouter.cs | 100 +- .../Tree/UrlMatchingNode.cs | 4 +- .../LinkGenerationDecisionTreeTest.cs | 113 +-- .../Template/RoutePrecedenceTests.cs | 4 +- .../Tree/TreeRouteBuilderTest.cs | 88 ++ .../Tree/TreeRouterTest.cs | 911 +++++------------- 17 files changed, 808 insertions(+), 896 deletions(-) delete mode 100644 src/Microsoft.AspNetCore.Routing/Internal/LinkGenerationMatch.cs create mode 100644 src/Microsoft.AspNetCore.Routing/Internal/OutboundMatchResult.cs create mode 100644 src/Microsoft.AspNetCore.Routing/Tree/InboundMatch.cs create mode 100644 src/Microsoft.AspNetCore.Routing/Tree/InboundRouteEntry.cs create mode 100644 src/Microsoft.AspNetCore.Routing/Tree/OutboundMatch.cs rename src/Microsoft.AspNetCore.Routing/Tree/{TreeRouteLinkGenerationEntry.cs => OutboundRouteEntry.cs} (51%) delete mode 100644 src/Microsoft.AspNetCore.Routing/Tree/TreeRouteMatchingEntry.cs create mode 100644 test/Microsoft.AspNetCore.Routing.Tests/Tree/TreeRouteBuilderTest.cs diff --git a/src/Microsoft.AspNetCore.Routing/DependencyInjection/RoutingServiceCollectionExtensions.cs b/src/Microsoft.AspNetCore.Routing/DependencyInjection/RoutingServiceCollectionExtensions.cs index 36c7574ce3..a0b2b6b891 100644 --- a/src/Microsoft.AspNetCore.Routing/DependencyInjection/RoutingServiceCollectionExtensions.cs +++ b/src/Microsoft.AspNetCore.Routing/DependencyInjection/RoutingServiceCollectionExtensions.cs @@ -5,6 +5,7 @@ using System; using System.Text.Encodings.Web; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Internal; +using Microsoft.AspNetCore.Routing.Tree; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.ObjectPool; @@ -36,6 +37,10 @@ namespace Microsoft.Extensions.DependencyInjection return provider.Create(new UriBuilderContextPooledObjectPolicy(encoder)); }); + // The TreeRouteBuilder is a builder for creating routes, it should stay transient because it's + // stateful. + services.TryAddTransient(); + services.TryAddSingleton(typeof(RoutingMarkerService)); return services; diff --git a/src/Microsoft.AspNetCore.Routing/Internal/LinkGenerationDecisionTree.cs b/src/Microsoft.AspNetCore.Routing/Internal/LinkGenerationDecisionTree.cs index b22be9d196..f71e6dd74d 100644 --- a/src/Microsoft.AspNetCore.Routing/Internal/LinkGenerationDecisionTree.cs +++ b/src/Microsoft.AspNetCore.Routing/Internal/LinkGenerationDecisionTree.cs @@ -11,23 +11,23 @@ namespace Microsoft.AspNetCore.Routing.Internal // A decision tree that matches link generation entries based on route data. public class LinkGenerationDecisionTree { - private readonly DecisionTreeNode _root; + private readonly DecisionTreeNode _root; - public LinkGenerationDecisionTree(IReadOnlyList entries) + public LinkGenerationDecisionTree(IReadOnlyList entries) { - _root = DecisionTreeBuilder.GenerateTree( + _root = DecisionTreeBuilder.GenerateTree( entries, - new AttributeRouteLinkGenerationEntryClassifier()); + new OutboundMatchClassifier()); } - public IList GetMatches(VirtualPathContext context) + public IList GetMatches(VirtualPathContext context) { // Perf: Avoid allocation for List if there aren't any Matches or Criteria if (_root.Matches.Count > 0 || _root.Criteria.Count > 0) { - var results = new List(); + var results = new List(); Walk(results, context, _root, isFallbackPath: false); - results.Sort(LinkGenerationMatchComparer.Instance); + results.Sort(OutboundMatchResultComparer.Instance); return results; } @@ -60,16 +60,16 @@ namespace Microsoft.AspNetCore.Routing.Internal // // The decision tree uses a tree data structure to execute these rules across all candidates at once. private void Walk( - List results, + List results, VirtualPathContext context, - DecisionTreeNode node, + 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)); + results.Add(new OutboundMatchResult(node.Matches[i], isFallbackPath)); } for (var i = 0; i < node.Criteria.Count; i++) @@ -80,7 +80,7 @@ namespace Microsoft.AspNetCore.Routing.Internal object value; if (context.Values.TryGetValue(key, out value)) { - DecisionTreeNode branch; + DecisionTreeNode branch; if (criterion.Branches.TryGetValue(value ?? string.Empty, out branch)) { Walk(results, context, branch, isFallbackPath); @@ -91,7 +91,7 @@ namespace Microsoft.AspNetCore.Routing.Internal // 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; + DecisionTreeNode branch; if (context.AmbientValues.TryGetValue(key, out value) && !criterion.Branches.Comparer.Equals(value, string.Empty)) { @@ -109,19 +109,19 @@ namespace Microsoft.AspNetCore.Routing.Internal } } - private class AttributeRouteLinkGenerationEntryClassifier : IClassifier + private class OutboundMatchClassifier : IClassifier { - public AttributeRouteLinkGenerationEntryClassifier() + public OutboundMatchClassifier() { ValueComparer = new RouteValueEqualityComparer(); } public IEqualityComparer ValueComparer { get; private set; } - public IDictionary GetCriteria(TreeRouteLinkGenerationEntry item) + public IDictionary GetCriteria(OutboundMatch item) { var results = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (var kvp in item.RequiredLinkValues) + foreach (var kvp in item.Entry.RequiredLinkValues) { results.Add(kvp.Key, new DecisionCriterionValue(kvp.Value ?? string.Empty)); } @@ -130,22 +130,22 @@ namespace Microsoft.AspNetCore.Routing.Internal } } - private class LinkGenerationMatchComparer : IComparer + private class OutboundMatchResultComparer : IComparer { - public static readonly LinkGenerationMatchComparer Instance = new LinkGenerationMatchComparer(); + public static readonly OutboundMatchResultComparer Instance = new OutboundMatchResultComparer(); - public int Compare(LinkGenerationMatch x, LinkGenerationMatch y) + public int Compare(OutboundMatchResult x, OutboundMatchResult y) { // For this comparison lower is better. - if (x.Entry.Order != y.Entry.Order) + if (x.Match.Entry.Order != y.Match.Entry.Order) { - return x.Entry.Order.CompareTo(y.Entry.Order); + return x.Match.Entry.Order.CompareTo(y.Match.Entry.Order); } - if (x.Entry.GenerationPrecedence != y.Entry.GenerationPrecedence) + if (x.Match.Entry.Precedence != y.Match.Entry.Precedence) { // Reversed because higher is better - return y.Entry.GenerationPrecedence.CompareTo(x.Entry.GenerationPrecedence); + return y.Match.Entry.Precedence.CompareTo(x.Match.Entry.Precedence); } if (x.IsFallbackMatch != y.IsFallbackMatch) @@ -154,7 +154,9 @@ namespace Microsoft.AspNetCore.Routing.Internal return x.IsFallbackMatch.CompareTo(y.IsFallbackMatch); } - return StringComparer.Ordinal.Compare(x.Entry.Template.TemplateText, y.Entry.Template.TemplateText); + return StringComparer.Ordinal.Compare( + x.Match.Entry.RouteTemplate.TemplateText, + y.Match.Entry.RouteTemplate.TemplateText); } } } diff --git a/src/Microsoft.AspNetCore.Routing/Internal/LinkGenerationMatch.cs b/src/Microsoft.AspNetCore.Routing/Internal/LinkGenerationMatch.cs deleted file mode 100644 index e2ab6ea73f..0000000000 --- a/src/Microsoft.AspNetCore.Routing/Internal/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.AspNetCore.Routing.Tree; - -namespace Microsoft.AspNetCore.Routing.Internal -{ - 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.AspNetCore.Routing/Internal/OutboundMatchResult.cs b/src/Microsoft.AspNetCore.Routing/Internal/OutboundMatchResult.cs new file mode 100644 index 0000000000..aee505f572 --- /dev/null +++ b/src/Microsoft.AspNetCore.Routing/Internal/OutboundMatchResult.cs @@ -0,0 +1,20 @@ +// 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.AspNetCore.Routing.Tree; + +namespace Microsoft.AspNetCore.Routing.Internal +{ + public struct OutboundMatchResult + { + public OutboundMatchResult(OutboundMatch match, bool isFallbackMatch) + { + Match = match; + IsFallbackMatch = isFallbackMatch; + } + + public OutboundMatch Match { get; } + + public bool IsFallbackMatch { get; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Routing/Template/RoutePrecedence.cs b/src/Microsoft.AspNetCore.Routing/Template/RoutePrecedence.cs index 1b506237bf..663f455923 100644 --- a/src/Microsoft.AspNetCore.Routing/Template/RoutePrecedence.cs +++ b/src/Microsoft.AspNetCore.Routing/Template/RoutePrecedence.cs @@ -8,7 +8,7 @@ using System.Linq; namespace Microsoft.AspNetCore.Routing.Template { /// - /// Computes precedence for an attribute route template. + /// Computes precedence for a route template. /// public static class RoutePrecedence { @@ -17,7 +17,7 @@ namespace Microsoft.AspNetCore.Routing.Template // /api/template/{id} == 1.13 // /api/{id:int} == 1.2 // /api/template/{id:int} == 1.12 - public static decimal ComputeMatched(RouteTemplate template) + public static decimal ComputeInbound(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). @@ -27,7 +27,7 @@ namespace Microsoft.AspNetCore.Routing.Template { var segment = template.Segments[i]; - var digit = ComputeMatchDigit(segment); + var digit = ComputeInboundPrecedenceDigit(segment); Debug.Assert(digit >= 0 && digit < 10); precedence += decimal.Divide(digit, (decimal)Math.Pow(10, i)); @@ -41,7 +41,7 @@ namespace Microsoft.AspNetCore.Routing.Template // /api/template/{id} == 5.53 // /api/{id:int} == 5.4 // /api/template/{id:int} == 5.54 - public static decimal ComputeGenerated(RouteTemplate template) + public static decimal ComputeOutbound(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). @@ -51,7 +51,7 @@ namespace Microsoft.AspNetCore.Routing.Template { var segment = template.Segments[i]; - var digit = ComputeGenerationDigit(segment); + var digit = ComputeOutboundPrecedenceDigit(segment); Debug.Assert(digit >= 0 && digit < 10); precedence += decimal.Divide(digit, (decimal)Math.Pow(10, i)); @@ -66,7 +66,7 @@ namespace Microsoft.AspNetCore.Routing.Template // 3 - Unconstrained parameter segements // 2 - Constrained wildcard parameter segments // 1 - Unconstrained wildcard parameter segments - private static int ComputeGenerationDigit(TemplateSegment segment) + private static int ComputeOutboundPrecedenceDigit(TemplateSegment segment) { if(segment.Parts.Count > 1) { @@ -98,7 +98,7 @@ namespace Microsoft.AspNetCore.Routing.Template // 3 - Unconstrained parameter segments // 4 - Constrained wildcard parameter segments // 5 - Unconstrained wildcard parameter segments - private static int ComputeMatchDigit(TemplateSegment segment) + private static int ComputeInboundPrecedenceDigit(TemplateSegment segment) { if (segment.Parts.Count > 1) { diff --git a/src/Microsoft.AspNetCore.Routing/Tree/InboundMatch.cs b/src/Microsoft.AspNetCore.Routing/Tree/InboundMatch.cs new file mode 100644 index 0000000000..5c731286a6 --- /dev/null +++ b/src/Microsoft.AspNetCore.Routing/Tree/InboundMatch.cs @@ -0,0 +1,14 @@ +// 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.AspNetCore.Routing.Template; + +namespace Microsoft.AspNetCore.Routing.Tree +{ + public class InboundMatch + { + public InboundRouteEntry Entry { get; set; } + + public TemplateMatcher TemplateMatcher { get; set; } + } +} diff --git a/src/Microsoft.AspNetCore.Routing/Tree/InboundRouteEntry.cs b/src/Microsoft.AspNetCore.Routing/Tree/InboundRouteEntry.cs new file mode 100644 index 0000000000..7c4a5f0abc --- /dev/null +++ b/src/Microsoft.AspNetCore.Routing/Tree/InboundRouteEntry.cs @@ -0,0 +1,56 @@ +// 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.AspNetCore.Routing.Template; + +namespace Microsoft.AspNetCore.Routing.Tree +{ + /// + /// Used to build an . Represents a URL template tha will be used to match incoming + /// request URLs. + /// + public class InboundRouteEntry + { + /// + /// Gets or sets the route constraints. + /// + public IDictionary Constraints { get; set; } + + /// + /// Gets or sets the route defaults. + /// + public RouteValueDictionary Defaults { get; set; } + + /// + /// Gets or sets the to invoke when this entry matches. + /// + public IRouter Handler { get; set; } + + /// + /// Gets or sets the order of the entry. + /// + /// + /// Entries are ordered first by (ascending) then by (descending). + /// + public int Order { get; set; } + + /// + /// Gets or sets the precedence of the entry. + /// + /// + /// Entries are ordered first by (ascending) then by (descending). + /// + public decimal Precedence { get; set; } + + /// + /// Gets or sets the name of the route. + /// + public string RouteName { get; set; } + + /// + /// Gets or sets the . + /// + public RouteTemplate RouteTemplate { get; set; } + } +} diff --git a/src/Microsoft.AspNetCore.Routing/Tree/OutboundMatch.cs b/src/Microsoft.AspNetCore.Routing/Tree/OutboundMatch.cs new file mode 100644 index 0000000000..a00212fd8f --- /dev/null +++ b/src/Microsoft.AspNetCore.Routing/Tree/OutboundMatch.cs @@ -0,0 +1,14 @@ +// 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.AspNetCore.Routing.Template; + +namespace Microsoft.AspNetCore.Routing.Tree +{ + public class OutboundMatch + { + public OutboundRouteEntry Entry { get; set; } + + public TemplateBinder TemplateBinder { get; set; } + } +} diff --git a/src/Microsoft.AspNetCore.Routing/Tree/TreeRouteLinkGenerationEntry.cs b/src/Microsoft.AspNetCore.Routing/Tree/OutboundRouteEntry.cs similarity index 51% rename from src/Microsoft.AspNetCore.Routing/Tree/TreeRouteLinkGenerationEntry.cs rename to src/Microsoft.AspNetCore.Routing/Tree/OutboundRouteEntry.cs index 659283a773..2364c3f350 100644 --- a/src/Microsoft.AspNetCore.Routing/Tree/TreeRouteLinkGenerationEntry.cs +++ b/src/Microsoft.AspNetCore.Routing/Tree/OutboundRouteEntry.cs @@ -7,16 +7,11 @@ using Microsoft.AspNetCore.Routing.Template; namespace Microsoft.AspNetCore.Routing.Tree { /// - /// Used to build a . Represents an individual URL-generating route that will be - /// aggregated into the . + /// Used to build a . Represents a URL template that will be used to generate + /// outgoing URLs. /// - public class TreeRouteLinkGenerationEntry + public class OutboundRouteEntry { - /// - /// Gets or sets the . - /// - public TemplateBinder Binder { get; set; } - /// /// Gets or sets the route constraints. /// @@ -25,37 +20,43 @@ namespace Microsoft.AspNetCore.Routing.Tree /// /// Gets or sets the route defaults. /// - public IDictionary Defaults { get; set; } + public RouteValueDictionary Defaults { get; set; } /// - /// Gets or sets the order of the template. + /// The to invoke when this entry matches. /// + public IRouter Handler { get; set; } + + /// + /// Gets or sets the order of the entry. + /// + /// + /// Entries are ordered first by (ascending) then by (descending). + /// public int Order { get; set; } /// /// Gets or sets the precedence of the template for link generation. A greater value of - /// means that an entry is considered first. + /// means that an entry is considered first. /// - public decimal GenerationPrecedence { get; set; } + /// + /// Entries are ordered first by (ascending) then by (descending). + /// + public decimal Precedence { get; set; } /// /// Gets or sets the name of the route. /// - public string Name { get; set; } - - /// - /// Gets or sets the route group. - /// - public string RouteGroup { get; set; } + public string RouteName { get; set; } /// /// Gets or sets the set of values that must be present for link genration. /// - public IDictionary RequiredLinkValues { get; set; } + public RouteValueDictionary RequiredLinkValues { get; set; } /// - /// Gets or sets the . + /// Gets or sets the . /// - public RouteTemplate Template { get; set; } + public RouteTemplate RouteTemplate { get; set; } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Routing/Tree/TreeRouteBuilder.cs b/src/Microsoft.AspNetCore.Routing/Tree/TreeRouteBuilder.cs index b1c515b6c6..d6e8d58e26 100644 --- a/src/Microsoft.AspNetCore.Routing/Tree/TreeRouteBuilder.cs +++ b/src/Microsoft.AspNetCore.Routing/Tree/TreeRouteBuilder.cs @@ -1,47 +1,195 @@ // 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 System.Text.Encodings.Web; +using Microsoft.AspNetCore.Routing.Internal; +using Microsoft.AspNetCore.Routing.Template; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.ObjectPool; namespace Microsoft.AspNetCore.Routing.Tree { public class TreeRouteBuilder { - private readonly IRouter _target; - private readonly List _generatingEntries; - private readonly List _matchingEntries; - private readonly ILogger _logger; private readonly ILogger _constraintLogger; + private readonly UrlEncoder _urlEncoder; + private readonly ObjectPool _objectPool; + private readonly IInlineConstraintResolver _constraintResolver; - public TreeRouteBuilder(IRouter target, ILoggerFactory loggerFactory) + public TreeRouteBuilder( + ILoggerFactory loggerFactory, + UrlEncoder urlEncoder, + ObjectPool objectPool, + IInlineConstraintResolver constraintResolver) { - _target = target; - _generatingEntries = new List(); - _matchingEntries = new List(); + if (loggerFactory == null) + { + throw new ArgumentNullException(nameof(loggerFactory)); + } + + if (urlEncoder == null) + { + throw new ArgumentNullException(nameof(urlEncoder)); + } + + if (objectPool == null) + { + throw new ArgumentNullException(nameof(objectPool)); + } + + if (constraintResolver == null) + { + throw new ArgumentNullException(nameof(constraintResolver)); + } + + _urlEncoder = urlEncoder; + _objectPool = objectPool; + _constraintResolver = constraintResolver; _logger = loggerFactory.CreateLogger(); _constraintLogger = loggerFactory.CreateLogger(typeof(RouteConstraintMatcher).FullName); } - public void Add(TreeRouteLinkGenerationEntry entry) + public InboundRouteEntry MapInbound( + IRouter handler, + RouteTemplate routeTemplate, + string routeName, + int order) { - _generatingEntries.Add(entry); + if (handler == null) + { + throw new ArgumentNullException(nameof(handler)); + } + + if (routeTemplate == null) + { + throw new ArgumentNullException(nameof(routeTemplate)); + } + + var entry = new InboundRouteEntry() + { + Handler = handler, + Order = order, + Precedence = RoutePrecedence.ComputeInbound(routeTemplate), + RouteName = routeName, + RouteTemplate = routeTemplate, + }; + + var constraintBuilder = new RouteConstraintBuilder(_constraintResolver, routeTemplate.TemplateText); + foreach (var parameter in routeTemplate.Parameters) + { + if (parameter.InlineConstraints != null) + { + if (parameter.IsOptional) + { + constraintBuilder.SetOptional(parameter.Name); + } + + foreach (var constraint in parameter.InlineConstraints) + { + constraintBuilder.AddResolvedConstraint(parameter.Name, constraint.Constraint); + } + } + } + + entry.Constraints = constraintBuilder.Build(); + + entry.Defaults = new RouteValueDictionary(); + foreach (var parameter in entry.RouteTemplate.Parameters) + { + if (parameter.DefaultValue != null) + { + entry.Defaults.Add(parameter.Name, parameter.DefaultValue); + } + } + + InboundEntries.Add(entry); + return entry; } - public void Add(TreeRouteMatchingEntry entry) + public OutboundRouteEntry MapOutbound( + IRouter handler, + RouteTemplate routeTemplate, + RouteValueDictionary requiredLinkValues, + string routeName, + int order) { - _matchingEntries.Add(entry); + if (handler == null) + { + throw new ArgumentNullException(nameof(handler)); + } + + if (routeTemplate == null) + { + throw new ArgumentNullException(nameof(routeTemplate)); + } + + if (requiredLinkValues == null) + { + throw new ArgumentNullException(nameof(requiredLinkValues)); + } + + var entry = new OutboundRouteEntry() + { + Handler = handler, + Order = order, + Precedence = RoutePrecedence.ComputeOutbound(routeTemplate), + RequiredLinkValues = requiredLinkValues, + RouteName = routeName, + RouteTemplate = routeTemplate, + }; + + var constraintBuilder = new RouteConstraintBuilder(_constraintResolver, routeTemplate.TemplateText); + foreach (var parameter in routeTemplate.Parameters) + { + if (parameter.InlineConstraints != null) + { + if (parameter.IsOptional) + { + constraintBuilder.SetOptional(parameter.Name); + } + + foreach (var constraint in parameter.InlineConstraints) + { + constraintBuilder.AddResolvedConstraint(parameter.Name, constraint.Constraint); + } + } + } + + entry.Constraints = constraintBuilder.Build(); + + entry.Defaults = new RouteValueDictionary(); + foreach (var parameter in entry.RouteTemplate.Parameters) + { + if (parameter.DefaultValue != null) + { + entry.Defaults.Add(parameter.Name, parameter.DefaultValue); + } + } + + OutboundEntries.Add(entry); + return entry; + } + + public IList InboundEntries { get; } = new List(); + + public IList OutboundEntries { get; } = new List(); + + public TreeRouter Build() + { + return Build(version: 0); } public TreeRouter Build(int version) { var trees = new Dictionary(); - foreach (var entry in _matchingEntries) + foreach (var entry in InboundEntries) { UrlMatchingTree tree; if (!trees.TryGetValue(entry.Order, out tree)) @@ -54,9 +202,10 @@ namespace Microsoft.AspNetCore.Routing.Tree } return new TreeRouter( - _target, trees.Values.OrderBy(tree => tree.Order).ToArray(), - _generatingEntries, + OutboundEntries, + _urlEncoder, + _objectPool, _logger, _constraintLogger, version); @@ -64,14 +213,16 @@ namespace Microsoft.AspNetCore.Routing.Tree public void Clear() { - _generatingEntries.Clear(); - _matchingEntries.Clear(); + InboundEntries.Clear(); + OutboundEntries.Clear(); } - private void AddEntryToTree(UrlMatchingTree tree, TreeRouteMatchingEntry entry) + private void AddEntryToTree(UrlMatchingTree tree, InboundRouteEntry entry) { var current = tree.Root; + var matcher = new TemplateMatcher(entry.RouteTemplate, entry.Defaults); + for (var i = 0; i < entry.RouteTemplate.Segments.Count; i++) { var segment = entry.RouteTemplate.Segments[i]; @@ -104,7 +255,7 @@ namespace Microsoft.AspNetCore.Routing.Tree if (part.IsParameter && (part.IsOptional || part.IsCatchAll)) { - current.Matches.Add(entry); + current.Matches.Add(new InboundMatch() { Entry = entry, TemplateMatcher = matcher }); } if (part.IsParameter && part.InlineConstraints.Any() && !part.IsCatchAll) @@ -154,11 +305,11 @@ namespace Microsoft.AspNetCore.Routing.Tree Debug.Fail("We shouldn't get here."); } - current.Matches.Add(entry); + current.Matches.Add(new InboundMatch() { Entry = entry, TemplateMatcher = matcher }); current.Matches.Sort((x, y) => { - var result = x.Precedence.CompareTo(y.Precedence); - return result == 0 ? x.RouteTemplate.TemplateText.CompareTo(y.RouteTemplate.TemplateText) : result; + var result = x.Entry.Precedence.CompareTo(y.Entry.Precedence); + return result == 0 ? x.Entry.RouteTemplate.TemplateText.CompareTo(y.Entry.RouteTemplate.TemplateText) : result; }); } } diff --git a/src/Microsoft.AspNetCore.Routing/Tree/TreeRouteMatchingEntry.cs b/src/Microsoft.AspNetCore.Routing/Tree/TreeRouteMatchingEntry.cs deleted file mode 100644 index ad3ecffbae..0000000000 --- a/src/Microsoft.AspNetCore.Routing/Tree/TreeRouteMatchingEntry.cs +++ /dev/null @@ -1,50 +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.AspNetCore.Routing.Template; - -namespace Microsoft.AspNetCore.Routing.Tree -{ - /// - /// 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; } - - /// - /// The to invoke when this entry matches. - /// - public IRouter Target { get; set; } - - /// - /// The name of the route. - /// - public string RouteName { get; set; } - - /// - /// The . - /// - public RouteTemplate RouteTemplate { get; set; } - - /// - /// The . - /// - public TemplateMatcher TemplateMatcher { get; set; } - - /// - /// The route constraints. - /// - public IDictionary Constraints { get; set; } - } -} diff --git a/src/Microsoft.AspNetCore.Routing/Tree/TreeRouter.cs b/src/Microsoft.AspNetCore.Routing/Tree/TreeRouter.cs index 0138fbf193..310161e3ce 100644 --- a/src/Microsoft.AspNetCore.Routing/Tree/TreeRouter.cs +++ b/src/Microsoft.AspNetCore.Routing/Tree/TreeRouter.cs @@ -4,12 +4,13 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Linq; +using System.Text.Encodings.Web; using System.Threading.Tasks; using Microsoft.AspNetCore.Routing.Internal; using Microsoft.AspNetCore.Routing.Logging; -using Microsoft.Extensions.Internal; +using Microsoft.AspNetCore.Routing.Template; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.ObjectPool; namespace Microsoft.AspNetCore.Routing.Tree { @@ -22,10 +23,9 @@ namespace Microsoft.AspNetCore.Routing.Tree // group of action descriptors. public static readonly string RouteGroupKey = "!__route_group"; - private readonly IRouter _next; private readonly LinkGenerationDecisionTree _linkGenerationTree; private readonly UrlMatchingTree[] _trees; - private readonly IDictionary _namedEntries; + private readonly IDictionary _namedEntries; private readonly ILogger _logger; private readonly ILogger _constraintLogger; @@ -33,26 +33,23 @@ namespace Microsoft.AspNetCore.Routing.Tree /// /// Creates a new . /// - /// The next router. Invoked when a route entry matches. /// The list of that contains the route entries. - /// The set of . + /// The set of . + /// The . + /// The . /// The instance. /// The instance used /// in . /// The version of this route. public TreeRouter( - IRouter next, UrlMatchingTree[] trees, - IEnumerable linkGenerationEntries, + IEnumerable linkGenerationEntries, + UrlEncoder urlEncoder, + ObjectPool objectPool, ILogger routeLogger, ILogger constraintLogger, int version) { - if (next == null) - { - throw new ArgumentNullException(nameof(next)); - } - if (trees == null) { throw new ArgumentNullException(nameof(trees)); @@ -63,6 +60,16 @@ namespace Microsoft.AspNetCore.Routing.Tree throw new ArgumentNullException(nameof(linkGenerationEntries)); } + if (urlEncoder == null) + { + throw new ArgumentNullException(nameof(urlEncoder)); + } + + if (objectPool == null) + { + throw new ArgumentNullException(nameof(objectPool)); + } + if (routeLogger == null) { throw new ArgumentNullException(nameof(routeLogger)); @@ -73,43 +80,49 @@ namespace Microsoft.AspNetCore.Routing.Tree throw new ArgumentNullException(nameof(constraintLogger)); } - _next = next; _trees = trees; _logger = routeLogger; _constraintLogger = constraintLogger; - var namedEntries = new Dictionary( - StringComparer.OrdinalIgnoreCase); + _namedEntries = new Dictionary(StringComparer.OrdinalIgnoreCase); + + var outboundMatches = new List(); foreach (var entry in linkGenerationEntries) { + + var binder = new TemplateBinder(urlEncoder, objectPool, entry.RouteTemplate, entry.Defaults); + var outboundMatch = new OutboundMatch() { Entry = entry, TemplateBinder = binder }; + outboundMatches.Add(outboundMatch); + // Skip unnamed entries - if (entry.Name == null) + if (entry.RouteName == null) { continue; } - // We only need to keep one AttributeRouteLinkGenerationEntry per route template + // We only need to keep one OutboundMatch 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.Template.TemplateText.Equals(entry.Template.TemplateText, StringComparison.OrdinalIgnoreCase)) + OutboundMatch namedMatch; + if (_namedEntries.TryGetValue(entry.RouteName, out namedMatch) && + !string.Equals( + namedMatch.Entry.RouteTemplate.TemplateText, + entry.RouteTemplate.TemplateText, + StringComparison.OrdinalIgnoreCase)) { throw new ArgumentException( - Resources.FormatAttributeRoute_DifferentLinkGenerationEntries_SameName(entry.Name), + Resources.FormatAttributeRoute_DifferentLinkGenerationEntries_SameName(entry.RouteName), nameof(linkGenerationEntries)); } - else if (namedEntry == null) + else if (namedMatch == null) { - namedEntries.Add(entry.Name, entry); + _namedEntries.Add(entry.RouteName, outboundMatch); } } - _namedEntries = namedEntries; - // The decision tree will take care of ordering for these entries. - _linkGenerationTree = new LinkGenerationDecisionTree(linkGenerationEntries.ToArray()); + _linkGenerationTree = new LinkGenerationDecisionTree(outboundMatches.ToArray()); Version = version; } @@ -145,7 +158,7 @@ namespace Microsoft.AspNetCore.Routing.Tree for (var i = 0; i < matches.Count; i++) { - var path = GenerateVirtualPath(context, matches[i].Entry); + var path = GenerateVirtualPath(context, matches[i].Match.Entry, matches[i].Match.TemplateBinder); if (path != null) { return path; @@ -175,7 +188,9 @@ namespace Microsoft.AspNetCore.Routing.Tree var node = treeEnumerator.Current; foreach (var item in node.Matches) { - if (!item.TemplateMatcher.TryMatch(context.HttpContext.Request.Path, context.RouteData.Values)) + var entry = item.Entry; + var matcher = item.TemplateMatcher; + if (!matcher.TryMatch(context.HttpContext.Request.Path, context.RouteData.Values)) { continue; } @@ -183,7 +198,7 @@ namespace Microsoft.AspNetCore.Routing.Tree try { if (!RouteConstraintMatcher.Match( - item.Constraints, + entry.Constraints, context.RouteData.Values, context.HttpContext, this, @@ -193,10 +208,10 @@ namespace Microsoft.AspNetCore.Routing.Tree continue; } - _logger.MatchedRoute(item.RouteName, item.RouteTemplate.TemplateText); + _logger.MatchedRoute(entry.RouteName, entry.RouteTemplate.TemplateText); + context.RouteData.Routers.Add(entry.Handler); - context.RouteData.Routers.Add(item.Target); - await item.Target.RouteAsync(context); + await entry.Handler.RouteAsync(context); if (context.Handler != null) { return; @@ -318,10 +333,10 @@ namespace Microsoft.AspNetCore.Routing.Tree private VirtualPathData GetVirtualPathForNamedRoute(VirtualPathContext context) { - TreeRouteLinkGenerationEntry entry; - if (_namedEntries.TryGetValue(context.RouteName, out entry)) + OutboundMatch match; + if (_namedEntries.TryGetValue(context.RouteName, out match)) { - var path = GenerateVirtualPath(context, entry); + var path = GenerateVirtualPath(context, match.Entry, match.TemplateBinder); if (path != null) { return path; @@ -330,7 +345,10 @@ namespace Microsoft.AspNetCore.Routing.Tree return null; } - private VirtualPathData GenerateVirtualPath(VirtualPathContext context, TreeRouteLinkGenerationEntry entry) + private VirtualPathData GenerateVirtualPath( + VirtualPathContext context, + OutboundRouteEntry entry, + TemplateBinder binder) { // 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 @@ -349,7 +367,7 @@ namespace Microsoft.AspNetCore.Routing.Tree { if (entry.RequiredLinkValues.ContainsKey(kvp.Key)) { - var parameter = entry.Template.GetParameter(kvp.Key); + var parameter = entry.RouteTemplate.GetParameter(kvp.Key); if (parameter == null) { @@ -360,7 +378,7 @@ namespace Microsoft.AspNetCore.Routing.Tree inputValues.Add(kvp.Key, kvp.Value); } - var bindingResult = entry.Binder.GetValues(context.AmbientValues, inputValues); + var bindingResult = binder.GetValues(context.AmbientValues, inputValues); if (bindingResult == null) { // A required parameter in the template didn't get a value. @@ -381,7 +399,7 @@ namespace Microsoft.AspNetCore.Routing.Tree return null; } - var pathData = _next.GetVirtualPath(context); + var pathData = entry.Handler.GetVirtualPath(context); if (pathData != null) { // If path is non-null then the target router short-circuited, we don't expect this @@ -389,7 +407,7 @@ namespace Microsoft.AspNetCore.Routing.Tree return pathData; } - var path = entry.Binder.BindValues(bindingResult.AcceptedValues); + var path = binder.BindValues(bindingResult.AcceptedValues); if (path == null) { return null; diff --git a/src/Microsoft.AspNetCore.Routing/Tree/UrlMatchingNode.cs b/src/Microsoft.AspNetCore.Routing/Tree/UrlMatchingNode.cs index 2e6ba9ed18..99b4b6e90d 100644 --- a/src/Microsoft.AspNetCore.Routing/Tree/UrlMatchingNode.cs +++ b/src/Microsoft.AspNetCore.Routing/Tree/UrlMatchingNode.cs @@ -12,7 +12,7 @@ namespace Microsoft.AspNetCore.Routing.Tree { Length = length; - Matches = new List(); + Matches = new List(); Literals = new Dictionary(StringComparer.OrdinalIgnoreCase); } @@ -21,7 +21,7 @@ namespace Microsoft.AspNetCore.Routing.Tree public bool IsCatchAll { get; set; } // These entries are sorted by precedence then template - public List Matches { get; } + public List Matches { get; } public Dictionary Literals { get; } diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Internal/LinkGenerationDecisionTreeTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/Internal/LinkGenerationDecisionTreeTest.cs index 8e21a291d0..8a35edcf13 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/Internal/LinkGenerationDecisionTreeTest.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/Internal/LinkGenerationDecisionTreeTest.cs @@ -16,9 +16,9 @@ namespace Microsoft.AspNetCore.Routing.Internal.Routing public void SelectSingleEntry_NoCriteria() { // Arrange - var entries = new List(); + var entries = new List(); - var entry = CreateEntry(new { }); + var entry = CreateMatch(new { }); entries.Add(entry); var tree = new LinkGenerationDecisionTree(entries); @@ -29,16 +29,16 @@ namespace Microsoft.AspNetCore.Routing.Internal.Routing var matches = tree.GetMatches(context); // Assert - Assert.Same(entry, Assert.Single(matches).Entry); + Assert.Same(entry, Assert.Single(matches).Match); } [Fact] public void SelectSingleEntry_MultipleCriteria() { // Arrange - var entries = new List(); + var entries = new List(); - var entry = CreateEntry(new { controller = "Store", action = "Buy" }); + var entry = CreateMatch(new { controller = "Store", action = "Buy" }); entries.Add(entry); var tree = new LinkGenerationDecisionTree(entries); @@ -49,16 +49,16 @@ namespace Microsoft.AspNetCore.Routing.Internal.Routing var matches = tree.GetMatches(context); // Assert - Assert.Same(entry, Assert.Single(matches).Entry); + Assert.Same(entry, Assert.Single(matches).Match); } [Fact] public void SelectSingleEntry_MultipleCriteria_AmbientValues() { // Arrange - var entries = new List(); + var entries = new List(); - var entry = CreateEntry(new { controller = "Store", action = "Buy" }); + var entry = CreateMatch(new { controller = "Store", action = "Buy" }); entries.Add(entry); var tree = new LinkGenerationDecisionTree(entries); @@ -70,7 +70,7 @@ namespace Microsoft.AspNetCore.Routing.Internal.Routing // Assert var match = Assert.Single(matches); - Assert.Same(entry, match.Entry); + Assert.Same(entry, match.Match); Assert.False(match.IsFallbackMatch); } @@ -78,9 +78,9 @@ namespace Microsoft.AspNetCore.Routing.Internal.Routing public void SelectSingleEntry_MultipleCriteria_Replaced() { // Arrange - var entries = new List(); + var entries = new List(); - var entry = CreateEntry(new { controller = "Store", action = "Buy" }); + var entry = CreateMatch(new { controller = "Store", action = "Buy" }); entries.Add(entry); var tree = new LinkGenerationDecisionTree(entries); @@ -94,7 +94,7 @@ namespace Microsoft.AspNetCore.Routing.Internal.Routing // Assert var match = Assert.Single(matches); - Assert.Same(entry, match.Entry); + Assert.Same(entry, match.Match); Assert.False(match.IsFallbackMatch); } @@ -102,9 +102,9 @@ namespace Microsoft.AspNetCore.Routing.Internal.Routing public void SelectSingleEntry_MultipleCriteria_AmbientValue_Ignored() { // Arrange - var entries = new List(); + var entries = new List(); - var entry = CreateEntry(new { controller = "Store", action = (string)null }); + var entry = CreateMatch(new { controller = "Store", action = (string)null }); entries.Add(entry); var tree = new LinkGenerationDecisionTree(entries); @@ -118,7 +118,7 @@ namespace Microsoft.AspNetCore.Routing.Internal.Routing // Assert var match = Assert.Single(matches); - Assert.Same(entry, match.Entry); + Assert.Same(entry, match.Match); Assert.True(match.IsFallbackMatch); } @@ -126,9 +126,9 @@ namespace Microsoft.AspNetCore.Routing.Internal.Routing public void SelectSingleEntry_MultipleCriteria_NoMatch() { // Arrange - var entries = new List(); + var entries = new List(); - var entry = CreateEntry(new { controller = "Store", action = "Buy" }); + var entry = CreateMatch(new { controller = "Store", action = "Buy" }); entries.Add(entry); var tree = new LinkGenerationDecisionTree(entries); @@ -146,9 +146,9 @@ namespace Microsoft.AspNetCore.Routing.Internal.Routing public void SelectSingleEntry_MultipleCriteria_AmbientValue_NoMatch() { // Arrange - var entries = new List(); + var entries = new List(); - var entry = CreateEntry(new { controller = "Store", action = "Buy" }); + var entry = CreateMatch(new { controller = "Store", action = "Buy" }); entries.Add(entry); var tree = new LinkGenerationDecisionTree(entries); @@ -168,12 +168,12 @@ namespace Microsoft.AspNetCore.Routing.Internal.Routing public void SelectMultipleEntries_OneDoesntMatch() { // Arrange - var entries = new List(); + var entries = new List(); - var entry1 = CreateEntry(new { controller = "Store", action = "Buy" }); + var entry1 = CreateMatch(new { controller = "Store", action = "Buy" }); entries.Add(entry1); - var entry2 = CreateEntry(new { controller = "Store", action = "Cart" }); + var entry2 = CreateMatch(new { controller = "Store", action = "Cart" }); entries.Add(entry2); var tree = new LinkGenerationDecisionTree(entries); @@ -186,20 +186,20 @@ namespace Microsoft.AspNetCore.Routing.Internal.Routing var matches = tree.GetMatches(context); // Assert - Assert.Same(entry1, Assert.Single(matches).Entry); + Assert.Same(entry1, Assert.Single(matches).Match); } [Fact] public void SelectMultipleEntries_BothMatch_CriteriaSubset() { // Arrange - var entries = new List(); + var entries = new List(); - var entry1 = CreateEntry(new { controller = "Store", action = "Buy" }); + var entry1 = CreateMatch(new { controller = "Store", action = "Buy" }); entries.Add(entry1); - var entry2 = CreateEntry(new { controller = "Store" }); - entry2.Order = 1; + var entry2 = CreateMatch(new { controller = "Store" }); + entry2.Entry.Order = 1; entries.Add(entry2); var tree = new LinkGenerationDecisionTree(entries); @@ -209,7 +209,7 @@ namespace Microsoft.AspNetCore.Routing.Internal.Routing ambientValues: new { controller = "Store", action = "Buy" }); // Act - var matches = tree.GetMatches(context).Select(m => m.Entry).ToList(); + var matches = tree.GetMatches(context).Select(m => m.Match).ToList(); // Assert Assert.Equal(entries, matches); @@ -219,13 +219,13 @@ namespace Microsoft.AspNetCore.Routing.Internal.Routing public void SelectMultipleEntries_BothMatch_NonOverlappingCriteria() { // Arrange - var entries = new List(); + var entries = new List(); - var entry1 = CreateEntry(new { controller = "Store", action = "Buy" }); + var entry1 = CreateMatch(new { controller = "Store", action = "Buy" }); entries.Add(entry1); - var entry2 = CreateEntry(new { slug = "1234" }); - entry2.Order = 1; + var entry2 = CreateMatch(new { slug = "1234" }); + entry2.Entry.Order = 1; entries.Add(entry2); var tree = new LinkGenerationDecisionTree(entries); @@ -233,7 +233,7 @@ namespace Microsoft.AspNetCore.Routing.Internal.Routing var context = CreateContext(new { controller = "Store", action = "Buy", slug = "1234" }); // Act - var matches = tree.GetMatches(context).Select(m => m.Entry).ToList(); + var matches = tree.GetMatches(context).Select(m => m.Match).ToList(); // Assert Assert.Equal(entries, matches); @@ -244,15 +244,15 @@ namespace Microsoft.AspNetCore.Routing.Internal.Routing public void SelectMultipleEntries_BothMatch_OrderedByOrder() { // Arrange - var entries = new List(); + var entries = new List(); - var entry1 = CreateEntry(new { controller = "Store", action = "Buy" }); - entry1.GenerationPrecedence = 0; + var entry1 = CreateMatch(new { controller = "Store", action = "Buy" }); + entry1.Entry.Precedence = 0; entries.Add(entry1); - var entry2 = CreateEntry(new { controller = "Store", action = "Buy" }); - entry2.Order = 1; - entry2.GenerationPrecedence = 1; + var entry2 = CreateMatch(new { controller = "Store", action = "Buy" }); + entry2.Entry.Order = 1; + entry2.Entry.Precedence = 1; entries.Add(entry2); var tree = new LinkGenerationDecisionTree(entries); @@ -260,7 +260,7 @@ namespace Microsoft.AspNetCore.Routing.Internal.Routing var context = CreateContext(new { controller = "Store", action = "Buy" }); // Act - var matches = tree.GetMatches(context).Select(m => m.Entry).ToList(); + var matches = tree.GetMatches(context).Select(m => m.Match).ToList(); // Assert Assert.Equal(entries, matches); @@ -271,14 +271,14 @@ namespace Microsoft.AspNetCore.Routing.Internal.Routing public void SelectMultipleEntries_BothMatch_OrderedByPrecedence() { // Arrange - var entries = new List(); + var entries = new List(); - var entry1 = CreateEntry(new { controller = "Store", action = "Buy" }); - entry1.GenerationPrecedence = 1; + var entry1 = CreateMatch(new { controller = "Store", action = "Buy" }); + entry1.Entry.Precedence = 1; entries.Add(entry1); - var entry2 = CreateEntry(new { controller = "Store", action = "Buy" }); - entry2.GenerationPrecedence = 0; + var entry2 = CreateMatch(new { controller = "Store", action = "Buy" }); + entry2.Entry.Precedence = 0; entries.Add(entry2); var tree = new LinkGenerationDecisionTree(entries); @@ -286,7 +286,7 @@ namespace Microsoft.AspNetCore.Routing.Internal.Routing var context = CreateContext(new { controller = "Store", action = "Buy" }); // Act - var matches = tree.GetMatches(context).Select(m => m.Entry).ToList(); + var matches = tree.GetMatches(context).Select(m => m.Match).ToList(); // Assert Assert.Equal(entries, matches); @@ -297,14 +297,14 @@ namespace Microsoft.AspNetCore.Routing.Internal.Routing public void SelectMultipleEntries_BothMatch_OrderedByTemplate() { // Arrange - var entries = new List(); + var entries = new List(); - var entry1 = CreateEntry(new { controller = "Store", action = "Buy" }); - entry1.Template = TemplateParser.Parse("a"); + var entry1 = CreateMatch(new { controller = "Store", action = "Buy" }); + entry1.Entry.RouteTemplate = TemplateParser.Parse("a"); entries.Add(entry1); - var entry2 = CreateEntry(new { controller = "Store", action = "Buy" }); - entry2.Template = TemplateParser.Parse("b"); + var entry2 = CreateMatch(new { controller = "Store", action = "Buy" }); + entry2.Entry.RouteTemplate = TemplateParser.Parse("b"); entries.Add(entry2); var tree = new LinkGenerationDecisionTree(entries); @@ -312,17 +312,18 @@ namespace Microsoft.AspNetCore.Routing.Internal.Routing var context = CreateContext(new { controller = "Store", action = "Buy" }); // Act - var matches = tree.GetMatches(context).Select(m => m.Entry).ToList(); + var matches = tree.GetMatches(context).Select(m => m.Match).ToList(); // Assert Assert.Equal(entries, matches); } - private TreeRouteLinkGenerationEntry CreateEntry(object requiredValues) + private OutboundMatch CreateMatch(object requiredValues) { - var entry = new TreeRouteLinkGenerationEntry(); - entry.RequiredLinkValues = new RouteValueDictionary(requiredValues); - return entry; + var match = new OutboundMatch(); + match.Entry = new OutboundRouteEntry(); + match.Entry.RequiredLinkValues = new RouteValueDictionary(requiredValues); + return match; } private VirtualPathContext CreateContext(object values, object ambientValues = null) diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Template/RoutePrecedenceTests.cs b/test/Microsoft.AspNetCore.Routing.Tests/Template/RoutePrecedenceTests.cs index 936b1df3d3..7dd04454e2 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/Template/RoutePrecedenceTests.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/Template/RoutePrecedenceTests.cs @@ -102,11 +102,11 @@ namespace Microsoft.AspNetCore.Routing.Template private static decimal ComputeMatched(string template) { - return Compute(template, RoutePrecedence.ComputeMatched); + return Compute(template, RoutePrecedence.ComputeInbound); } private static decimal ComputeGenerated(string template) { - return Compute(template, RoutePrecedence.ComputeGenerated); + return Compute(template, RoutePrecedence.ComputeOutbound); } private static decimal Compute(string template, Func func) diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Tree/TreeRouteBuilderTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/Tree/TreeRouteBuilderTest.cs new file mode 100644 index 0000000000..a1ba058d17 --- /dev/null +++ b/test/Microsoft.AspNetCore.Routing.Tests/Tree/TreeRouteBuilderTest.cs @@ -0,0 +1,88 @@ +// 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.Text.Encodings.Web; +using Microsoft.AspNetCore.Routing.Internal; +using Microsoft.AspNetCore.Routing.Template; +using Microsoft.AspNetCore.Testing; +using Microsoft.Extensions.Logging.Testing; +using Microsoft.Extensions.ObjectPool; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Routing.Tree +{ + public class TreeRouteBuilderTest + { + [Fact] + public void TreeRouter_BuildThrows_RoutesWithTheSameNameAndDifferentTemplates() + { + // Arrange + var builder = CreateBuilder(); + + var message = "Two or more routes named 'Get_Products' have different templates."; + + builder.MapOutbound( + Mock.Of(), + TemplateParser.Parse("api/Products"), + new RouteValueDictionary(), + "Get_Products", + order: 0); + + builder.MapOutbound( + Mock.Of(), + TemplateParser.Parse("Products/Index"), + new RouteValueDictionary(), + "Get_Products", + order: 0); + + // Act & Assert + ExceptionAssert.ThrowsArgument(() => + { + builder.Build(); + }, "linkGenerationEntries", message); + } + + [Fact] + public void TreeRouter_BuildDoesNotThrow_RoutesWithTheSameNameAndSameTemplates() + { + // Arrange + var builder = CreateBuilder(); + + builder.MapOutbound( + Mock.Of(), + TemplateParser.Parse("api/Products"), + new RouteValueDictionary(), + "Get_Products", + order: 0); + + builder.MapOutbound( + Mock.Of(), + TemplateParser.Parse("api/products"), + new RouteValueDictionary(), + "Get_Products", + order: 0); + + // Act & Assert (does not throw) + builder.Build(); + } + + private static TreeRouteBuilder CreateBuilder() + { + var objectPoolProvider = new DefaultObjectPoolProvider(); + var objectPolicy = new UriBuilderContextPooledObjectPolicy(UrlEncoder.Default); + var objectPool = objectPoolProvider.Create(objectPolicy); + + var constraintResolver = Mock.Of(); + var builder = new TreeRouteBuilder( + NullLoggerFactory.Instance, + UrlEncoder.Default, + objectPool, + constraintResolver); + return builder; + } + } +} diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Tree/TreeRouterTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/Tree/TreeRouterTest.cs index 8fb123f38b..e519ab5c04 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/Tree/TreeRouterTest.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/Tree/TreeRouterTest.cs @@ -44,25 +44,16 @@ namespace Microsoft.AspNetCore.Routing.Tree string secondTemplate) { // Arrange - var expectedRouteGroup = string.Format("{0}&&{1}", 0, firstTemplate); + var expectedRouteGroup = CreateRouteGroup(0, firstTemplate); - var next = new Mock(); - next - .Setup(r => r.RouteAsync(It.IsAny())) - .Callback(c => c.Handler = NullHandler) - .Returns(Task.FromResult(true)) - .Verifiable(); - - var firstRoute = CreateMatchingEntry(next.Object, firstTemplate, order: 0); - var secondRoute = CreateMatchingEntry(next.Object, secondTemplate, order: 0); + var builder = CreateBuilder(); // 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 }; + MapInboundEntry(builder, secondTemplate); + MapInboundEntry(builder, firstTemplate); - var linkGenerationEntries = Enumerable.Empty(); - - var route = CreateAttributeRoute(next.Object, matchingRoutes, linkGenerationEntries); + var route = builder.Build(); var context = CreateRouteContext("/template/5"); @@ -89,26 +80,17 @@ namespace Microsoft.AspNetCore.Routing.Tree string secondTemplate) { // Arrange - var expectedRouteGroup = string.Format("{0}&&{1}", 0, secondTemplate); + var expectedRouteGroup = CreateRouteGroup(0, secondTemplate); - var next = new Mock(); - next - .Setup(r => r.RouteAsync(It.IsAny())) - .Callback(c => c.Handler = NullHandler) - .Returns(Task.FromResult(true)) - .Verifiable(); - - var firstRoute = CreateMatchingEntry(next.Object, firstTemplate, order: 1); - var secondRoute = CreateMatchingEntry(next.Object, secondTemplate, order: 0); + var builder = CreateBuilder(); // 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 }; + MapInboundEntry(builder, firstTemplate, order: 1); + MapInboundEntry(builder, secondTemplate, order: 0); - var linkGenerationEntries = Enumerable.Empty(); - - var route = CreateAttributeRoute(next.Object, matchingRoutes, linkGenerationEntries); + var route = builder.Build(); var context = CreateRouteContext("/template/5"); @@ -131,21 +113,14 @@ namespace Microsoft.AspNetCore.Routing.Tree string expectedResult) { // Arrange - var next = new Mock(); - next - .Setup(r => r.RouteAsync(It.IsAny())) - .Callback(c => c.Handler = NullHandler) - .Returns(Task.FromResult(true)) - .Verifiable(); + var builder = CreateBuilder(); + MapInboundEntry(builder, template); + var route = builder.Build(); - var firstRoute = CreateMatchingEntry(next.Object, template, order: 0); - var matchingRoutes = new[] { firstRoute }; - var linkGenerationEntries = Enumerable.Empty(); - var attributeRoute = CreateAttributeRoute(next.Object, matchingRoutes, linkGenerationEntries); var context = CreateRouteContext(requestPath); // Act - await attributeRoute.RouteAsync(context); + await route.RouteAsync(context); // Assert Assert.NotNull(context.Handler); @@ -160,21 +135,14 @@ namespace Microsoft.AspNetCore.Routing.Tree string requestPath) { // Arrange - var next = new Mock(); - next - .Setup(r => r.RouteAsync(It.IsAny())) - .Callback(c => c.Handler = NullHandler) - .Returns(Task.FromResult(true)) - .Verifiable(); + var builder = CreateBuilder(); + MapInboundEntry(builder, template); + var route = builder.Build(); - var firstRoute = CreateMatchingEntry(next.Object, template, order: 0); - var matchingRoutes = new[] { firstRoute }; - var linkGenerationEntries = Enumerable.Empty(); - var attributeRoute = CreateAttributeRoute(next.Object, matchingRoutes, linkGenerationEntries); var context = CreateRouteContext(requestPath); // Act - await attributeRoute.RouteAsync(context); + await route.RouteAsync(context); // Assert Assert.NotNull(context.Handler); @@ -189,23 +157,15 @@ namespace Microsoft.AspNetCore.Routing.Tree string requestPath) { // Arrange - var next = new Mock(); - next - .Setup(r => r.RouteAsync(It.IsAny())) - .Callback(c => c.Handler = NullHandler) - .Returns(Task.FromResult(true)) - .Verifiable(); - - var firstRoute = CreateMatchingEntry(next.Object, template, order: 0); - var matchingRoutes = new[] { firstRoute }; - var linkGenerationEntries = Enumerable.Empty(); - var attributeRoute = CreateAttributeRoute(next.Object, matchingRoutes, linkGenerationEntries); + var builder = CreateBuilder(); + MapInboundEntry(builder, template); + var route = builder.Build(); var context = CreateRouteContext(requestPath); context.RouteData.Values["path"] = "existing-value"; // Act - await attributeRoute.RouteAsync(context); + await route.RouteAsync(context); // Assert Assert.NotNull(context.Handler); @@ -220,23 +180,15 @@ namespace Microsoft.AspNetCore.Routing.Tree string requestPath) { // Arrange - var next = new Mock(); - next - .Setup(r => r.RouteAsync(It.IsAny())) - .Callback(c => c.Handler = NullHandler) - .Returns(Task.FromResult(true)) - .Verifiable(); - - var firstRoute = CreateMatchingEntry(next.Object, template, order: 0); - var matchingRoutes = new[] { firstRoute }; - var linkGenerationEntries = Enumerable.Empty(); - var attributeRoute = CreateAttributeRoute(next.Object, matchingRoutes, linkGenerationEntries); + var builder = CreateBuilder(); + MapInboundEntry(builder, template); + var route = builder.Build(); var context = CreateRouteContext(requestPath); context.RouteData.Values["path"] = "existing-value"; // Act - await attributeRoute.RouteAsync(context); + await route.RouteAsync(context); // Assert Assert.NotNull(context.Handler); @@ -252,25 +204,16 @@ namespace Microsoft.AspNetCore.Routing.Tree public async Task TreeRouter_RouteAsync_RespectsOrder(string template) { // Arrange - var expectedRouteGroup = string.Format("{0}&&{1}", 0, template); + var expectedRouteGroup = CreateRouteGroup(0, template); - var next = new Mock(); - next - .Setup(r => r.RouteAsync(It.IsAny())) - .Callback(c => c.Handler = NullHandler) - .Returns(Task.FromResult(true)) - .Verifiable(); - - var firstRoute = CreateMatchingEntry(next.Object, template, order: 1); - var secondRoute = CreateMatchingEntry(next.Object, template, order: 0); + var builder = CreateBuilder(); // 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 }; + MapInboundEntry(builder, template, order: 1); + MapInboundEntry(builder, template, order: 0); - var linkGenerationEntries = Enumerable.Empty(); - - var route = CreateAttributeRoute(next.Object, matchingRoutes, linkGenerationEntries); + var route = builder.Build(); var context = CreateRouteContext("/template/5"); @@ -289,27 +232,16 @@ namespace Microsoft.AspNetCore.Routing.Tree public async Task TreeRouter_RouteAsync_EnsuresStableOrdering(string first, string second) { // Arrange - var expectedRouteGroup = string.Format("{0}&&{1}", 0, first); + var expectedRouteGroup = CreateRouteGroup(0, first); - var next = new Mock(); - next - .Setup(r => r.RouteAsync(It.IsAny())) - .Callback(c => c.Handler = NullHandler) - .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); + var builder = CreateBuilder(); // 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 }; + MapInboundEntry(builder, first); + MapInboundEntry(builder, second); - var linkGenerationEntries = Enumerable.Empty(); - - var route = CreateAttributeRoute(next.Object, matchingRoutes, linkGenerationEntries); + var route = builder.Build(); var context = CreateRouteContext("/template/5"); @@ -331,24 +263,11 @@ namespace Microsoft.AspNetCore.Routing.Tree bool expectedResult) { // Arrange - var expectedRouteGroup = string.Format("{0}&&{1}", 0, template); + var expectedRouteGroup = CreateRouteGroup(0, template); - var next = new Mock(); - next - .Setup(r => r.RouteAsync(It.IsAny())) - .Callback(c => c.Handler = NullHandler) - .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 builder = CreateBuilder(); + MapInboundEntry(builder, template); + var route = builder.Build(); var context = CreateRouteContext(request); @@ -394,22 +313,12 @@ namespace Microsoft.AspNetCore.Routing.Tree string p3) { // Arrange - var expectedRouteGroup = string.Format("{0}&&{1}", 0, template); + var expectedRouteGroup = CreateRouteGroup(0, template); - var next = new Mock(); - next - .Setup(r => r.RouteAsync(It.IsAny())) - .Callback(c => c.Handler = NullHandler) - .Returns(Task.FromResult(true)) - .Verifiable(); + var builder = CreateBuilder(); + MapInboundEntry(builder, template); + var route = builder.Build(); - 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 @@ -449,22 +358,11 @@ namespace Microsoft.AspNetCore.Routing.Tree string request) { // Arrange - var expectedRouteGroup = string.Format("{0}&&{1}", 0, template); + var expectedRouteGroup = CreateRouteGroup(0, template); - var next = new Mock(); - next - .Setup(r => r.RouteAsync(It.IsAny())) - .Callback(c => c.Handler = NullHandler) - .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 builder = CreateBuilder(); + MapInboundEntry(builder, template); + var route = builder.Build(); var context = CreateRouteContext(request); @@ -495,7 +393,7 @@ namespace Microsoft.AspNetCore.Routing.Tree {"id", 5 } }; - var route = CreateAttributeRoute(firstTemplate, secondTemplate); + var route = CreateTreeRouter(firstTemplate, secondTemplate); var context = CreateVirtualPathContext( values: values, ambientValues: null); @@ -517,7 +415,7 @@ namespace Microsoft.AspNetCore.Routing.Tree var firstTemplate = "template"; var secondTemplate = "template/{parameter:int=1003}"; - var route = CreateAttributeRoute(firstTemplate, secondTemplate); + var route = CreateTreeRouter(firstTemplate, secondTemplate); var context = CreateVirtualPathContext( values: null, ambientValues: null); @@ -543,7 +441,7 @@ namespace Microsoft.AspNetCore.Routing.Tree string expectedPath) { // Arrange - var route = CreateAttributeRoute(firstTemplate, secondTemplate); + var route = CreateTreeRouter(firstTemplate, secondTemplate); var parameter = 5; var id = 1234; var values = new Dictionary @@ -575,7 +473,7 @@ namespace Microsoft.AspNetCore.Routing.Tree string expectedPath) { // Arrange - var route = CreateAttributeRoute(firstTemplate, secondTemplate); + var route = CreateTreeRouter(firstTemplate, secondTemplate); var parameter = 5; var id = 1234; var values = new Dictionary @@ -611,16 +509,14 @@ namespace Microsoft.AspNetCore.Routing.Tree public void TreeRouter_GenerateLink_RespectsPrecedence(string firstTemplate, string secondTemplate) { // Arrange - var matchingRoutes = Enumerable.Empty(); - - var firstEntry = CreateGenerationEntry(firstTemplate, requiredValues: null); - var secondEntry = CreateGenerationEntry(secondTemplate, requiredValues: null, order: 0); + var builder = CreateBuilder(); // 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 }; + MapOutboundEntry(builder, secondTemplate); + MapOutboundEntry(builder, firstTemplate); - var route = CreateAttributeRoute(matchingRoutes, linkGenerationEntries); + var route = builder.Build(); var context = CreateVirtualPathContext(values: null, ambientValues: new { parameter = 5 }); @@ -650,13 +546,9 @@ namespace Microsoft.AspNetCore.Routing.Tree object parameter) { // Arrange - var matchingRoutes = Enumerable.Empty(); - - var entry = CreateGenerationEntry(template, requiredValues: null); - - var linkGenerationEntries = new[] { entry }; - - var route = CreateAttributeRoute(matchingRoutes, linkGenerationEntries); + var builder = CreateBuilder(); + MapOutboundEntry(builder, template); + var route = builder.Build(); VirtualPathContext context; if (parameter != null) @@ -699,17 +591,15 @@ namespace Microsoft.AspNetCore.Routing.Tree public void TreeRouter_GenerateLink_RespectsOrderOverPrecedence(string firstTemplate, string secondTemplate) { // Arrange - var matchingRoutes = Enumerable.Empty(); - - var firstRoute = CreateGenerationEntry(firstTemplate, requiredValues: null, order: 1); - var secondRoute = CreateGenerationEntry(secondTemplate, requiredValues: null, order: 0); + var builder = CreateBuilder(); // 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 }; + MapOutboundEntry(builder, firstTemplate, order: 1); + MapOutboundEntry(builder, secondTemplate, order: 0); - var route = CreateAttributeRoute(matchingRoutes, linkGenerationEntries); + var route = builder.Build(); var context = CreateVirtualPathContext(null, ambientValues: new { parameter = 5 }); @@ -732,16 +622,14 @@ namespace Microsoft.AspNetCore.Routing.Tree public void TreeRouter_GenerateLink_RespectsOrder(string firstTemplate, string secondTemplate) { // Arrange - var matchingRoutes = Enumerable.Empty(); - - var firstRoute = CreateGenerationEntry(firstTemplate, requiredValues: null, order: 1); - var secondRoute = CreateGenerationEntry(secondTemplate, requiredValues: null, order: 0); - + var builder = CreateBuilder(); + // 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 }; + MapOutboundEntry(builder, firstTemplate, requiredValues: null, order: 1); + MapOutboundEntry(builder, secondTemplate, requiredValues: null, order: 0); - var route = CreateAttributeRoute(matchingRoutes, linkGenerationEntries); + var route = builder.Build(); var context = CreateVirtualPathContext(values: null, ambientValues: new { first = 5, second = 5 }); @@ -764,16 +652,14 @@ namespace Microsoft.AspNetCore.Routing.Tree public void TreeRouter_GenerateLink_EnsuresStableOrder(string firstTemplate, string secondTemplate) { // Arrange - var matchingRoutes = Enumerable.Empty(); - - var firstRoute = CreateGenerationEntry(firstTemplate, requiredValues: null, order: 0); - var secondRoute = CreateGenerationEntry(secondTemplate, requiredValues: null, order: 0); - + var builder = CreateBuilder(); + // 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 }; + MapOutboundEntry(builder, secondTemplate, requiredValues: null, order: 0); + MapOutboundEntry(builder, firstTemplate, requiredValues: null, order: 0); - var route = CreateAttributeRoute(matchingRoutes, linkGenerationEntries); + var route = builder.Build(); var context = CreateVirtualPathContext(values: null, ambientValues: new { first = 5, second = 5 }); @@ -787,139 +673,31 @@ namespace Microsoft.AspNetCore.Routing.Tree Assert.Empty(result.DataTokens); } - 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 TreeRouter_CreateTreeRouter_ThrowsIfDifferentEntriesHaveTheSameName( - IEnumerable namedEntries) + [Fact] + public void TreeRouter_GeneratesLink_ForMultipleNamedEntriesWithTheSameTemplate() { // Arrange - string expectedExceptionMessage = "Two or more routes named 'NamedEntry' have different templates." + - Environment.NewLine + - "Parameter name: linkGenerationEntries"; + var builder = CreateBuilder(); - var next = new Mock().Object; + MapOutboundEntry(builder, "Template", name: "NamedEntry", order: 1); + MapOutboundEntry(builder, "TEMPLATE", name: "NamedEntry", order: 2); - // Act - var builder = new TreeRouteBuilder(next, NullLoggerFactory.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 TreeRouter_GeneratesLink_ForMultipleNamedEntriesWithTheSameTemplate( - IEnumerable namedEntries) - { - // Arrange - var expectedLink = - namedEntries.First().Template.Parameters.Any() ? "/template/5" : "/template"; - - var matchingEntries = Enumerable.Empty(); - - var route = CreateAttributeRoute(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); + // Act & Assert (does not throw) + builder.Build(); } [Fact] public void TreeRouter_GenerateLink_WithName() { // Arrange - var namedEntry = CreateGenerationEntry("named", requiredValues: null, order: 1, name: "NamedRoute"); - var unnamedEntry = CreateGenerationEntry("unnamed", requiredValues: null, order: 0); + var builder = CreateBuilder(); // 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 }; + MapOutboundEntry(builder, "named", requiredValues: null, order: 1, name: "NamedRoute"); + MapOutboundEntry(builder, "unnamed", requiredValues: null, order: 0); - var matchingEntries = Enumerable.Empty(); - - var route = CreateAttributeRoute(matchingEntries, linkGenerationEntries); + var route = builder.Build(); var context = CreateVirtualPathContext(values: null, ambientValues: null, name: "NamedRoute"); @@ -937,18 +715,16 @@ namespace Microsoft.AspNetCore.Routing.Tree public void TreeRouter_DoesNotGenerateLink_IfThereIsNoRouteForAGivenName() { // Arrange - 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); - + var builder = CreateBuilder(); + // 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 }; + MapOutboundEntry(builder, "named", requiredValues: null, order: 1, name: "NamedRoute"); - var matchingEntries = Enumerable.Empty(); + // Add an unnamed entry to ensure we don't fall back to generating a link for an unnamed route. + MapOutboundEntry(builder, "unnamed", requiredValues: null, order: 0); - var route = CreateAttributeRoute(matchingEntries, linkGenerationEntries); + var route = builder.Build(); var context = CreateVirtualPathContext(values: null, ambientValues: null, name: "NonExistingNamedRoute"); @@ -968,21 +744,18 @@ namespace Microsoft.AspNetCore.Routing.Tree public void TreeRouter_DoesNotGenerateLink_IfValuesDoNotMatchNamedEntry(string template, string value) { // Arrange - 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); + var builder = CreateBuilder(); // 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 }; + MapOutboundEntry(builder, template, requiredValues: null, order: 1, name: "NamedRoute"); - var matchingEntries = Enumerable.Empty(); + // Add an unnamed entry to ensure we don't fall back to generating a link for an unnamed route. + MapOutboundEntry(builder, "unnamed", requiredValues: null, order: 0); - var route = CreateAttributeRoute(matchingEntries, linkGenerationEntries); + var route = builder.Build(); var ambientValues = value == null ? null : new { parameter = value }; - var context = CreateVirtualPathContext(values: null, ambientValues: ambientValues, name: "NamedRoute"); // Act @@ -1000,26 +773,18 @@ namespace Microsoft.AspNetCore.Routing.Tree public void TreeRouter_GeneratesLink_IfValuesMatchNamedEntry(string template, string value) { // Arrange - var next = new Mock(); - next - .Setup(s => s.GetVirtualPath(It.IsAny())) - .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); - + var builder = CreateBuilder(); + // 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 }; + MapOutboundEntry(builder, template, requiredValues: null, order: 1, name: "NamedRoute"); - var matchingEntries = Enumerable.Empty(); + // Add an unnamed entry to ensure we don't fall back to generating a link for an unnamed route. + MapOutboundEntry(builder, "unnamed", requiredValues: null, order: 0); - var route = CreateAttributeRoute(next.Object, matchingEntries, linkGenerationEntries); + var route = builder.Build(); var ambientValues = value == null ? null : new { parameter = value }; - var context = CreateVirtualPathContext(values: null, ambientValues: ambientValues, name: "NamedRoute"); // Act @@ -1036,8 +801,9 @@ namespace Microsoft.AspNetCore.Routing.Tree public void TreeRouter_GenerateLink_NoRequiredValues() { // Arrange - var entry = CreateGenerationEntry("api/Store", new { }); - var route = CreateAttributeRoute(entry); + var builder = CreateBuilder(); + MapOutboundEntry(builder, "api/Store", new { }); + var route = builder.Build(); var context = CreateVirtualPathContext(new { }); @@ -1055,8 +821,9 @@ namespace Microsoft.AspNetCore.Routing.Tree public void TreeRouter_GenerateLink_Match() { // Arrange - var entry = CreateGenerationEntry("api/Store", new { action = "Index", controller = "Store" }); - var route = CreateAttributeRoute(entry); + var builder = CreateBuilder(); + MapOutboundEntry(builder, "api/Store", new { action = "Index", controller = "Store" }); + var route = builder.Build(); var context = CreateVirtualPathContext(new { action = "Index", controller = "Store" }); @@ -1074,8 +841,9 @@ namespace Microsoft.AspNetCore.Routing.Tree public void TreeRouter_GenerateLink_NoMatch() { // Arrange - var entry = CreateGenerationEntry("api/Store", new { action = "Details", controller = "Store" }); - var route = CreateAttributeRoute(entry); + var builder = CreateBuilder(); + MapOutboundEntry(builder, "api/Store", new { action = "Details", controller = "Store" }); + var route = builder.Build(); var context = CreateVirtualPathContext(new { action = "Index", controller = "Store" }); @@ -1090,8 +858,9 @@ namespace Microsoft.AspNetCore.Routing.Tree public void TreeRouter_GenerateLink_Match_WithAmbientValues() { // Arrange - var entry = CreateGenerationEntry("api/Store", new { action = "Index", controller = "Store" }); - var route = CreateAttributeRoute(entry); + var builder = CreateBuilder(); + MapOutboundEntry(builder, "api/Store", new { action = "Index", controller = "Store" }); + var route = builder.Build(); var context = CreateVirtualPathContext(new { }, new { action = "Index", controller = "Store" }); @@ -1109,8 +878,9 @@ namespace Microsoft.AspNetCore.Routing.Tree public void TreeRouter_GenerateLink_Match_WithParameters() { // Arrange - var entry = CreateGenerationEntry("api/Store/{action}", new { action = "Index", controller = "Store" }); - var route = CreateAttributeRoute(entry); + var builder = CreateBuilder(); + MapOutboundEntry(builder, "api/Store/{action}", new { action = "Index", controller = "Store" }); + var route = builder.Build(); var context = CreateVirtualPathContext(new { action = "Index", controller = "Store" }); @@ -1128,12 +898,12 @@ namespace Microsoft.AspNetCore.Routing.Tree public void TreeRouter_GenerateLink_Match_WithMoreParameters() { // Arrange - var entry = CreateGenerationEntry( + var builder = CreateBuilder(); + MapOutboundEntry(builder, "api/{area}/dosomething/{controller}/{action}", new { action = "Index", controller = "Store", area = "AwesomeCo" }); - var next = new StubRouter(); - var route = CreateAttributeRoute(next, entry); + var route = builder.Build(); var context = CreateVirtualPathContext( new { action = "Index", controller = "Store" }, @@ -1153,8 +923,9 @@ namespace Microsoft.AspNetCore.Routing.Tree public void TreeRouter_GenerateLink_Match_WithDefault() { // Arrange - var entry = CreateGenerationEntry("api/Store/{action=Index}", new { action = "Index", controller = "Store" }); - var route = CreateAttributeRoute(entry); + var builder = CreateBuilder(); + MapOutboundEntry(builder, "api/Store/{action=Index}", new { action = "Index", controller = "Store" }); + var route = builder.Build(); var context = CreateVirtualPathContext(new { action = "Index", controller = "Store" }); @@ -1172,10 +943,10 @@ namespace Microsoft.AspNetCore.Routing.Tree public void TreeRouter_GenerateLink_Match_WithConstraint() { // Arrange - var entry = CreateGenerationEntry("api/Store/{action}/{id:int}", new { action = "Index", controller = "Store" }); + var builder = CreateBuilder(); + MapOutboundEntry(builder, "api/Store/{action}/{id:int}", new { action = "Index", controller = "Store" }); - var next = new StubRouter(); - var route = CreateAttributeRoute(next, entry); + var route = builder.Build(); var context = CreateVirtualPathContext(new { action = "Index", controller = "Store", id = 5 }); @@ -1193,8 +964,9 @@ namespace Microsoft.AspNetCore.Routing.Tree public void TreeRouter_GenerateLink_NoMatch_WithConstraint() { // Arrange - var entry = CreateGenerationEntry("api/Store/{action}/{id:int}", new { action = "Index", controller = "Store" }); - var route = CreateAttributeRoute(entry); + var builder = CreateBuilder(); + MapOutboundEntry(builder, "api/Store/{action}/{id:int}", new { action = "Index", controller = "Store" }); + var route = builder.Build(); var next = new StubRouter(); var context = CreateVirtualPathContext(new { action = "Index", controller = "Store", id = "heyyyy" }); @@ -1210,8 +982,9 @@ namespace Microsoft.AspNetCore.Routing.Tree public void TreeRouter_GenerateLink_Match_WithMixedAmbientValues() { // Arrange - var entry = CreateGenerationEntry("api/Store", new { action = "Index", controller = "Store" }); - var route = CreateAttributeRoute(entry); + var builder = CreateBuilder(); + MapOutboundEntry(builder, "api/Store", new { action = "Index", controller = "Store" }); + var route = builder.Build(); var context = CreateVirtualPathContext(new { action = "Index" }, new { controller = "Store" }); @@ -1229,8 +1002,9 @@ namespace Microsoft.AspNetCore.Routing.Tree public void TreeRouter_GenerateLink_Match_WithQueryString() { // Arrange - var entry = CreateGenerationEntry("api/Store", new { action = "Index", controller = "Store" }); - var route = CreateAttributeRoute(entry); + var builder = CreateBuilder(); + MapOutboundEntry(builder, "api/Store", new { action = "Index", controller = "Store" }); + var route = builder.Build(); var context = CreateVirtualPathContext(new { action = "Index", id = 5 }, new { controller = "Store" }); @@ -1248,10 +1022,11 @@ namespace Microsoft.AspNetCore.Routing.Tree public void TreeRouter_GenerateLink_RejectedByFirstRoute() { // Arrange - var entry1 = CreateGenerationEntry("api/Store", new { action = "Index", controller = "Store" }); - var entry2 = CreateGenerationEntry("api2/{controller}", new { action = "Index", controller = "Blog" }); + var builder = CreateBuilder(); + MapOutboundEntry(builder, "api/Store", new { action = "Index", controller = "Store" }); + MapOutboundEntry(builder, "api2/{controller}", new { action = "Index", controller = "Blog" }); - var route = CreateAttributeRoute(entry1, entry2); + var route = builder.Build(); var context = CreateVirtualPathContext(new { action = "Index", controller = "Blog" }); @@ -1269,15 +1044,14 @@ namespace Microsoft.AspNetCore.Routing.Tree public void TreeRouter_GenerateLink_ToArea() { // Arrange - var entry1 = CreateGenerationEntry("Help/Store", new { area = "Help", action = "Edit", controller = "Store" }); - entry1.GenerationPrecedence = 2; + var builder = CreateBuilder(); + var entry1 = MapOutboundEntry(builder, "Help/Store", new { area = "Help", action = "Edit", controller = "Store" }); + entry1.Precedence = 2; - var entry2 = CreateGenerationEntry("Store", new { area = (string)null, action = "Edit", controller = "Store" }); - entry2.GenerationPrecedence = 1; + var entry2 = MapOutboundEntry(builder, "Store", new { area = (string)null, action = "Edit", controller = "Store" }); + entry2.Precedence = 1; - var next = new StubRouter(); - - var route = CreateAttributeRoute(next, entry1, entry2); + var route = builder.Build(); var context = CreateVirtualPathContext(new { area = "Help", action = "Edit", controller = "Store" }); @@ -1295,15 +1069,14 @@ namespace Microsoft.AspNetCore.Routing.Tree public void TreeRouter_GenerateLink_ToArea_PredecedenceReversed() { // Arrange - var entry1 = CreateGenerationEntry("Help/Store", new { area = "Help", action = "Edit", controller = "Store" }); - entry1.GenerationPrecedence = 1; + var builder = CreateBuilder(); + var entry1 = MapOutboundEntry(builder, "Help/Store", new { area = "Help", action = "Edit", controller = "Store" }); + entry1.Precedence = 1; - var entry2 = CreateGenerationEntry("Store", new { area = (string)null, action = "Edit", controller = "Store" }); - entry2.GenerationPrecedence = 2; + var entry2 = MapOutboundEntry(builder, "Store", new { area = (string)null, action = "Edit", controller = "Store" }); + entry2.Precedence = 2; - var next = new StubRouter(); - - var route = CreateAttributeRoute(next, entry1, entry2); + var route = builder.Build(); var context = CreateVirtualPathContext(new { area = "Help", action = "Edit", controller = "Store" }); @@ -1321,15 +1094,14 @@ namespace Microsoft.AspNetCore.Routing.Tree public void TreeRouter_GenerateLink_ToArea_WithAmbientValues() { // Arrange - var entry1 = CreateGenerationEntry("Help/Store", new { area = "Help", action = "Edit", controller = "Store" }); - entry1.GenerationPrecedence = 2; + var builder = CreateBuilder(); + var entry1 = MapOutboundEntry(builder, "Help/Store", new { area = "Help", action = "Edit", controller = "Store" }); + entry1.Precedence = 2; - var entry2 = CreateGenerationEntry("Store", new { area = (string)null, action = "Edit", controller = "Store" }); - entry2.GenerationPrecedence = 1; + var entry2 = MapOutboundEntry(builder, "Store", new { area = (string)null, action = "Edit", controller = "Store" }); + entry2.Precedence = 1; - var next = new StubRouter(); - - var route = CreateAttributeRoute(next, entry1, entry2); + var route = builder.Build(); var context = CreateVirtualPathContext( values: new { action = "Edit", controller = "Store" }, @@ -1349,15 +1121,14 @@ namespace Microsoft.AspNetCore.Routing.Tree public void TreeRouter_GenerateLink_OutOfArea_IgnoresAmbientValue() { // Arrange - var entry1 = CreateGenerationEntry("Help/Store", new { area = "Help", action = "Edit", controller = "Store" }); - entry1.GenerationPrecedence = 2; + var builder = CreateBuilder(); + var entry1 = MapOutboundEntry(builder, "Help/Store", new { area = "Help", action = "Edit", controller = "Store" }); + entry1.Precedence = 2; - var entry2 = CreateGenerationEntry("Store", new { area = (string)null, action = "Edit", controller = "Store" }); - entry2.GenerationPrecedence = 1; + var entry2 = MapOutboundEntry(builder, "Store", new { area = (string)null, action = "Edit", controller = "Store" }); + entry2.Precedence = 1; - var next = new StubRouter(); - - var route = CreateAttributeRoute(next, entry1, entry2); + var route = builder.Build(); var context = CreateVirtualPathContext( values: new { action = "Edit", controller = "Store" }, @@ -1451,8 +1222,9 @@ namespace Microsoft.AspNetCore.Routing.Tree string expected) { // Arrange - var entry = CreateGenerationEntry(template, null); - var route = CreateAttributeRoute(entry); + var builder = CreateBuilder(); + MapOutboundEntry(builder, template); + var route = builder.Build(); var context = CreateVirtualPathContext(values, ambientValues); @@ -1466,54 +1238,13 @@ namespace Microsoft.AspNetCore.Routing.Tree Assert.Empty(pathData.DataTokens); } - [Fact] - public async Task TreeRouter_SnapshotsRouteData() - { - // Arrange - RouteValueDictionary nestedValues = null; - - var next = new Mock(); - next - .Setup(r => r.RouteAsync(It.IsAny())) - .Callback(c => - { - nestedValues = new RouteValueDictionary(c.RouteData.Values); - c.Handler = null; // Not a match - }) - .Returns(Task.FromResult(true)) - .Verifiable(); - - var entry = CreateMatchingEntry(next.Object, "api/Store", order: 0); - var route = CreateAttributeRoute(next.Object, entry); - - var context = CreateRouteContext("/api/Store"); - - var routeData = context.RouteData; - routeData.Values.Add("action", "Index"); - - var originalValues = new RouteValueDictionary(context.RouteData.Values); - - // Act - await route.RouteAsync(context); - - // Assert - Assert.Equal(originalValues, context.RouteData.Values); - Assert.NotEqual(nestedValues, context.RouteData.Values); - } - [Fact] public async Task TreeRouter_ReplacesExistingRouteValues_IfNotNull() { // Arrange - var router = new Mock(); - router - .Setup(r => r.RouteAsync(It.IsAny())) - .Callback(c => c.Handler = NullHandler) - .Returns(Task.FromResult(true)) - .Verifiable(); - - var entry = CreateMatchingEntry(router.Object, "Foo/{*path}", order: 0); - var route = CreateAttributeRoute(router.Object, entry); + var builder = CreateBuilder(); + MapInboundEntry(builder, "Foo/{*path}"); + var route = builder.Build(); var context = CreateRouteContext("/Foo/Bar"); @@ -1531,15 +1262,9 @@ namespace Microsoft.AspNetCore.Routing.Tree public async Task TreeRouter_DoesNotReplaceExistingRouteValues_IfNull() { // Arrange - var router = new Mock(); - router - .Setup(r => r.RouteAsync(It.IsAny())) - .Callback(c => c.Handler = NullHandler) - .Returns(Task.FromResult(true)) - .Verifiable(); - - var entry = CreateMatchingEntry(router.Object, "Foo/{*path}", order: 0); - var route = CreateAttributeRoute(router.Object, entry); + var builder = CreateBuilder(); + MapInboundEntry(builder, "Foo/{*path}"); + var route = builder.Build(); var context = CreateRouteContext("/Foo/"); @@ -1553,6 +1278,43 @@ namespace Microsoft.AspNetCore.Routing.Tree Assert.Equal("default", context.RouteData.Values["path"]); } + [Fact] + public async Task TreeRouter_SnapshotsRouteData() + { + // Arrange + RouteValueDictionary nestedValues = null; + List nestedRouters = null; + + var next = new Mock(); + next + .Setup(r => r.RouteAsync(It.IsAny())) + .Callback(c => + { + nestedValues = new RouteValueDictionary(c.RouteData.Values); + nestedRouters = new List(c.RouteData.Routers); + c.Handler = null; // Not a match + }) + .Returns(TaskCache.CompletedTask); + + var builder = CreateBuilder(); + MapInboundEntry(builder, "api/Store", handler: next.Object); + var route = builder.Build(); + + var context = CreateRouteContext("/api/Store"); + + var routeData = context.RouteData; + routeData.Values.Add("action", "Index"); + + var originalValues = new RouteValueDictionary(context.RouteData.Values); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.Equal(originalValues, context.RouteData.Values); + Assert.NotEqual(nestedValues, context.RouteData.Values); + } + [Fact] public async Task TreeRouter_SnapshotsRouteData_ResetsWhenNotMatched() { @@ -1567,12 +1329,13 @@ namespace Microsoft.AspNetCore.Routing.Tree { nestedValues = new RouteValueDictionary(c.RouteData.Values); nestedRouters = new List(c.RouteData.Routers); + c.Handler = null; // Not a match }) - .Returns(Task.FromResult(true)) - .Verifiable(); + .Returns(TaskCache.CompletedTask); - var entry = CreateMatchingEntry(next.Object, "api/Store", order: 0); - var route = CreateAttributeRoute(next.Object, entry); + var builder = CreateBuilder(); + MapInboundEntry(builder, "api/Store", handler: next.Object); + var route = builder.Build(); var context = CreateRouteContext("/api/Store"); @@ -1597,11 +1360,12 @@ namespace Microsoft.AspNetCore.Routing.Tree } [Fact] - public async Task TreeRouter_CreatesNewRouteData_ResetsWhenThrows() + public async Task TreeRouter_SnapshotsRouteData_ResetsWhenThrows() { // Arrange RouteValueDictionary nestedValues = null; List nestedRouters = null; + var next = new Mock(); next .Setup(r => r.RouteAsync(It.IsAny())) @@ -1609,11 +1373,13 @@ namespace Microsoft.AspNetCore.Routing.Tree { nestedValues = new RouteValueDictionary(c.RouteData.Values); nestedRouters = new List(c.RouteData.Routers); + throw new Exception(); }) - .Throws(new Exception()); + .Returns(TaskCache.CompletedTask); - var entry = CreateMatchingEntry(next.Object, "api/Store", order: 0); - var route = CreateAttributeRoute(next.Object, entry); + var builder = CreateBuilder(); + MapInboundEntry(builder, "api/Store", handler: next.Object); + var route = builder.Build(); var context = CreateRouteContext("/api/Store"); context.RouteData.Values.Add("action", "Index"); @@ -1665,105 +1431,46 @@ namespace Microsoft.AspNetCore.Routing.Tree 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); - - var defaults = new RouteValueDictionary(); - foreach (var parameter in parsedRouteTemplate.Parameters) - { - if (parameter.DefaultValue != null) - { - defaults.Add(parameter.Name, parameter.DefaultValue); - } - } - - defaults["test_route_group"] = routeGroup; - - entry.TemplateMatcher = new TemplateMatcher(parsedRouteTemplate, defaults); - entry.Precedence = RoutePrecedence.ComputeMatched(parsedRouteTemplate); - entry.Order = order; - entry.Constraints = GetRouteConstriants(CreateConstraintResolver(), template, parsedRouteTemplate); - return entry; - } - - private static TreeRouteLinkGenerationEntry CreateGenerationEntry( + private static InboundRouteEntry MapInboundEntry( + TreeRouteBuilder builder, string template, - object requiredValues, int order = 0, - string name = null) + IRouter handler = null) { - var constraintResolver = CreateConstraintResolver(); + var entry = builder.MapInbound( + handler ?? new StubRouter(), + TemplateParser.Parse(template), + routeName: null, + order: order); - var entry = new TreeRouteLinkGenerationEntry(); - entry.Template = TemplateParser.Parse(template); + // Add a generated 'route group' so we can identify later which entry matched. + entry.Defaults["test_route_group"] = CreateRouteGroup(order, template); - var defaults = new RouteValueDictionary(); - foreach (var parameter in entry.Template.Parameters) - { - if (parameter.DefaultValue != null) - { - defaults.Add(parameter.Name, parameter.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(Encoder, Pool, entry.Template, defaults); - entry.Order = order; - entry.GenerationPrecedence = RoutePrecedence.ComputeGenerated(entry.Template); - entry.RequiredLinkValues = new RouteValueDictionary(requiredValues); - entry.RouteGroup = CreateRouteGroup(order, template); - entry.Name = name; return entry; } - private TreeRouteMatchingEntry CreateMatchingEntry(string template) + private static OutboundRouteEntry MapOutboundEntry( + TreeRouteBuilder builder, + string template, + object requiredValues = null, + int order = 0, + string name = null, + IRouter handler = null) { - var mockConstraint = new Mock(); - mockConstraint.Setup(c => c.Match( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny())) - .Returns(true); + var entry = builder.MapOutbound( + handler ?? new StubRouter(), + TemplateParser.Parse(template), + requiredLinkValues: new RouteValueDictionary(requiredValues), + routeName: name, + order: order); - 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); + // Add a generated 'route group' so we can identify later which entry matched. + entry.Defaults["test_route_group"] = CreateRouteGroup(order, template); return entry; } + private static string CreateRouteGroup(int order, string template) { return string.Format("{0}&{1}", order, template); @@ -1778,123 +1485,31 @@ namespace Microsoft.AspNetCore.Routing.Tree return new DefaultInlineConstraintResolver(optionsMock.Object); } - private static TreeRouter CreateAttributeRoute(TreeRouteLinkGenerationEntry entry) + private static TreeRouteBuilder CreateBuilder() { - return CreateAttributeRoute(new StubRouter(), entry); + var objectPoolProvider = new DefaultObjectPoolProvider(); + var objectPolicy = new UriBuilderContextPooledObjectPolicy(UrlEncoder.Default); + var objectPool = objectPoolProvider.Create(objectPolicy); + + var constraintResolver = CreateConstraintResolver(); + var builder = new TreeRouteBuilder( + NullLoggerFactory.Instance, + UrlEncoder.Default, + objectPool, + constraintResolver); + return builder; } - 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( - IEnumerable matchingEntries, - IEnumerable generationEntries) - { - return CreateAttributeRoute(new StubRouter(), matchingEntries, generationEntries); - } - - private static TreeRouter CreateAttributeRoute( - IRouter next, - IEnumerable matchingEntries, - IEnumerable generationEntries) - { - var builder = new TreeRouteBuilder(next, NullLoggerFactory.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( + private static TreeRouter CreateTreeRouter( string firstTemplate, string secondTemplate) { - var next = new Mock(); - next - .Setup(n => n.GetVirtualPath(It.IsAny())) - .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 }); + var builder = CreateBuilder(); + MapOutboundEntry(builder, firstTemplate); + MapOutboundEntry(builder, secondTemplate); + return builder.Build(); } - private static TreeRouter CreateRoutingAttributeRoute( - ILoggerFactory loggerFactory = null, - params TreeRouteMatchingEntry[] entries) - { - loggerFactory = loggerFactory ?? NullLoggerFactory.Instance; - - var builder = new TreeRouteBuilder(new StubRouter(), loggerFactory); - - foreach (var entry in entries) - { - builder.Add(entry); - } - - return builder.Build(version: 1); - } - - private static IDictionary 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; }