From 1b0258ab8fccff1306e350fd036d05c3110bbc8e Mon Sep 17 00:00:00 2001 From: Jass Bagga Date: Mon, 20 Nov 2017 14:18:50 -0800 Subject: [PATCH] Move AddEntryToTree to shared source (#496) Addresses #495 --- .../TreeEnumerator.cs | 6 +- .../UrlMatchingTree.cs | 195 ++++++++++++++++++ .../Patterns/RoutePatternMatcher.cs | 3 +- .../Tree/TreeMatcher.cs | 165 +-------------- .../Template/TemplatePart.cs | 4 +- .../Tree/TreeRouteBuilder.cs | 167 +-------------- 6 files changed, 204 insertions(+), 336 deletions(-) diff --git a/shared/Microsoft.AspNetCore.Routing.UrlMatchingTree.Sources/TreeEnumerator.cs b/shared/Microsoft.AspNetCore.Routing.UrlMatchingTree.Sources/TreeEnumerator.cs index 916b154ab0..a5b58f5aa7 100644 --- a/shared/Microsoft.AspNetCore.Routing.UrlMatchingTree.Sources/TreeEnumerator.cs +++ b/shared/Microsoft.AspNetCore.Routing.UrlMatchingTree.Sources/TreeEnumerator.cs @@ -5,8 +5,9 @@ using System.Collections; using System.Collections.Generic; using System.Diagnostics; using Microsoft.AspNetCore.Dispatcher.Internal; - #if ROUTING +using Microsoft.AspNetCore.Routing.Template; + namespace Microsoft.AspNetCore.Routing.Tree #elif DISPATCHER namespace Microsoft.AspNetCore.Dispatcher @@ -115,5 +116,4 @@ namespace Microsoft.AspNetCore.Dispatcher Current = null; } } - -} \ No newline at end of file +} diff --git a/shared/Microsoft.AspNetCore.Routing.UrlMatchingTree.Sources/UrlMatchingTree.cs b/shared/Microsoft.AspNetCore.Routing.UrlMatchingTree.Sources/UrlMatchingTree.cs index d3aa7dba05..1983c0b082 100644 --- a/shared/Microsoft.AspNetCore.Routing.UrlMatchingTree.Sources/UrlMatchingTree.cs +++ b/shared/Microsoft.AspNetCore.Routing.UrlMatchingTree.Sources/UrlMatchingTree.cs @@ -1,7 +1,13 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Microsoft.AspNetCore.Dispatcher.Patterns; #if ROUTING +using Microsoft.AspNetCore.Routing.Template; + namespace Microsoft.AspNetCore.Routing.Tree #elif DISPATCHER namespace Microsoft.AspNetCore.Dispatcher @@ -36,5 +42,194 @@ namespace Microsoft.AspNetCore.Dispatcher /// Gets the root of the . /// public UrlMatchingNode Root { get; } = new UrlMatchingNode(0); + + internal static void AddEntryToTree(UrlMatchingTree tree, InboundRouteEntry entry) + { + // The url matching tree represents all the routes asociated with a given + // order. Each node in the tree represents all the different categories + // a segment can have for which there is a defined inbound route entry. + // Each node contains a set of Matches that indicate all the routes for which + // a URL is a potential match. This list contains the routes with the same + // number of segments and the routes with the same number of segments plus an + // additional catch all parameter (as it can be empty). + // For example, for a set of routes like: + // 'Customer/Index/{id}' + // '{Controller}/{Action}/{*parameters}' + // + // The route tree will look like: + // Root -> + // Literals: Customer -> + // Literals: Index -> + // Parameters: {id} + // Matches: 'Customer/Index/{id}' + // Parameters: {Controller} -> + // Parameters: {Action} -> + // Matches: '{Controller}/{Action}/{*parameters}' + // CatchAlls: {*parameters} + // Matches: '{Controller}/{Action}/{*parameters}' + // + // When the tree router tries to match a route, it iterates the list of url matching trees + // in ascending order. For each tree it traverses each node starting from the root in the + // following order: Literals, constrained parameters, parameters, constrained catch all routes, catch alls. + // When it gets to a node of the same length as the route its trying to match, it simply looks at the list of + // candidates (which is in precence order) and tries to match the url against it. + + var current = tree.Root; +#if ROUTING + var routePattern = entry.RouteTemplate.ToRoutePattern(); + var matcher = new TemplateMatcher(entry.RouteTemplate, entry.Defaults); +#elif DISPATCHER + var routePattern = entry.RoutePattern; + var matcher = new RoutePatternMatcher(routePattern, entry.Defaults); +#else +#error +#endif + + for (var i = 0; i < routePattern.PathSegments.Count; i++) + { + var segment = routePattern.PathSegments[i]; + if (!segment.IsSimple) + { + // Treat complex segments as a constrained parameter + if (current.ConstrainedParameters == null) + { + current.ConstrainedParameters = new UrlMatchingNode(i + 1); + } + + current = current.ConstrainedParameters; + continue; + } + + Debug.Assert(segment.Parts.Count == 1); + var part = segment.Parts[0]; + if (part.IsLiteral) + { + var literal = (RoutePatternLiteral)part; + if (!current.Literals.TryGetValue(literal.Content, out var next)) + { + next = new UrlMatchingNode(i + 1); + current.Literals.Add(literal.Content, next); + } + + current = next; + continue; + } + + // We accept templates that have intermediate optional values, but we ignore + // those values for route matching. For that reason, we need to add the entry + // to the list of matches, only if the remaining segments are optional. For example: + // /{controller}/{action=Index}/{id} will be equivalent to /{controller}/{action}/{id} + // for the purposes of route matching. + if (part.IsParameter && + RemainingSegmentsAreOptional(routePattern.PathSegments, i)) + { +#if ROUTING + current.Matches.Add(new InboundMatch() { Entry = entry, TemplateMatcher = matcher }); +#elif DISPATCHER + current.Matches.Add(new InboundMatch() { Entry = entry, RoutePatternMatcher = matcher }); +#else +#error +#endif + } + + var parameter = (RoutePatternParameter)part; + if (parameter != null && parameter.Constraints.Any() && !parameter.IsCatchAll) + { + if (current.ConstrainedParameters == null) + { + current.ConstrainedParameters = new UrlMatchingNode(i + 1); + } + + current = current.ConstrainedParameters; + continue; + } + + if (parameter != null && !parameter.IsCatchAll) + { + if (current.Parameters == null) + { + current.Parameters = new UrlMatchingNode(i + 1); + } + + current = current.Parameters; + continue; + } + + if (parameter != null && parameter.Constraints.Any() && parameter.IsCatchAll) + { + if (current.ConstrainedCatchAlls == null) + { + current.ConstrainedCatchAlls = new UrlMatchingNode(i + 1) { IsCatchAll = true }; + } + + current = current.ConstrainedCatchAlls; + continue; + } + + if (parameter != null && parameter.IsCatchAll) + { + if (current.CatchAlls == null) + { + current.CatchAlls = new UrlMatchingNode(i + 1) { IsCatchAll = true }; + } + + current = current.CatchAlls; + continue; + } + + Debug.Fail("We shouldn't get here."); + } + +#if ROUTING + current.Matches.Add(new InboundMatch() { Entry = entry, TemplateMatcher = matcher }); + current.Matches.Sort((x, y) => + { + var result = x.Entry.Precedence.CompareTo(y.Entry.Precedence); + return result == 0 ? x.Entry.RouteTemplate.TemplateText.CompareTo(y.Entry.RouteTemplate.TemplateText) : result; + }); +#elif DISPATCHER + current.Matches.Add(new InboundMatch() { Entry = entry, RoutePatternMatcher = matcher }); + current.Matches.Sort((x, y) => + { + var result = x.Entry.Precedence.CompareTo(y.Entry.Precedence); + return result == 0 ? x.Entry.RoutePattern.RawText.CompareTo(y.Entry.RoutePattern.RawText) : result; + }); +#else +#error +#endif + + } + + private static bool RemainingSegmentsAreOptional(IReadOnlyList segments, int currentParameterIndex) + { + for (var i = currentParameterIndex; i < segments.Count; i++) + { + if (!segments[i].IsSimple) + { + // /{complex}-{segment} + return false; + } + + var part = segments[i].Parts[0]; + if (!part.IsParameter) + { + // /literal + return false; + } + + var parameter = (RoutePatternParameter)part; + var isOptionalCatchAllOrHasDefaultValue = parameter.IsOptional || + parameter.IsCatchAll || + parameter.DefaultValue != null; + + if (!isOptionalCatchAllOrHasDefaultValue) + { + // /{parameter} + return false; + } + } + + return true; + } } } diff --git a/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternMatcher.cs b/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternMatcher.cs index 0340ead1b1..3da6a745c4 100644 --- a/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternMatcher.cs +++ b/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternMatcher.cs @@ -52,9 +52,8 @@ namespace Microsoft.AspNetCore.Dispatcher continue; } - object value; var parameter = (RoutePatternParameter)part; - if (Defaults.TryGetValue(parameter.Name, out value)) + if (Defaults.TryGetValue(parameter.Name, out var value)) { _hasDefaultValue[i] = true; _defaultValues[i] = value; diff --git a/src/Microsoft.AspNetCore.Dispatcher/Tree/TreeMatcher.cs b/src/Microsoft.AspNetCore.Dispatcher/Tree/TreeMatcher.cs index 836ccaf22b..292f99ee22 100644 --- a/src/Microsoft.AspNetCore.Dispatcher/Tree/TreeMatcher.cs +++ b/src/Microsoft.AspNetCore.Dispatcher/Tree/TreeMatcher.cs @@ -175,7 +175,7 @@ namespace Microsoft.AspNetCore.Dispatcher var tree = trees[entry.Order]; - AddEntryToTree(tree, entry); + UrlMatchingTree.AddEntryToTree(tree, entry); } return new Cache(trees.ToArray()); @@ -229,169 +229,6 @@ namespace Microsoft.AspNetCore.Dispatcher return entry; } - internal static void AddEntryToTree(UrlMatchingTree tree, InboundRouteEntry entry) - { - // The url matching tree represents all the routes asociated with a given - // order. Each node in the tree represents all the different categories - // a segment can have for which there is a defined inbound route entry. - // Each node contains a set of Matches that indicate all the routes for which - // a URL is a potential match. This list contains the routes with the same - // number of segments and the routes with the same number of segments plus an - // additional catch all parameter (as it can be empty). - // For example, for a set of routes like: - // 'Customer/Index/{id}' - // '{Controller}/{Action}/{*parameters}' - // - // The route tree will look like: - // Root -> - // Literals: Customer -> - // Literals: Index -> - // Parameters: {id} - // Matches: 'Customer/Index/{id}' - // Parameters: {Controller} -> - // Parameters: {Action} -> - // Matches: '{Controller}/{Action}/{*parameters}' - // CatchAlls: {*parameters} - // Matches: '{Controller}/{Action}/{*parameters}' - // - // When the tree router tries to match a route, it iterates the list of url matching trees - // in ascending order. For each tree it traverses each node starting from the root in the - // following order: Literals, constrained parameters, parameters, constrained catch all routes, catch alls. - // When it gets to a node of the same length as the route its trying to match, it simply looks at the list of - // candidates (which is in precence order) and tries to match the url against it. - - var current = tree.Root; - var matcher = new RoutePatternMatcher(entry.RoutePattern, entry.Defaults); - - for (var i = 0; i < entry.RoutePattern.PathSegments.Count; i++) - { - var segment = entry.RoutePattern.PathSegments[i]; - if (!segment.IsSimple) - { - // Treat complex segments as a constrained parameter - if (current.ConstrainedParameters == null) - { - current.ConstrainedParameters = new UrlMatchingNode(depth: i + 1); - } - - current = current.ConstrainedParameters; - continue; - } - - Debug.Assert(segment.Parts.Count == 1); - var part = segment.Parts[0]; - if (part.IsLiteral) - { - var literal = (RoutePatternLiteral)part; - if (!current.Literals.TryGetValue(literal.Content, out var next)) - { - next = new UrlMatchingNode(depth: i + 1); - current.Literals.Add(literal.Content, next); - } - - current = next; - continue; - } - - // We accept templates that have intermediate optional values, but we ignore - // those values for route matching. For that reason, we need to add the entry - // to the list of matches, only if the remaining segments are optional. For example: - // /{controller}/{action=Index}/{id} will be equivalent to /{controller}/{action}/{id} - // for the purposes of route matching. - if (part.IsParameter && - RemainingSegmentsAreOptional(entry.RoutePattern.PathSegments, i)) - { - current.Matches.Add(new InboundMatch() { Entry = entry, RoutePatternMatcher = matcher }); - } - - var parameter = part as RoutePatternParameter; - if (parameter != null && parameter.Constraints.Any() && !parameter.IsCatchAll) - { - if (current.ConstrainedParameters == null) - { - current.ConstrainedParameters = new UrlMatchingNode(depth: i + 1); - } - - current = current.ConstrainedParameters; - continue; - } - - if (parameter != null && !parameter.IsCatchAll) - { - if (current.Parameters == null) - { - current.Parameters = new UrlMatchingNode(depth: i + 1); - } - - current = current.Parameters; - continue; - } - - if (parameter != null && parameter.Constraints.Any() && parameter.IsCatchAll) - { - if (current.ConstrainedCatchAlls == null) - { - current.ConstrainedCatchAlls = new UrlMatchingNode(depth: i + 1) { IsCatchAll = true }; - } - - current = current.ConstrainedCatchAlls; - continue; - } - - if (parameter != null && parameter.IsCatchAll) - { - if (current.CatchAlls == null) - { - current.CatchAlls = new UrlMatchingNode(depth: i + 1) { IsCatchAll = true }; - } - - current = current.CatchAlls; - continue; - } - - Debug.Fail("We shouldn't get here."); - } - - current.Matches.Add(new InboundMatch() { Entry = entry, RoutePatternMatcher = matcher }); - current.Matches.Sort((x, y) => - { - var result = x.Entry.Precedence.CompareTo(y.Entry.Precedence); - return result == 0 ? x.Entry.RoutePattern.RawText.CompareTo(y.Entry.RoutePattern.RawText) : result; - }); - } - - private static bool RemainingSegmentsAreOptional(IReadOnlyList segments, int currentParameterIndex) - { - for (var i = currentParameterIndex; i < segments.Count; i++) - { - if (!segments[i].IsSimple) - { - // /{complex}-{segment} - return false; - } - - var part = segments[i].Parts[0]; - if (!part.IsParameter) - { - // /literal - return false; - } - - var parameter = (RoutePatternParameter)part; - var isOptionlCatchAllOrHasDefaultValue = parameter.IsOptional || - parameter.IsCatchAll || - parameter.DefaultValue != null; - - if (!isOptionlCatchAllOrHasDefaultValue) - { - // /{parameter} - return false; - } - } - - return true; - } - private struct Key : IEquatable { public readonly int Order; diff --git a/src/Microsoft.AspNetCore.Routing/Template/TemplatePart.cs b/src/Microsoft.AspNetCore.Routing/Template/TemplatePart.cs index deed52a093..f195c50102 100644 --- a/src/Microsoft.AspNetCore.Routing/Template/TemplatePart.cs +++ b/src/Microsoft.AspNetCore.Routing/Template/TemplatePart.cs @@ -25,7 +25,7 @@ namespace Microsoft.AspNetCore.Routing.Template { Text = literal.Content; } - else if (other.IsParameter && other is Microsoft.AspNetCore.Dispatcher.Patterns.RoutePatternParameter parameter) + else if (other.IsParameter && other is Dispatcher.Patterns.RoutePatternParameter parameter) { // Text is unused by TemplatePart and assumed to be null when the part is a parameter. Name = parameter.Name; @@ -34,7 +34,7 @@ namespace Microsoft.AspNetCore.Routing.Template DefaultValue = parameter.DefaultValue; InlineConstraints = parameter.Constraints?.Select(p => new InlineConstraint(p)); } - else if (other.IsSeparator && other is Microsoft.AspNetCore.Dispatcher.Patterns.RoutePatternSeparator separator) + else if (other.IsSeparator && other is Dispatcher.Patterns.RoutePatternSeparator separator) { Text = separator.Content; IsOptionalSeperator = true; diff --git a/src/Microsoft.AspNetCore.Routing/Tree/TreeRouteBuilder.cs b/src/Microsoft.AspNetCore.Routing/Tree/TreeRouteBuilder.cs index 9b3c58df6b..526b25c47a 100644 --- a/src/Microsoft.AspNetCore.Routing/Tree/TreeRouteBuilder.cs +++ b/src/Microsoft.AspNetCore.Routing/Tree/TreeRouteBuilder.cs @@ -221,14 +221,13 @@ namespace Microsoft.AspNetCore.Routing.Tree foreach (var entry in InboundEntries) { - UrlMatchingTree tree; - if (!trees.TryGetValue(entry.Order, out tree)) + if (!trees.TryGetValue(entry.Order, out var tree)) { tree = new UrlMatchingTree(entry.Order); trees.Add(entry.Order, tree); } - AddEntryToTree(tree, entry); + UrlMatchingTree.AddEntryToTree(tree, entry); } return new TreeRouter( @@ -249,167 +248,5 @@ namespace Microsoft.AspNetCore.Routing.Tree InboundEntries.Clear(); OutboundEntries.Clear(); } - - internal static void AddEntryToTree(UrlMatchingTree tree, InboundRouteEntry entry) - { - // The url matching tree represents all the routes asociated with a given - // order. Each node in the tree represents all the different categories - // a segment can have for which there is a defined inbound route entry. - // Each node contains a set of Matches that indicate all the routes for which - // a URL is a potential match. This list contains the routes with the same - // number of segments and the routes with the same number of segments plus an - // additional catch all parameter (as it can be empty). - // For example, for a set of routes like: - // 'Customer/Index/{id}' - // '{Controller}/{Action}/{*parameters}' - // - // The route tree will look like: - // Root -> - // Literals: Customer -> - // Literals: Index -> - // Parameters: {id} - // Matches: 'Customer/Index/{id}' - // Parameters: {Controller} -> - // Parameters: {Action} -> - // Matches: '{Controller}/{Action}/{*parameters}' - // CatchAlls: {*parameters} - // Matches: '{Controller}/{Action}/{*parameters}' - // - // When the tree router tries to match a route, it iterates the list of url matching trees - // in ascending order. For each tree it traverses each node starting from the root in the - // following order: Literals, constrained parameters, parameters, constrained catch all routes, catch alls. - // When it gets to a node of the same length as the route its trying to match, it simply looks at the list of - // candidates (which is in precence order) and tries to match the url against it. - // - - 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]; - 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; - } - - // We accept templates that have intermediate optional values, but we ignore - // those values for route matching. For that reason, we need to add the entry - // to the list of matches, only if the remaining segments are optional. For example: - // /{controller}/{action=Index}/{id} will be equivalent to /{controller}/{action}/{id} - // for the purposes of route matching. - if (part.IsParameter && - RemainingSegmentsAreOptional(entry.RouteTemplate.Segments, i)) - { - current.Matches.Add(new InboundMatch() { Entry = entry, TemplateMatcher = matcher }); - } - - 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) { IsCatchAll = true }; - } - - current = current.ConstrainedCatchAlls; - continue; - } - - if (part.IsParameter && part.IsCatchAll) - { - if (current.CatchAlls == null) - { - current.CatchAlls = new UrlMatchingNode(length: i + 1) { IsCatchAll = true }; - } - - current = current.CatchAlls; - continue; - } - - Debug.Fail("We shouldn't get here."); - } - - current.Matches.Add(new InboundMatch() { Entry = entry, TemplateMatcher = matcher }); - current.Matches.Sort((x, y) => - { - var result = x.Entry.Precedence.CompareTo(y.Entry.Precedence); - return result == 0 ? x.Entry.RouteTemplate.TemplateText.CompareTo(y.Entry.RouteTemplate.TemplateText) : result; - }); - } - - private static bool RemainingSegmentsAreOptional(IList segments, int currentParameterIndex) - { - for (var i = currentParameterIndex; i < segments.Count; i++) - { - if (!segments[i].IsSimple) - { - // /{complex}-{segment} - return false; - } - - var part = segments[i].Parts[0]; - if (!part.IsParameter) - { - // /literal - return false; - } - - var isOptionlCatchAllOrHasDefaultValue = part.IsOptional || - part.IsCatchAll || - part.DefaultValue != null; - - if (!isOptionlCatchAllOrHasDefaultValue) - { - // /{parameter} - return false; - } - } - - return true; - } } }