From b8b222b295c2bf7f74570e60b8d45f30346e8299 Mon Sep 17 00:00:00 2001 From: Ajay Bhargav Baaskaran Date: Wed, 28 Oct 2015 12:05:33 -0700 Subject: [PATCH] Replace InnerAttributeRoute with TreeRouter --- .../Routing/LinkGenerationDecisionTree.cs | 16 +- .../Internal/Routing/LinkGenerationMatch.cs | 6 +- ...sions.cs => TreeRouterLoggerExtensions.cs} | 9 +- .../Routing/AttributeRoute.cs | 43 +- .../Routing/Tree/TreeRouteBuilder.cs | 167 ++++++++ .../TreeRouteLinkGenerationEntry.cs} | 6 +- .../TreeRouteMatchingEntry.cs} | 8 +- .../TreeRouter.cs} | 386 ++++++++++++------ .../Routing/Tree/UrlMatchingNode.cs | 34 ++ .../Routing/Tree/UrlMatchingTree.cs | 17 + src/Microsoft.AspNet.Mvc.Core/project.json | 4 + .../Routing/LinkGenerationDecisionTreeTest.cs | 30 +- ...ttributeRouteTest.cs => TreeRouterTest.cs} | 144 ++++--- 13 files changed, 612 insertions(+), 258 deletions(-) rename src/Microsoft.AspNet.Mvc.Core/Logging/{InnerAttributeRouteLoggerExtensions.cs => TreeRouterLoggerExtensions.cs} (70%) create mode 100644 src/Microsoft.AspNet.Mvc.Core/Routing/Tree/TreeRouteBuilder.cs rename src/Microsoft.AspNet.Mvc.Core/Routing/{AttributeRouteLinkGenerationEntry.cs => Tree/TreeRouteLinkGenerationEntry.cs} (89%) rename src/Microsoft.AspNet.Mvc.Core/Routing/{AttributeRouteMatchingEntry.cs => Tree/TreeRouteMatchingEntry.cs} (76%) rename src/Microsoft.AspNet.Mvc.Core/Routing/{InnerAttributeRoute.cs => Tree/TreeRouter.cs} (53%) create mode 100644 src/Microsoft.AspNet.Mvc.Core/Routing/Tree/UrlMatchingNode.cs create mode 100644 src/Microsoft.AspNet.Mvc.Core/Routing/Tree/UrlMatchingTree.cs rename test/Microsoft.AspNet.Mvc.Core.Test/Routing/{InnerAttributeRouteTest.cs => TreeRouterTest.cs} (94%) diff --git a/src/Microsoft.AspNet.Mvc.Core/Internal/Routing/LinkGenerationDecisionTree.cs b/src/Microsoft.AspNet.Mvc.Core/Internal/Routing/LinkGenerationDecisionTree.cs index d42cc18a9c..7c7b43f121 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Internal/Routing/LinkGenerationDecisionTree.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Internal/Routing/LinkGenerationDecisionTree.cs @@ -12,11 +12,11 @@ namespace Microsoft.AspNet.Mvc.Internal.Routing // A decision tree that matches link generation entries based on route data. public class LinkGenerationDecisionTree { - private readonly DecisionTreeNode _root; + private readonly DecisionTreeNode _root; - public LinkGenerationDecisionTree(IReadOnlyList entries) + public LinkGenerationDecisionTree(IReadOnlyList entries) { - _root = DecisionTreeBuilder.GenerateTree( + _root = DecisionTreeBuilder.GenerateTree( entries, new AttributeRouteLinkGenerationEntryClassifier()); } @@ -57,7 +57,7 @@ namespace Microsoft.AspNet.Mvc.Internal.Routing private void Walk( 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 @@ -75,7 +75,7 @@ namespace Microsoft.AspNet.Mvc.Internal.Routing 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); @@ -86,7 +86,7 @@ namespace Microsoft.AspNet.Mvc.Internal.Routing // 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)) { @@ -104,7 +104,7 @@ namespace Microsoft.AspNet.Mvc.Internal.Routing } } - private class AttributeRouteLinkGenerationEntryClassifier : IClassifier + private class AttributeRouteLinkGenerationEntryClassifier : IClassifier { public AttributeRouteLinkGenerationEntryClassifier() { @@ -113,7 +113,7 @@ namespace Microsoft.AspNet.Mvc.Internal.Routing public IEqualityComparer ValueComparer { get; private set; } - public IDictionary GetCriteria(AttributeRouteLinkGenerationEntry item) + public IDictionary GetCriteria(TreeRouteLinkGenerationEntry item) { var results = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var kvp in item.RequiredLinkValues) diff --git a/src/Microsoft.AspNet.Mvc.Core/Internal/Routing/LinkGenerationMatch.cs b/src/Microsoft.AspNet.Mvc.Core/Internal/Routing/LinkGenerationMatch.cs index d4fb447bc5..fa5ce76716 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Internal/Routing/LinkGenerationMatch.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Internal/Routing/LinkGenerationMatch.cs @@ -8,15 +8,15 @@ namespace Microsoft.AspNet.Mvc.Internal.Routing public struct LinkGenerationMatch { private readonly bool _isFallbackMatch; - private readonly AttributeRouteLinkGenerationEntry _entry; + private readonly TreeRouteLinkGenerationEntry _entry; - public LinkGenerationMatch(AttributeRouteLinkGenerationEntry entry, bool isFallbackMatch) + public LinkGenerationMatch(TreeRouteLinkGenerationEntry entry, bool isFallbackMatch) { _entry = entry; _isFallbackMatch = isFallbackMatch; } - public AttributeRouteLinkGenerationEntry Entry { get { return _entry; } } + public TreeRouteLinkGenerationEntry Entry { get { return _entry; } } public bool IsFallbackMatch { get { return _isFallbackMatch; } } } diff --git a/src/Microsoft.AspNet.Mvc.Core/Logging/InnerAttributeRouteLoggerExtensions.cs b/src/Microsoft.AspNet.Mvc.Core/Logging/TreeRouterLoggerExtensions.cs similarity index 70% rename from src/Microsoft.AspNet.Mvc.Core/Logging/InnerAttributeRouteLoggerExtensions.cs rename to src/Microsoft.AspNet.Mvc.Core/Logging/TreeRouterLoggerExtensions.cs index ee3a9dc59e..2bd5ea47b4 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Logging/InnerAttributeRouteLoggerExtensions.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Logging/TreeRouterLoggerExtensions.cs @@ -1,13 +1,16 @@ -using System; +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; using Microsoft.Extensions.Logging; namespace Microsoft.AspNet.Mvc.Logging { - internal static class InnerAttributeRouteLoggerExtensions + internal static class TreeRouterLoggerExtensions { private static readonly Action _matchedRouteName; - static InnerAttributeRouteLoggerExtensions() + static TreeRouterLoggerExtensions() { _matchedRouteName = LoggerMessage.Define( LogLevel.Verbose, diff --git a/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRoute.cs b/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRoute.cs index f2954c9f4a..b5301071ae 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRoute.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRoute.cs @@ -19,12 +19,10 @@ namespace Microsoft.AspNet.Mvc.Routing private readonly IRouter _target; private readonly IActionDescriptorsCollectionProvider _actionDescriptorsCollectionProvider; private readonly IInlineConstraintResolver _constraintResolver; - - // These loggers are used by the inner route, keep them around to avoid re-creating. private readonly ILogger _routeLogger; private readonly ILogger _constraintLogger; - private InnerAttributeRoute _inner; + private TreeRouter _router; public AttributeRoute( IRouter target, @@ -56,48 +54,48 @@ namespace Microsoft.AspNet.Mvc.Routing _actionDescriptorsCollectionProvider = actionDescriptorsCollectionProvider; _constraintResolver = constraintResolver; - _routeLogger = loggerFactory.CreateLogger(); + _routeLogger = loggerFactory.CreateLogger(); _constraintLogger = loggerFactory.CreateLogger(typeof(RouteConstraintMatcher).FullName); } /// public VirtualPathData GetVirtualPath(VirtualPathContext context) { - var route = GetInnerRoute(); - return route.GetVirtualPath(context); + var router = GetTreeRouter(); + return router.GetVirtualPath(context); } /// public Task RouteAsync(RouteContext context) { - var route = GetInnerRoute(); - return route.RouteAsync(context); + var router = GetTreeRouter(); + return router.RouteAsync(context); } - private InnerAttributeRoute GetInnerRoute() + private TreeRouter GetTreeRouter() { var actions = _actionDescriptorsCollectionProvider.ActionDescriptors; - // This is a safe-race. We'll never set inner back to null after initializing + // This is a safe-race. We'll never set router back to null after initializing // it on startup. - if (_inner == null || _inner.Version != actions.Version) + if (_router == null || _router.Version != actions.Version) { - _inner = BuildRoute(actions); + _router = BuildRoute(actions); } - return _inner; + return _router; } - private InnerAttributeRoute BuildRoute(ActionDescriptorsCollection actions) + private TreeRouter BuildRoute(ActionDescriptorsCollection actions) { + var routeBuilder = new TreeRouteBuilder(_target, _routeLogger, _constraintLogger); var routeInfos = GetRouteInfos(_constraintResolver, actions.Items); // We're creating one AttributeRouteGenerationEntry per action. This allows us to match the intended // action by expected route values, and then use the TemplateBinder to generate the link. - var generationEntries = new List(); foreach (var routeInfo in routeInfos) { - generationEntries.Add(new AttributeRouteLinkGenerationEntry() + routeBuilder.Add(new TreeRouteLinkGenerationEntry() { Binder = new TemplateBinder(routeInfo.ParsedTemplate, routeInfo.Defaults), Defaults = routeInfo.Defaults, @@ -116,16 +114,15 @@ namespace Microsoft.AspNet.Mvc.Routing // groups. It's guaranteed that all members of the group have the same template and precedence, // so we only need to hang on to a single instance of the RouteInfo for each group. var distinctRouteInfosByGroup = GroupRouteInfosByGroupId(routeInfos); - var matchingEntries = new List(); foreach (var routeInfo in distinctRouteInfosByGroup) { - matchingEntries.Add(new AttributeRouteMatchingEntry() + routeBuilder.Add(new TreeRouteMatchingEntry() { Order = routeInfo.Order, Precedence = routeInfo.MatchPrecedence, Target = _target, RouteName = routeInfo.Name, - RouteTemplate = routeInfo.RouteTemplate, + RouteTemplate = TemplateParser.Parse(routeInfo.RouteTemplate), TemplateMatcher = new TemplateMatcher( routeInfo.ParsedTemplate, new Dictionary(StringComparer.OrdinalIgnoreCase) @@ -136,13 +133,7 @@ namespace Microsoft.AspNet.Mvc.Routing }); } - return new InnerAttributeRoute( - _target, - matchingEntries, - generationEntries, - _routeLogger, - _constraintLogger, - actions.Version); + return routeBuilder.Build(actions.Version); } private static IEnumerable GroupRouteInfosByGroupId(List routeInfos) diff --git a/src/Microsoft.AspNet.Mvc.Core/Routing/Tree/TreeRouteBuilder.cs b/src/Microsoft.AspNet.Mvc.Core/Routing/Tree/TreeRouteBuilder.cs new file mode 100644 index 0000000000..821cde46b2 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/Routing/Tree/TreeRouteBuilder.cs @@ -0,0 +1,167 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Microsoft.AspNet.Routing; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNet.Mvc.Routing +{ + public class TreeRouteBuilder + { + private readonly IRouter _target; + private readonly List _generatingEntries; + private readonly List _matchingEntries; + + private readonly ILogger _logger; + private readonly ILogger _constraintLogger; + + public TreeRouteBuilder(IRouter target, ILogger routeLogger, ILogger constraintLogger) + { + _target = target; + _generatingEntries = new List(); + _matchingEntries = new List(); + + _logger = routeLogger; + _constraintLogger = constraintLogger; + } + + public void Add(TreeRouteLinkGenerationEntry entry) + { + _generatingEntries.Add(entry); + } + + public void Add(TreeRouteMatchingEntry entry) + { + _matchingEntries.Add(entry); + } + + public TreeRouter Build(int version) + { + var trees = new Dictionary(); + + foreach (var entry in _matchingEntries) + { + UrlMatchingTree tree; + if (!trees.TryGetValue(entry.Order, out tree)) + { + tree = new UrlMatchingTree(entry.Order); + trees.Add(entry.Order, tree); + } + + AddEntryToTree(tree, entry); + } + + return new TreeRouter( + _target, + trees.Values.OrderBy(tree => tree.Order).ToArray(), + _generatingEntries, + _logger, + _constraintLogger, + version); + } + + public void Clear() + { + _generatingEntries.Clear(); + _matchingEntries.Clear(); + } + + private void AddEntryToTree(UrlMatchingTree tree, TreeRouteMatchingEntry entry) + { + var current = tree.Root; + + for (var i = 0; i < entry.RouteTemplate.Segments.Count; i++) + { + var segment = entry.RouteTemplate.Segments[i]; + if (!segment.IsSimple) + { + // Treat complex segments as a constrained parameter + if (current.ConstrainedParameters == null) + { + current.ConstrainedParameters = new UrlMatchingNode(length: i + 1); + } + + current = current.ConstrainedParameters; + continue; + } + + Debug.Assert(segment.Parts.Count == 1); + var part = segment.Parts[0]; + if (part.IsLiteral) + { + UrlMatchingNode next; + if (!current.Literals.TryGetValue(part.Text, out next)) + { + next = new UrlMatchingNode(length: i + 1); + current.Literals.Add(part.Text, next); + } + + current = next; + continue; + } + + if (part.IsParameter && (part.IsOptional || part.IsCatchAll)) + { + current.Matches.Add(entry); + } + + if (part.IsParameter && part.InlineConstraints.Any() && !part.IsCatchAll) + { + if (current.ConstrainedParameters == null) + { + current.ConstrainedParameters = new UrlMatchingNode(length: i + 1); + } + + current = current.ConstrainedParameters; + continue; + } + + if (part.IsParameter && !part.IsCatchAll) + { + if (current.Parameters == null) + { + current.Parameters = new UrlMatchingNode(length: i + 1); + } + + current = current.Parameters; + continue; + } + + if (part.IsParameter && part.InlineConstraints.Any() && part.IsCatchAll) + { + if (current.ConstrainedCatchAlls == null) + { + current.ConstrainedCatchAlls = new UrlMatchingNode(length: i + 1); + } + + current = current.ConstrainedCatchAlls; + continue; + } + + if (part.IsParameter && part.IsCatchAll) + { + if (current.CatchAlls == null) + { + current.CatchAlls = new UrlMatchingNode(length: i + 1); + } + + current = current.CatchAlls; + continue; + } + + Debug.Fail("We shouldn't get here."); + } + + current.Matches.Add(entry); + current.Matches.Sort((x, y) => + { + var result = x.Precedence.CompareTo(y.Precedence); + return result == 0 ? x.RouteTemplate.Template.CompareTo(y.RouteTemplate.Template) : result; + }); + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRouteLinkGenerationEntry.cs b/src/Microsoft.AspNet.Mvc.Core/Routing/Tree/TreeRouteLinkGenerationEntry.cs similarity index 89% rename from src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRouteLinkGenerationEntry.cs rename to src/Microsoft.AspNet.Mvc.Core/Routing/Tree/TreeRouteLinkGenerationEntry.cs index db86e27706..2b032cdedb 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRouteLinkGenerationEntry.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Routing/Tree/TreeRouteLinkGenerationEntry.cs @@ -8,10 +8,10 @@ using Microsoft.AspNet.Routing.Template; namespace Microsoft.AspNet.Mvc.Routing { /// - /// Used to build an . Represents an individual URL-generating route that will be - /// aggregated into the . + /// Used to build a . Represents an individual URL-generating route that will be + /// aggregated into the . /// - public class AttributeRouteLinkGenerationEntry + public class TreeRouteLinkGenerationEntry { /// /// The . diff --git a/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRouteMatchingEntry.cs b/src/Microsoft.AspNet.Mvc.Core/Routing/Tree/TreeRouteMatchingEntry.cs similarity index 76% rename from src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRouteMatchingEntry.cs rename to src/Microsoft.AspNet.Mvc.Core/Routing/Tree/TreeRouteMatchingEntry.cs index 491ea0fc09..5b82149a8b 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRouteMatchingEntry.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Routing/Tree/TreeRouteMatchingEntry.cs @@ -9,10 +9,10 @@ using Microsoft.Extensions.Logging; namespace Microsoft.AspNet.Mvc.Routing { /// - /// Used to build an . Represents an individual URL-matching route that will be - /// aggregated into the . + /// Used to build an . Represents an individual URL-matching route that will be + /// aggregated into the . /// - public class AttributeRouteMatchingEntry + public class TreeRouteMatchingEntry { /// /// The order of the template. @@ -28,7 +28,7 @@ namespace Microsoft.AspNet.Mvc.Routing public string RouteName { get; set; } - public string RouteTemplate { get; set; } + public RouteTemplate RouteTemplate { get; set; } public TemplateMatcher TemplateMatcher { get; set; } diff --git a/src/Microsoft.AspNet.Mvc.Core/Routing/InnerAttributeRoute.cs b/src/Microsoft.AspNet.Mvc.Core/Routing/Tree/TreeRouter.cs similarity index 53% rename from src/Microsoft.AspNet.Mvc.Core/Routing/InnerAttributeRoute.cs rename to src/Microsoft.AspNet.Mvc.Core/Routing/Tree/TreeRouter.cs index e4dbbafa5d..d46b82ed25 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Routing/InnerAttributeRoute.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Routing/Tree/TreeRouter.cs @@ -1,7 +1,8 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -9,7 +10,8 @@ using Microsoft.AspNet.Mvc.Core; using Microsoft.AspNet.Mvc.Internal.Routing; using Microsoft.AspNet.Mvc.Logging; using Microsoft.AspNet.Routing; -using Microsoft.AspNet.Routing.Template; +using Microsoft.AspNet.Routing.Internal; +using Microsoft.Extensions.Internal; using Microsoft.Extensions.Logging; namespace Microsoft.AspNet.Mvc.Routing @@ -17,26 +19,31 @@ namespace Microsoft.AspNet.Mvc.Routing /// /// An implementation for attribute routing. /// - public class InnerAttributeRoute : IRouter + public class TreeRouter : IRouter { private readonly IRouter _next; private readonly LinkGenerationDecisionTree _linkGenerationTree; - private readonly AttributeRouteMatchingEntry[] _matchingEntries; - private readonly IDictionary _namedEntries; + private readonly UrlMatchingTree[] _trees; + private readonly IDictionary _namedEntries; - private ILogger _logger; - private ILogger _constraintLogger; + private readonly ILogger _logger; + private readonly ILogger _constraintLogger; /// - /// Creates a new . + /// Creates a new . /// /// The next router. Invoked when a route entry matches. - /// The set of route entries. - public InnerAttributeRoute( + /// The list of that contains the route entries. + /// The set of . + /// The instance. + /// The instance used + /// in . + /// The version of this route. + public TreeRouter( IRouter next, - IEnumerable matchingEntries, - IEnumerable linkGenerationEntries, - ILogger logger, + UrlMatchingTree[] trees, + IEnumerable linkGenerationEntries, + ILogger routeLogger, ILogger constraintLogger, int version) { @@ -45,9 +52,9 @@ namespace Microsoft.AspNet.Mvc.Routing throw new ArgumentNullException(nameof(next)); } - if (matchingEntries == null) + if (trees == null) { - throw new ArgumentNullException(nameof(matchingEntries)); + throw new ArgumentNullException(nameof(trees)); } if (linkGenerationEntries == null) @@ -55,9 +62,9 @@ namespace Microsoft.AspNet.Mvc.Routing throw new ArgumentNullException(nameof(linkGenerationEntries)); } - if (logger == null) + if (routeLogger == null) { - throw new ArgumentNullException(nameof(logger)); + throw new ArgumentNullException(nameof(routeLogger)); } if (constraintLogger == null) @@ -66,23 +73,11 @@ namespace Microsoft.AspNet.Mvc.Routing } _next = next; - _logger = logger; + _trees = trees; + _logger = routeLogger; _constraintLogger = constraintLogger; - Version = version; - - // Order all the entries by order, then precedence, and then finally by template in order to provide - // a stable routing and link generation order for templates with same order and precedence. - // We use ordinal comparison for the templates because we only care about them being exactly equal and - // we don't want to make any equivalence between templates based on the culture of the machine. - - _matchingEntries = matchingEntries - .OrderBy(o => o.Order) - .ThenBy(e => e.Precedence) - .ThenBy(e => e.RouteTemplate, StringComparer.Ordinal) - .ToArray(); - - var namedEntries = new Dictionary( + var namedEntries = new Dictionary( StringComparer.OrdinalIgnoreCase); foreach (var entry in linkGenerationEntries) @@ -96,7 +91,7 @@ namespace Microsoft.AspNet.Mvc.Routing // We only need to keep one AttributeRouteLinkGenerationEntry per route template // so in case two entries have the same name and the same template we only keep // the first entry. - AttributeRouteLinkGenerationEntry namedEntry = null; + TreeRouteLinkGenerationEntry namedEntry = null; if (namedEntries.TryGetValue(entry.Name, out namedEntry) && !namedEntry.TemplateText.Equals(entry.TemplateText, StringComparison.OrdinalIgnoreCase)) { @@ -114,6 +109,8 @@ namespace Microsoft.AspNet.Mvc.Routing // The decision tree will take care of ordering for these entries. _linkGenerationTree = new LinkGenerationDecisionTree(linkGenerationEntries.ToArray()); + + Version = version; } /// @@ -122,67 +119,6 @@ namespace Microsoft.AspNet.Mvc.Routing /// public int Version { get; } - /// - public async Task RouteAsync(RouteContext context) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - foreach (var matchingEntry in _matchingEntries) - { - var requestPath = context.HttpContext.Request.Path; - var values = matchingEntry.TemplateMatcher.Match(requestPath); - if (values == null) - { - // If we got back a null value set, that means the URI did not match - continue; - } - - var oldRouteData = context.RouteData; - - var newRouteData = new RouteData(oldRouteData); - newRouteData.Routers.Add(matchingEntry.Target); - MergeValues(newRouteData.Values, values); - - if (!RouteConstraintMatcher.Match( - matchingEntry.Constraints, - newRouteData.Values, - context.HttpContext, - this, - RouteDirection.IncomingRequest, - _constraintLogger)) - { - continue; - } - - _logger.MatchedRouteName( - matchingEntry.RouteName, - matchingEntry.RouteTemplate); - - try - { - context.RouteData = newRouteData; - - await matchingEntry.Target.RouteAsync(context); - } - finally - { - // Restore the original values to prevent polluting the route data. - if (!context.IsHandled) - { - context.RouteData = oldRouteData; - } - } - - if (context.IsHandled) - { - break; - } - } - } - /// public VirtualPathData GetVirtualPath(VirtualPathContext context) { @@ -215,9 +151,232 @@ namespace Microsoft.AspNet.Mvc.Routing return null; } + /// + public async Task RouteAsync(RouteContext context) + { + foreach (var tree in _trees) + { + var tokenizer = new PathTokenizer(context.HttpContext.Request.Path); + var enumerator = tokenizer.GetEnumerator(); + var root = tree.Root; + + var treeEnumerator = new TreeEnumerator(root, tokenizer); + + while (treeEnumerator.MoveNext()) + { + var node = treeEnumerator.Current; + foreach (var item in node.Matches) + { + var values = item.TemplateMatcher.Match(context.HttpContext.Request.Path); + if (values == null) + { + continue; + } + + var match = new TemplateMatch(item, values); + + var oldRouteData = context.RouteData; + + var newRouteData = new RouteData(oldRouteData); + + newRouteData.Routers.Add(match.Entry.Target); + MergeValues(newRouteData.Values, match.Values); + + if (!RouteConstraintMatcher.Match( + match.Entry.Constraints, + newRouteData.Values, + context.HttpContext, + this, + RouteDirection.IncomingRequest, + _constraintLogger)) + { + return; + } + + _logger.MatchedRouteName(match.Entry.RouteName, match.Entry.RouteTemplate.Template); + + context.RouteData = newRouteData; + + try + { + await match.Entry.Target.RouteAsync(context); + } + finally + { + if (!context.IsHandled) + { + // Restore the original values to prevent polluting the route data. + context.RouteData = oldRouteData; + } + } + + if (context.IsHandled) + { + return; + } + } + } + } + } + + private struct TreeEnumerator : IEnumerator + { + private readonly Stack _stack; + private readonly PathTokenizer _tokenizer; + + private int _segmentIndex; + + public TreeEnumerator(UrlMatchingNode root, PathTokenizer tokenizer) + { + _stack = new Stack(); + _tokenizer = tokenizer; + Current = null; + _segmentIndex = -1; + + _stack.Push(root); + } + + public UrlMatchingNode Current { get; private set; } + + object IEnumerator.Current => Current; + + public void Dispose() + { + } + + public bool MoveNext() + { + if (_stack == null) + { + return false; + } + + while (_stack.Count > 0) + { + var next = _stack.Pop(); + if (++_segmentIndex >= _tokenizer.Count) + { + _segmentIndex--; + if (next.Matches.Count > 0) + { + Current = next; + return true; + } + } + + if (_tokenizer.Count == 0) + { + continue; + } + + if (next.CatchAlls != null) + { + _stack.Push(next.CatchAlls); + } + + if (next.ConstrainedCatchAlls != null) + { + _stack.Push(next.ConstrainedCatchAlls); + } + + if (next.Parameters != null) + { + _stack.Push(next.Parameters); + } + + if (next.ConstrainedParameters != null) + { + _stack.Push(next.ConstrainedParameters); + } + + if (next.Literals.Count > 0) + { + UrlMatchingNode node; + if (next.Literals.TryGetValue(_tokenizer[_segmentIndex].Value, out node)) + { + _stack.Push(node); + } + } + } + + return false; + } + + public void Reset() + { + _stack.Clear(); + Current = null; + _segmentIndex = -1; + } + } + + private static void MergeValues( + IDictionary destination, + IDictionary values) + { + foreach (var kvp in values) + { + if (kvp.Value != null) + { + // This will replace the original value for the specified key. + // Values from the matched route will take preference over previous + // data in the route context. + destination[kvp.Key] = kvp.Value; + } + } + } + + private struct TemplateMatch : IEquatable + { + public TemplateMatch(TreeRouteMatchingEntry entry, IDictionary values) + { + Entry = entry; + Values = values; + } + + public TreeRouteMatchingEntry Entry { get; } + + public IDictionary Values { get; } + + public override bool Equals(object obj) + { + if (obj is TemplateMatch) + { + return Equals((TemplateMatch)obj); + } + + return false; + } + + public bool Equals(TemplateMatch other) + { + return + object.ReferenceEquals(Entry, other.Entry) && + object.ReferenceEquals(Values, other.Values); + } + + public override int GetHashCode() + { + var hash = new HashCodeCombiner(); + hash.Add(Entry); + hash.Add(Values); + return hash.CombinedHash; + } + + public static bool operator ==(TemplateMatch left, TemplateMatch right) + { + return left.Equals(right); + } + + public static bool operator !=(TemplateMatch left, TemplateMatch right) + { + return !left.Equals(right); + } + } + private VirtualPathData GetVirtualPathForNamedRoute(VirtualPathContext context) { - AttributeRouteLinkGenerationEntry entry; + TreeRouteLinkGenerationEntry entry; if (_namedEntries.TryGetValue(context.RouteName, out entry)) { var path = GenerateVirtualPath(context, entry); @@ -230,7 +389,7 @@ namespace Microsoft.AspNet.Mvc.Routing return null; } - private VirtualPathData GenerateVirtualPath(VirtualPathContext context, AttributeRouteLinkGenerationEntry entry) + private VirtualPathData GenerateVirtualPath(VirtualPathContext context, TreeRouteLinkGenerationEntry entry) { // In attribute the context includes the values that are used to select this entry - typically // these will be the standard 'action', 'controller' and maybe 'area' tokens. However, we don't @@ -315,40 +474,5 @@ namespace Microsoft.AspNet.Mvc.Routing return new VirtualPathData(this, path); } - - private static void MergeValues( - IDictionary destination, - IDictionary values) - { - foreach (var kvp in values) - { - if (kvp.Value != null) - { - // This will replace the original value for the specified key. - // Values from the matched route will take preference over previous - // data in the route context. - destination[kvp.Key] = kvp.Value; - } - } - } - - private bool ContextHasSameValue(VirtualPathContext context, string key, object value) - { - object providedValue; - if (!context.Values.TryGetValue(key, out providedValue)) - { - // If the required value is an 'empty' route value, then ignore ambient values. - // This handles a case where we're generating a link to an action like: - // { area = "", controller = "Home", action = "Index" } - // - // and the ambient values has a value for area. - if (value != null) - { - context.AmbientValues.TryGetValue(key, out providedValue); - } - } - - return TemplateBinder.RoutePartsEqual(providedValue, value); - } } } diff --git a/src/Microsoft.AspNet.Mvc.Core/Routing/Tree/UrlMatchingNode.cs b/src/Microsoft.AspNet.Mvc.Core/Routing/Tree/UrlMatchingNode.cs new file mode 100644 index 0000000000..e611df51a6 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/Routing/Tree/UrlMatchingNode.cs @@ -0,0 +1,34 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; + +namespace Microsoft.AspNet.Mvc.Routing +{ + public class UrlMatchingNode + { + public UrlMatchingNode(int length) + { + Length = length; + + Matches = new List(); + Literals = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + public int Length { get; } + + // These entries are sorted by precedence then template + public List Matches { get; } + + public Dictionary Literals { get; } + + public UrlMatchingNode ConstrainedParameters { get; set; } + + public UrlMatchingNode Parameters { get; set; } + + public UrlMatchingNode ConstrainedCatchAlls { get; set; } + + public UrlMatchingNode CatchAlls { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/Routing/Tree/UrlMatchingTree.cs b/src/Microsoft.AspNet.Mvc.Core/Routing/Tree/UrlMatchingTree.cs new file mode 100644 index 0000000000..70c054201f --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/Routing/Tree/UrlMatchingTree.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNet.Mvc.Routing +{ + public class UrlMatchingTree + { + public UrlMatchingTree(int order) + { + Order = order; + } + + public int Order { get; } + + public UrlMatchingNode Root { get; } = new UrlMatchingNode(length: 0); + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/project.json b/src/Microsoft.AspNet.Mvc.Core/project.json index 4c30720bc6..34b275be7c 100644 --- a/src/Microsoft.AspNet.Mvc.Core/project.json +++ b/src/Microsoft.AspNet.Mvc.Core/project.json @@ -19,6 +19,10 @@ "version": "1.0.0-*", "type": "build" }, + "Microsoft.Extensions.HashCodeCombiner.Sources": { + "type": "build", + "version": "1.0.0-*" + }, "Microsoft.Extensions.Logging.Abstractions": "1.0.0-*", "Microsoft.Extensions.MemoryPool": "1.0.0-*", "Microsoft.Extensions.PropertyActivator.Sources": { diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Internal/Routing/LinkGenerationDecisionTreeTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Internal/Routing/LinkGenerationDecisionTreeTest.cs index e55aaf5742..29e31e2573 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/Internal/Routing/LinkGenerationDecisionTreeTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Internal/Routing/LinkGenerationDecisionTreeTest.cs @@ -16,7 +16,7 @@ namespace Microsoft.AspNet.Mvc.Internal.Routing public void SelectSingleEntry_NoCriteria() { // Arrange - var entries = new List(); + var entries = new List(); var entry = CreateEntry(new { }); entries.Add(entry); @@ -36,7 +36,7 @@ namespace Microsoft.AspNet.Mvc.Internal.Routing public void SelectSingleEntry_MultipleCriteria() { // Arrange - var entries = new List(); + var entries = new List(); var entry = CreateEntry(new { controller = "Store", action = "Buy" }); entries.Add(entry); @@ -56,7 +56,7 @@ namespace Microsoft.AspNet.Mvc.Internal.Routing public void SelectSingleEntry_MultipleCriteria_AmbientValues() { // Arrange - var entries = new List(); + var entries = new List(); var entry = CreateEntry(new { controller = "Store", action = "Buy" }); entries.Add(entry); @@ -78,7 +78,7 @@ namespace Microsoft.AspNet.Mvc.Internal.Routing public void SelectSingleEntry_MultipleCriteria_Replaced() { // Arrange - var entries = new List(); + var entries = new List(); var entry = CreateEntry(new { controller = "Store", action = "Buy" }); entries.Add(entry); @@ -102,7 +102,7 @@ namespace Microsoft.AspNet.Mvc.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 }); entries.Add(entry); @@ -126,7 +126,7 @@ namespace Microsoft.AspNet.Mvc.Internal.Routing public void SelectSingleEntry_MultipleCriteria_NoMatch() { // Arrange - var entries = new List(); + var entries = new List(); var entry = CreateEntry(new { controller = "Store", action = "Buy" }); entries.Add(entry); @@ -146,7 +146,7 @@ namespace Microsoft.AspNet.Mvc.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" }); entries.Add(entry); @@ -168,7 +168,7 @@ namespace Microsoft.AspNet.Mvc.Internal.Routing public void SelectMultipleEntries_OneDoesntMatch() { // Arrange - var entries = new List(); + var entries = new List(); var entry1 = CreateEntry(new { controller = "Store", action = "Buy" }); entries.Add(entry1); @@ -193,7 +193,7 @@ namespace Microsoft.AspNet.Mvc.Internal.Routing public void SelectMultipleEntries_BothMatch_CriteriaSubset() { // Arrange - var entries = new List(); + var entries = new List(); var entry1 = CreateEntry(new { controller = "Store", action = "Buy" }); entries.Add(entry1); @@ -219,7 +219,7 @@ namespace Microsoft.AspNet.Mvc.Internal.Routing public void SelectMultipleEntries_BothMatch_NonOverlappingCriteria() { // Arrange - var entries = new List(); + var entries = new List(); var entry1 = CreateEntry(new { controller = "Store", action = "Buy" }); entries.Add(entry1); @@ -244,7 +244,7 @@ namespace Microsoft.AspNet.Mvc.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; @@ -271,7 +271,7 @@ namespace Microsoft.AspNet.Mvc.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; @@ -297,7 +297,7 @@ namespace Microsoft.AspNet.Mvc.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.TemplateText = "a"; @@ -318,9 +318,9 @@ namespace Microsoft.AspNet.Mvc.Internal.Routing Assert.Equal(entries, matches); } - private AttributeRouteLinkGenerationEntry CreateEntry(object requiredValues) + private TreeRouteLinkGenerationEntry CreateEntry(object requiredValues) { - var entry = new AttributeRouteLinkGenerationEntry(); + var entry = new TreeRouteLinkGenerationEntry(); entry.RequiredLinkValues = new RouteValueDictionary(requiredValues); return entry; } diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Routing/InnerAttributeRouteTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Routing/TreeRouterTest.cs similarity index 94% rename from test/Microsoft.AspNet.Mvc.Core.Test/Routing/InnerAttributeRouteTest.cs rename to test/Microsoft.AspNet.Mvc.Core.Test/Routing/TreeRouterTest.cs index f95121c7f7..8cc7c6977c 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/Routing/InnerAttributeRouteTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Routing/TreeRouterTest.cs @@ -16,7 +16,7 @@ using Xunit; namespace Microsoft.AspNet.Mvc.Routing { - public class InnerAttributeRouteTest + public class TreeRouterTest { [Theory] [InlineData("template/5", "template/{parameter:int}")] @@ -53,7 +53,7 @@ namespace Microsoft.AspNet.Mvc.Routing // try to route the request, the route with a higher precedence gets tried first. var matchingRoutes = new[] { secondRoute, firstRoute }; - var linkGenerationEntries = Enumerable.Empty(); + var linkGenerationEntries = Enumerable.Empty(); var route = CreateAttributeRoute(next.Object, matchingRoutes, linkGenerationEntries); @@ -102,7 +102,7 @@ namespace Microsoft.AspNet.Mvc.Routing // relative order gets tried first. var matchingRoutes = new[] { firstRoute, secondRoute }; - var linkGenerationEntries = Enumerable.Empty(); + var linkGenerationEntries = Enumerable.Empty(); var route = CreateAttributeRoute(next.Object, matchingRoutes, linkGenerationEntries); @@ -143,7 +143,7 @@ namespace Microsoft.AspNet.Mvc.Routing // we try to route the request, the route with the higher relative order gets tried first. var matchingRoutes = new[] { firstRoute, secondRoute }; - var linkGenerationEntries = Enumerable.Empty(); + var linkGenerationEntries = Enumerable.Empty(); var route = CreateAttributeRoute(next.Object, matchingRoutes, linkGenerationEntries); @@ -185,7 +185,7 @@ namespace Microsoft.AspNet.Mvc.Routing // we try to route the request, the route with the higher template order gets tried first. var matchingRoutes = new[] { secondRoute, firstRoute }; - var linkGenerationEntries = Enumerable.Empty(); + var linkGenerationEntries = Enumerable.Empty(); var route = CreateAttributeRoute(next.Object, matchingRoutes, linkGenerationEntries); @@ -225,7 +225,7 @@ namespace Microsoft.AspNet.Mvc.Routing // try to route the request, the route with a higher precedence gets tried first. var matchingRoutes = new[] { firstRoute }; - var linkGenerationEntries = Enumerable.Empty(); + var linkGenerationEntries = Enumerable.Empty(); var route = CreateAttributeRoute(next.Object, matchingRoutes, linkGenerationEntries); @@ -290,7 +290,7 @@ namespace Microsoft.AspNet.Mvc.Routing // 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 linkGenerationEntries = Enumerable.Empty(); var route = CreateAttributeRoute(next.Object, matchingEntries, linkGenerationEntries); var context = CreateRouteContext(request); @@ -348,7 +348,7 @@ namespace Microsoft.AspNet.Mvc.Routing // 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 linkGenerationEntries = Enumerable.Empty(); var route = CreateAttributeRoute(next.Object, matchingEntries, linkGenerationEntries); var context = CreateRouteContext(request); @@ -547,7 +547,7 @@ namespace Microsoft.AspNet.Mvc.Routing }) .Returns((VirtualPathData)null); - var matchingRoutes = Enumerable.Empty(); + var matchingRoutes = Enumerable.Empty(); var firstEntry = CreateGenerationEntry(firstTemplate, requiredValues: null); var secondEntry = CreateGenerationEntry(secondTemplate, requiredValues: null, order: 0); @@ -598,7 +598,7 @@ namespace Microsoft.AspNet.Mvc.Routing }) .Returns((VirtualPathData)null); - var matchingRoutes = Enumerable.Empty(); + var matchingRoutes = Enumerable.Empty(); var entry = CreateGenerationEntry(template, requiredValues: null); @@ -658,7 +658,7 @@ namespace Microsoft.AspNet.Mvc.Routing }) .Returns((VirtualPathData)null); - var matchingRoutes = Enumerable.Empty(); + var matchingRoutes = Enumerable.Empty(); var firstRoute = CreateGenerationEntry(firstTemplate, requiredValues: null, order: 1); var secondRoute = CreateGenerationEntry(secondTemplate, requiredValues: null, order: 0); @@ -704,7 +704,7 @@ namespace Microsoft.AspNet.Mvc.Routing }) .Returns((VirtualPathData)null); - var matchingRoutes = Enumerable.Empty(); + var matchingRoutes = Enumerable.Empty(); var firstRoute = CreateGenerationEntry(firstTemplate, requiredValues: null, order: 1); var secondRoute = CreateGenerationEntry(secondTemplate, requiredValues: null, order: 0); @@ -749,7 +749,7 @@ namespace Microsoft.AspNet.Mvc.Routing }) .Returns((VirtualPathData)null); - var matchingRoutes = Enumerable.Empty(); + var matchingRoutes = Enumerable.Empty(); var firstRoute = CreateGenerationEntry(firstTemplate, requiredValues: null, order: 0); var secondRoute = CreateGenerationEntry(secondTemplate, requiredValues: null, order: 0); @@ -778,7 +778,7 @@ namespace Microsoft.AspNet.Mvc.Routing { get { - var data = new TheoryData>(); + var data = new TheoryData>(); data.Add(new[] { CreateGenerationEntry("template", null, 0, "NamedEntry"), @@ -806,9 +806,9 @@ namespace Microsoft.AspNet.Mvc.Routing } [Theory] - [MemberData(nameof(InnerAttributeRouteTest.NamedEntriesWithDifferentTemplates))] + [MemberData(nameof(TreeRouterTest.NamedEntriesWithDifferentTemplates))] public void AttributeRoute_CreateAttributeRoute_ThrowsIfDifferentEntriesHaveTheSameName( - IEnumerable namedEntries) + IEnumerable namedEntries) { // Arrange string expectedExceptionMessage = "Two or more routes named 'NamedEntry' have different templates." + @@ -817,18 +817,19 @@ namespace Microsoft.AspNet.Mvc.Routing var next = new Mock().Object; - var matchingEntries = Enumerable.Empty(); - // Act + var builder = new TreeRouteBuilder(next, NullLogger.Instance, NullLogger.Instance); var exception = Assert.Throws( "linkGenerationEntries", - () => new InnerAttributeRoute( - next, - matchingEntries, - namedEntries, - NullLogger.Instance, - NullLogger.Instance, - version: 1)); + () => + { + foreach (var entry in namedEntries) + { + builder.Add(entry); + } + + return builder.Build(version: 1); + }); Assert.Equal(expectedExceptionMessage, exception.Message, StringComparer.OrdinalIgnoreCase); } @@ -837,7 +838,7 @@ namespace Microsoft.AspNet.Mvc.Routing { get { - var data = new TheoryData>(); + var data = new TheoryData>(); data.Add(new[] { @@ -866,9 +867,9 @@ namespace Microsoft.AspNet.Mvc.Routing } [Theory] - [MemberData(nameof(InnerAttributeRouteTest.NamedEntriesWithTheSameTemplate))] + [MemberData(nameof(TreeRouterTest.NamedEntriesWithTheSameTemplate))] public void AttributeRoute_GeneratesLink_ForMultipleNamedEntriesWithTheSameTemplate( - IEnumerable namedEntries) + IEnumerable namedEntries) { // Arrange var expectedLink = new PathString( @@ -885,7 +886,7 @@ namespace Microsoft.AspNet.Mvc.Routing }) .Returns((VirtualPathData)null); - var matchingEntries = Enumerable.Empty(); + var matchingEntries = Enumerable.Empty(); var route = CreateAttributeRoute(next.Object, matchingEntries, namedEntries); @@ -926,7 +927,7 @@ namespace Microsoft.AspNet.Mvc.Routing // if it were an unnamed route. var linkGenerationEntries = new[] { namedEntry, unnamedEntry }; - var matchingEntries = Enumerable.Empty(); + var matchingEntries = Enumerable.Empty(); var route = CreateAttributeRoute(next.Object, matchingEntries, linkGenerationEntries); @@ -966,7 +967,7 @@ namespace Microsoft.AspNet.Mvc.Routing // if it were an unnamed route. var linkGenerationEntries = new[] { namedEntry, unnamedEntry }; - var matchingEntries = Enumerable.Empty(); + var matchingEntries = Enumerable.Empty(); var route = CreateAttributeRoute(next.Object, matchingEntries, linkGenerationEntries); @@ -1006,7 +1007,7 @@ namespace Microsoft.AspNet.Mvc.Routing // if it were an unnamed route. var linkGenerationEntries = new[] { namedEntry, unnamedEntry }; - var matchingEntries = Enumerable.Empty(); + var matchingEntries = Enumerable.Empty(); var route = CreateAttributeRoute(next.Object, matchingEntries, linkGenerationEntries); @@ -1048,7 +1049,7 @@ namespace Microsoft.AspNet.Mvc.Routing // if it were an unnamed route. var linkGenerationEntries = new[] { namedEntry, unnamedEntry }; - var matchingEntries = Enumerable.Empty(); + var matchingEntries = Enumerable.Empty(); var route = CreateAttributeRoute(next.Object, matchingEntries, linkGenerationEntries); @@ -1796,12 +1797,12 @@ namespace Microsoft.AspNet.Mvc.Routing name); } - private static AttributeRouteMatchingEntry CreateMatchingEntry(IRouter router, string template, int order) + private static TreeRouteMatchingEntry CreateMatchingEntry(IRouter router, string template, int order) { var routeGroup = string.Format("{0}&&{1}", order, template); - var entry = new AttributeRouteMatchingEntry(); + var entry = new TreeRouteMatchingEntry(); entry.Target = router; - entry.RouteTemplate = template; + entry.RouteTemplate = TemplateParser.Parse(template); var parsedRouteTemplate = TemplateParser.Parse(template); entry.TemplateMatcher = new TemplateMatcher( parsedRouteTemplate, @@ -1812,7 +1813,7 @@ namespace Microsoft.AspNet.Mvc.Routing return entry; } - private static AttributeRouteLinkGenerationEntry CreateGenerationEntry( + private static TreeRouteLinkGenerationEntry CreateGenerationEntry( string template, object requiredValues, int order = 0, @@ -1820,7 +1821,7 @@ namespace Microsoft.AspNet.Mvc.Routing { var constraintResolver = CreateConstraintResolver(); - var entry = new AttributeRouteLinkGenerationEntry(); + var entry = new TreeRouteLinkGenerationEntry(); entry.TemplateText = template; entry.Template = TemplateParser.Parse(template); @@ -1858,7 +1859,7 @@ namespace Microsoft.AspNet.Mvc.Routing return entry; } - private AttributeRouteMatchingEntry CreateMatchingEntry(string template) + private TreeRouteMatchingEntry CreateMatchingEntry(string template) { var mockConstraint = new Mock(); mockConstraint.Setup(c => c.Match( @@ -1874,9 +1875,9 @@ namespace Microsoft.AspNet.Mvc.Routing It.IsAny())) .Returns(mockConstraint.Object); - var entry = new AttributeRouteMatchingEntry(); + var entry = new TreeRouteMatchingEntry(); entry.Target = new StubRouter(); - entry.RouteTemplate = template; + entry.RouteTemplate = TemplateParser.Parse(template); return entry; } @@ -1895,52 +1896,61 @@ namespace Microsoft.AspNet.Mvc.Routing return new DefaultInlineConstraintResolver(optionsMock.Object); } - private static InnerAttributeRoute CreateAttributeRoute(AttributeRouteLinkGenerationEntry entry) + private static TreeRouter CreateAttributeRoute(TreeRouteLinkGenerationEntry entry) { return CreateAttributeRoute(new StubRouter(), entry); } - private static InnerAttributeRoute CreateAttributeRoute(IRouter next, AttributeRouteLinkGenerationEntry entry) + private static TreeRouter CreateAttributeRoute(IRouter next, TreeRouteLinkGenerationEntry entry) { return CreateAttributeRoute(next, new[] { entry }); } - private static InnerAttributeRoute CreateAttributeRoute(params AttributeRouteLinkGenerationEntry[] entries) + private static TreeRouter CreateAttributeRoute(params TreeRouteLinkGenerationEntry[] entries) { return CreateAttributeRoute(new StubRouter(), entries); } - private static InnerAttributeRoute CreateAttributeRoute(IRouter next, params AttributeRouteLinkGenerationEntry[] entries) + private static TreeRouter CreateAttributeRoute(IRouter next, params TreeRouteLinkGenerationEntry[] entries) { return CreateAttributeRoute( next, - Enumerable.Empty(), + Enumerable.Empty(), entries); } - private static InnerAttributeRoute CreateAttributeRoute(IRouter next, params AttributeRouteMatchingEntry[] entries) + private static TreeRouter CreateAttributeRoute(IRouter next, params TreeRouteMatchingEntry[] entries) { return CreateAttributeRoute( next, entries, - Enumerable.Empty()); + Enumerable.Empty()); } - private static InnerAttributeRoute CreateAttributeRoute( + private static TreeRouter CreateAttributeRoute( IRouter next, - IEnumerable matchingEntries, - IEnumerable generationEntries) + IEnumerable matchingEntries, + IEnumerable generationEntries) { - return new InnerAttributeRoute( + var builder = new TreeRouteBuilder( next, - matchingEntries, - generationEntries, NullLogger.Instance, - NullLogger.Instance, - version: 1); + NullLogger.Instance); + + foreach (var entry in matchingEntries) + { + builder.Add(entry); + } + + foreach (var entry in generationEntries) + { + builder.Add(entry); + } + + return builder.Build(version: 1); } - private static InnerAttributeRoute CreateAttributeRoute( + private static TreeRouter CreateAttributeRoute( Action virtualPathCallback, string firstTemplate, string secondTemplate) @@ -1949,7 +1959,7 @@ namespace Microsoft.AspNet.Mvc.Routing next.Setup(n => n.GetVirtualPath(It.IsAny())).Callback(virtualPathCallback) .Returns((VirtualPathData)null); - var matchingRoutes = Enumerable.Empty(); + var matchingRoutes = Enumerable.Empty(); var firstEntry = CreateGenerationEntry(firstTemplate, requiredValues: null); var secondEntry = CreateGenerationEntry(secondTemplate, requiredValues: null); @@ -1959,19 +1969,23 @@ namespace Microsoft.AspNet.Mvc.Routing new[] { secondEntry, firstEntry }); } - private static InnerAttributeRoute CreateRoutingAttributeRoute( + private static TreeRouter CreateRoutingAttributeRoute( ILoggerFactory loggerFactory = null, - params AttributeRouteMatchingEntry[] entries) + params TreeRouteMatchingEntry[] entries) { loggerFactory = loggerFactory ?? NullLoggerFactory.Instance; - return new InnerAttributeRoute( + var builder = new TreeRouteBuilder( new StubRouter(), - entries, - Enumerable.Empty(), loggerFactory.CreateLogger(), - loggerFactory.CreateLogger(typeof(RouteConstraintMatcher).FullName), - version: 1); + loggerFactory.CreateLogger(typeof(RouteConstraintMatcher).FullName)); + + foreach (var entry in entries) + { + builder.Add(entry); + } + + return builder.Build(version: 1); } private static IReadOnlyDictionary GetRouteConstriants(