Replace InnerAttributeRoute with TreeRouter

This commit is contained in:
Ajay Bhargav Baaskaran 2015-10-28 12:05:33 -07:00
parent b8d58133c3
commit b8b222b295
13 changed files with 612 additions and 258 deletions

View File

@ -12,11 +12,11 @@ namespace Microsoft.AspNet.Mvc.Internal.Routing
// A decision tree that matches link generation entries based on route data. // A decision tree that matches link generation entries based on route data.
public class LinkGenerationDecisionTree public class LinkGenerationDecisionTree
{ {
private readonly DecisionTreeNode<AttributeRouteLinkGenerationEntry> _root; private readonly DecisionTreeNode<TreeRouteLinkGenerationEntry> _root;
public LinkGenerationDecisionTree(IReadOnlyList<AttributeRouteLinkGenerationEntry> entries) public LinkGenerationDecisionTree(IReadOnlyList<TreeRouteLinkGenerationEntry> entries)
{ {
_root = DecisionTreeBuilder<AttributeRouteLinkGenerationEntry>.GenerateTree( _root = DecisionTreeBuilder<TreeRouteLinkGenerationEntry>.GenerateTree(
entries, entries,
new AttributeRouteLinkGenerationEntryClassifier()); new AttributeRouteLinkGenerationEntryClassifier());
} }
@ -57,7 +57,7 @@ namespace Microsoft.AspNet.Mvc.Internal.Routing
private void Walk( private void Walk(
List<LinkGenerationMatch> results, List<LinkGenerationMatch> results,
VirtualPathContext context, VirtualPathContext context,
DecisionTreeNode<AttributeRouteLinkGenerationEntry> node, DecisionTreeNode<TreeRouteLinkGenerationEntry> node,
bool isFallbackPath) bool isFallbackPath)
{ {
// Any entries in node.Matches have had all their required values satisfied, so add them // 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; object value;
if (context.Values.TryGetValue(key, out value)) if (context.Values.TryGetValue(key, out value))
{ {
DecisionTreeNode<AttributeRouteLinkGenerationEntry> branch; DecisionTreeNode<TreeRouteLinkGenerationEntry> branch;
if (criterion.Branches.TryGetValue(value ?? string.Empty, out branch)) if (criterion.Branches.TryGetValue(value ?? string.Empty, out branch))
{ {
Walk(results, context, branch, isFallbackPath); 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 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 // if an ambient value was supplied. The path explored with the empty value is considered
// the fallback path. // the fallback path.
DecisionTreeNode<AttributeRouteLinkGenerationEntry> branch; DecisionTreeNode<TreeRouteLinkGenerationEntry> branch;
if (context.AmbientValues.TryGetValue(key, out value) && if (context.AmbientValues.TryGetValue(key, out value) &&
!criterion.Branches.Comparer.Equals(value, string.Empty)) !criterion.Branches.Comparer.Equals(value, string.Empty))
{ {
@ -104,7 +104,7 @@ namespace Microsoft.AspNet.Mvc.Internal.Routing
} }
} }
private class AttributeRouteLinkGenerationEntryClassifier : IClassifier<AttributeRouteLinkGenerationEntry> private class AttributeRouteLinkGenerationEntryClassifier : IClassifier<TreeRouteLinkGenerationEntry>
{ {
public AttributeRouteLinkGenerationEntryClassifier() public AttributeRouteLinkGenerationEntryClassifier()
{ {
@ -113,7 +113,7 @@ namespace Microsoft.AspNet.Mvc.Internal.Routing
public IEqualityComparer<object> ValueComparer { get; private set; } public IEqualityComparer<object> ValueComparer { get; private set; }
public IDictionary<string, DecisionCriterionValue> GetCriteria(AttributeRouteLinkGenerationEntry item) public IDictionary<string, DecisionCriterionValue> GetCriteria(TreeRouteLinkGenerationEntry item)
{ {
var results = new Dictionary<string, DecisionCriterionValue>(StringComparer.OrdinalIgnoreCase); var results = new Dictionary<string, DecisionCriterionValue>(StringComparer.OrdinalIgnoreCase);
foreach (var kvp in item.RequiredLinkValues) foreach (var kvp in item.RequiredLinkValues)

View File

@ -8,15 +8,15 @@ namespace Microsoft.AspNet.Mvc.Internal.Routing
public struct LinkGenerationMatch public struct LinkGenerationMatch
{ {
private readonly bool _isFallbackMatch; 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; _entry = entry;
_isFallbackMatch = isFallbackMatch; _isFallbackMatch = isFallbackMatch;
} }
public AttributeRouteLinkGenerationEntry Entry { get { return _entry; } } public TreeRouteLinkGenerationEntry Entry { get { return _entry; } }
public bool IsFallbackMatch { get { return _isFallbackMatch; } } public bool IsFallbackMatch { get { return _isFallbackMatch; } }
} }

View File

@ -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; using Microsoft.Extensions.Logging;
namespace Microsoft.AspNet.Mvc.Logging namespace Microsoft.AspNet.Mvc.Logging
{ {
internal static class InnerAttributeRouteLoggerExtensions internal static class TreeRouterLoggerExtensions
{ {
private static readonly Action<ILogger, string, string, Exception> _matchedRouteName; private static readonly Action<ILogger, string, string, Exception> _matchedRouteName;
static InnerAttributeRouteLoggerExtensions() static TreeRouterLoggerExtensions()
{ {
_matchedRouteName = LoggerMessage.Define<string, string>( _matchedRouteName = LoggerMessage.Define<string, string>(
LogLevel.Verbose, LogLevel.Verbose,

View File

@ -19,12 +19,10 @@ namespace Microsoft.AspNet.Mvc.Routing
private readonly IRouter _target; private readonly IRouter _target;
private readonly IActionDescriptorsCollectionProvider _actionDescriptorsCollectionProvider; private readonly IActionDescriptorsCollectionProvider _actionDescriptorsCollectionProvider;
private readonly IInlineConstraintResolver _constraintResolver; private readonly IInlineConstraintResolver _constraintResolver;
// These loggers are used by the inner route, keep them around to avoid re-creating.
private readonly ILogger _routeLogger; private readonly ILogger _routeLogger;
private readonly ILogger _constraintLogger; private readonly ILogger _constraintLogger;
private InnerAttributeRoute _inner; private TreeRouter _router;
public AttributeRoute( public AttributeRoute(
IRouter target, IRouter target,
@ -56,48 +54,48 @@ namespace Microsoft.AspNet.Mvc.Routing
_actionDescriptorsCollectionProvider = actionDescriptorsCollectionProvider; _actionDescriptorsCollectionProvider = actionDescriptorsCollectionProvider;
_constraintResolver = constraintResolver; _constraintResolver = constraintResolver;
_routeLogger = loggerFactory.CreateLogger<InnerAttributeRoute>(); _routeLogger = loggerFactory.CreateLogger<TreeRouter>();
_constraintLogger = loggerFactory.CreateLogger(typeof(RouteConstraintMatcher).FullName); _constraintLogger = loggerFactory.CreateLogger(typeof(RouteConstraintMatcher).FullName);
} }
/// <inheritdoc /> /// <inheritdoc />
public VirtualPathData GetVirtualPath(VirtualPathContext context) public VirtualPathData GetVirtualPath(VirtualPathContext context)
{ {
var route = GetInnerRoute(); var router = GetTreeRouter();
return route.GetVirtualPath(context); return router.GetVirtualPath(context);
} }
/// <inheritdoc /> /// <inheritdoc />
public Task RouteAsync(RouteContext context) public Task RouteAsync(RouteContext context)
{ {
var route = GetInnerRoute(); var router = GetTreeRouter();
return route.RouteAsync(context); return router.RouteAsync(context);
} }
private InnerAttributeRoute GetInnerRoute() private TreeRouter GetTreeRouter()
{ {
var actions = _actionDescriptorsCollectionProvider.ActionDescriptors; 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. // 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); var routeInfos = GetRouteInfos(_constraintResolver, actions.Items);
// We're creating one AttributeRouteGenerationEntry per action. This allows us to match the intended // We're creating one AttributeRouteGenerationEntry per action. This allows us to match the intended
// action by expected route values, and then use the TemplateBinder to generate the link. // action by expected route values, and then use the TemplateBinder to generate the link.
var generationEntries = new List<AttributeRouteLinkGenerationEntry>();
foreach (var routeInfo in routeInfos) foreach (var routeInfo in routeInfos)
{ {
generationEntries.Add(new AttributeRouteLinkGenerationEntry() routeBuilder.Add(new TreeRouteLinkGenerationEntry()
{ {
Binder = new TemplateBinder(routeInfo.ParsedTemplate, routeInfo.Defaults), Binder = new TemplateBinder(routeInfo.ParsedTemplate, routeInfo.Defaults),
Defaults = 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, // 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. // so we only need to hang on to a single instance of the RouteInfo for each group.
var distinctRouteInfosByGroup = GroupRouteInfosByGroupId(routeInfos); var distinctRouteInfosByGroup = GroupRouteInfosByGroupId(routeInfos);
var matchingEntries = new List<AttributeRouteMatchingEntry>();
foreach (var routeInfo in distinctRouteInfosByGroup) foreach (var routeInfo in distinctRouteInfosByGroup)
{ {
matchingEntries.Add(new AttributeRouteMatchingEntry() routeBuilder.Add(new TreeRouteMatchingEntry()
{ {
Order = routeInfo.Order, Order = routeInfo.Order,
Precedence = routeInfo.MatchPrecedence, Precedence = routeInfo.MatchPrecedence,
Target = _target, Target = _target,
RouteName = routeInfo.Name, RouteName = routeInfo.Name,
RouteTemplate = routeInfo.RouteTemplate, RouteTemplate = TemplateParser.Parse(routeInfo.RouteTemplate),
TemplateMatcher = new TemplateMatcher( TemplateMatcher = new TemplateMatcher(
routeInfo.ParsedTemplate, routeInfo.ParsedTemplate,
new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase) new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase)
@ -136,13 +133,7 @@ namespace Microsoft.AspNet.Mvc.Routing
}); });
} }
return new InnerAttributeRoute( return routeBuilder.Build(actions.Version);
_target,
matchingEntries,
generationEntries,
_routeLogger,
_constraintLogger,
actions.Version);
} }
private static IEnumerable<RouteInfo> GroupRouteInfosByGroupId(List<RouteInfo> routeInfos) private static IEnumerable<RouteInfo> GroupRouteInfosByGroupId(List<RouteInfo> routeInfos)

View File

@ -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<TreeRouteLinkGenerationEntry> _generatingEntries;
private readonly List<TreeRouteMatchingEntry> _matchingEntries;
private readonly ILogger _logger;
private readonly ILogger _constraintLogger;
public TreeRouteBuilder(IRouter target, ILogger routeLogger, ILogger constraintLogger)
{
_target = target;
_generatingEntries = new List<TreeRouteLinkGenerationEntry>();
_matchingEntries = new List<TreeRouteMatchingEntry>();
_logger = routeLogger;
_constraintLogger = constraintLogger;
}
public void Add(TreeRouteLinkGenerationEntry entry)
{
_generatingEntries.Add(entry);
}
public void Add(TreeRouteMatchingEntry entry)
{
_matchingEntries.Add(entry);
}
public TreeRouter Build(int version)
{
var trees = new Dictionary<int, UrlMatchingTree>();
foreach (var entry in _matchingEntries)
{
UrlMatchingTree tree;
if (!trees.TryGetValue(entry.Order, out tree))
{
tree = new UrlMatchingTree(entry.Order);
trees.Add(entry.Order, tree);
}
AddEntryToTree(tree, entry);
}
return new TreeRouter(
_target,
trees.Values.OrderBy(tree => tree.Order).ToArray(),
_generatingEntries,
_logger,
_constraintLogger,
version);
}
public void Clear()
{
_generatingEntries.Clear();
_matchingEntries.Clear();
}
private void AddEntryToTree(UrlMatchingTree tree, TreeRouteMatchingEntry entry)
{
var current = tree.Root;
for (var i = 0; i < entry.RouteTemplate.Segments.Count; i++)
{
var segment = entry.RouteTemplate.Segments[i];
if (!segment.IsSimple)
{
// Treat complex segments as a constrained parameter
if (current.ConstrainedParameters == null)
{
current.ConstrainedParameters = new UrlMatchingNode(length: i + 1);
}
current = current.ConstrainedParameters;
continue;
}
Debug.Assert(segment.Parts.Count == 1);
var part = segment.Parts[0];
if (part.IsLiteral)
{
UrlMatchingNode next;
if (!current.Literals.TryGetValue(part.Text, out next))
{
next = new UrlMatchingNode(length: i + 1);
current.Literals.Add(part.Text, next);
}
current = next;
continue;
}
if (part.IsParameter && (part.IsOptional || part.IsCatchAll))
{
current.Matches.Add(entry);
}
if (part.IsParameter && part.InlineConstraints.Any() && !part.IsCatchAll)
{
if (current.ConstrainedParameters == null)
{
current.ConstrainedParameters = new UrlMatchingNode(length: i + 1);
}
current = current.ConstrainedParameters;
continue;
}
if (part.IsParameter && !part.IsCatchAll)
{
if (current.Parameters == null)
{
current.Parameters = new UrlMatchingNode(length: i + 1);
}
current = current.Parameters;
continue;
}
if (part.IsParameter && part.InlineConstraints.Any() && part.IsCatchAll)
{
if (current.ConstrainedCatchAlls == null)
{
current.ConstrainedCatchAlls = new UrlMatchingNode(length: i + 1);
}
current = current.ConstrainedCatchAlls;
continue;
}
if (part.IsParameter && part.IsCatchAll)
{
if (current.CatchAlls == null)
{
current.CatchAlls = new UrlMatchingNode(length: i + 1);
}
current = current.CatchAlls;
continue;
}
Debug.Fail("We shouldn't get here.");
}
current.Matches.Add(entry);
current.Matches.Sort((x, y) =>
{
var result = x.Precedence.CompareTo(y.Precedence);
return result == 0 ? x.RouteTemplate.Template.CompareTo(y.RouteTemplate.Template) : result;
});
}
}
}

View File

@ -8,10 +8,10 @@ using Microsoft.AspNet.Routing.Template;
namespace Microsoft.AspNet.Mvc.Routing namespace Microsoft.AspNet.Mvc.Routing
{ {
/// <summary> /// <summary>
/// Used to build an <see cref="InnerAttributeRoute"/>. Represents an individual URL-generating route that will be /// Used to build a <see cref="TreeRouter"/>. Represents an individual URL-generating route that will be
/// aggregated into the <see cref="InnerAttributeRoute"/>. /// aggregated into the <see cref="TreeRouter"/>.
/// </summary> /// </summary>
public class AttributeRouteLinkGenerationEntry public class TreeRouteLinkGenerationEntry
{ {
/// <summary> /// <summary>
/// The <see cref="TemplateBinder"/>. /// The <see cref="TemplateBinder"/>.

View File

@ -9,10 +9,10 @@ using Microsoft.Extensions.Logging;
namespace Microsoft.AspNet.Mvc.Routing namespace Microsoft.AspNet.Mvc.Routing
{ {
/// <summary> /// <summary>
/// Used to build an <see cref="InnerAttributeRoute"/>. Represents an individual URL-matching route that will be /// Used to build an <see cref="TreeRouter"/>. Represents an individual URL-matching route that will be
/// aggregated into the <see cref="InnerAttributeRoute"/>. /// aggregated into the <see cref="TreeRouter"/>.
/// </summary> /// </summary>
public class AttributeRouteMatchingEntry public class TreeRouteMatchingEntry
{ {
/// <summary> /// <summary>
/// The order of the template. /// The order of the template.
@ -28,7 +28,7 @@ namespace Microsoft.AspNet.Mvc.Routing
public string RouteName { get; set; } public string RouteName { get; set; }
public string RouteTemplate { get; set; } public RouteTemplate RouteTemplate { get; set; }
public TemplateMatcher TemplateMatcher { get; set; } public TemplateMatcher TemplateMatcher { get; set; }

View File

@ -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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -9,7 +10,8 @@ using Microsoft.AspNet.Mvc.Core;
using Microsoft.AspNet.Mvc.Internal.Routing; using Microsoft.AspNet.Mvc.Internal.Routing;
using Microsoft.AspNet.Mvc.Logging; using Microsoft.AspNet.Mvc.Logging;
using Microsoft.AspNet.Routing; using Microsoft.AspNet.Routing;
using Microsoft.AspNet.Routing.Template; using Microsoft.AspNet.Routing.Internal;
using Microsoft.Extensions.Internal;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Microsoft.AspNet.Mvc.Routing namespace Microsoft.AspNet.Mvc.Routing
@ -17,26 +19,31 @@ namespace Microsoft.AspNet.Mvc.Routing
/// <summary> /// <summary>
/// An <see cref="IRouter"/> implementation for attribute routing. /// An <see cref="IRouter"/> implementation for attribute routing.
/// </summary> /// </summary>
public class InnerAttributeRoute : IRouter public class TreeRouter : IRouter
{ {
private readonly IRouter _next; private readonly IRouter _next;
private readonly LinkGenerationDecisionTree _linkGenerationTree; private readonly LinkGenerationDecisionTree _linkGenerationTree;
private readonly AttributeRouteMatchingEntry[] _matchingEntries; private readonly UrlMatchingTree[] _trees;
private readonly IDictionary<string, AttributeRouteLinkGenerationEntry> _namedEntries; private readonly IDictionary<string, TreeRouteLinkGenerationEntry> _namedEntries;
private ILogger _logger; private readonly ILogger _logger;
private ILogger _constraintLogger; private readonly ILogger _constraintLogger;
/// <summary> /// <summary>
/// Creates a new <see cref="InnerAttributeRoute"/>. /// Creates a new <see cref="TreeRouter"/>.
/// </summary> /// </summary>
/// <param name="next">The next router. Invoked when a route entry matches.</param> /// <param name="next">The next router. Invoked when a route entry matches.</param>
/// <param name="entries">The set of route entries.</param> /// <param name="trees">The list of <see cref="UrlMatchingTree"/> that contains the route entries.</param>
public InnerAttributeRoute( /// <param name="linkGenerationEntries">The set of <see cref="TreeRouteLinkGenerationEntry"/>.</param>
/// <param name="routeLogger">The <see cref="ILogger"/> instance.</param>
/// <param name="constraintLogger">The <see cref="ILogger"/> instance used
/// in <see cref="RouteConstraintMatcher"/>.</param>
/// <param name="version">The version of this route.</param>
public TreeRouter(
IRouter next, IRouter next,
IEnumerable<AttributeRouteMatchingEntry> matchingEntries, UrlMatchingTree[] trees,
IEnumerable<AttributeRouteLinkGenerationEntry> linkGenerationEntries, IEnumerable<TreeRouteLinkGenerationEntry> linkGenerationEntries,
ILogger logger, ILogger routeLogger,
ILogger constraintLogger, ILogger constraintLogger,
int version) int version)
{ {
@ -45,9 +52,9 @@ namespace Microsoft.AspNet.Mvc.Routing
throw new ArgumentNullException(nameof(next)); throw new ArgumentNullException(nameof(next));
} }
if (matchingEntries == null) if (trees == null)
{ {
throw new ArgumentNullException(nameof(matchingEntries)); throw new ArgumentNullException(nameof(trees));
} }
if (linkGenerationEntries == null) if (linkGenerationEntries == null)
@ -55,9 +62,9 @@ namespace Microsoft.AspNet.Mvc.Routing
throw new ArgumentNullException(nameof(linkGenerationEntries)); throw new ArgumentNullException(nameof(linkGenerationEntries));
} }
if (logger == null) if (routeLogger == null)
{ {
throw new ArgumentNullException(nameof(logger)); throw new ArgumentNullException(nameof(routeLogger));
} }
if (constraintLogger == null) if (constraintLogger == null)
@ -66,23 +73,11 @@ namespace Microsoft.AspNet.Mvc.Routing
} }
_next = next; _next = next;
_logger = logger; _trees = trees;
_logger = routeLogger;
_constraintLogger = constraintLogger; _constraintLogger = constraintLogger;
Version = version; var namedEntries = new Dictionary<string, TreeRouteLinkGenerationEntry>(
// 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<string, AttributeRouteLinkGenerationEntry>(
StringComparer.OrdinalIgnoreCase); StringComparer.OrdinalIgnoreCase);
foreach (var entry in linkGenerationEntries) foreach (var entry in linkGenerationEntries)
@ -96,7 +91,7 @@ namespace Microsoft.AspNet.Mvc.Routing
// We only need to keep one AttributeRouteLinkGenerationEntry per route template // 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 // so in case two entries have the same name and the same template we only keep
// the first entry. // the first entry.
AttributeRouteLinkGenerationEntry namedEntry = null; TreeRouteLinkGenerationEntry namedEntry = null;
if (namedEntries.TryGetValue(entry.Name, out namedEntry) && if (namedEntries.TryGetValue(entry.Name, out namedEntry) &&
!namedEntry.TemplateText.Equals(entry.TemplateText, StringComparison.OrdinalIgnoreCase)) !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. // The decision tree will take care of ordering for these entries.
_linkGenerationTree = new LinkGenerationDecisionTree(linkGenerationEntries.ToArray()); _linkGenerationTree = new LinkGenerationDecisionTree(linkGenerationEntries.ToArray());
Version = version;
} }
/// <summary> /// <summary>
@ -122,67 +119,6 @@ namespace Microsoft.AspNet.Mvc.Routing
/// </summary> /// </summary>
public int Version { get; } public int Version { get; }
/// <inheritdoc />
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;
}
}
}
/// <inheritdoc /> /// <inheritdoc />
public VirtualPathData GetVirtualPath(VirtualPathContext context) public VirtualPathData GetVirtualPath(VirtualPathContext context)
{ {
@ -215,9 +151,232 @@ namespace Microsoft.AspNet.Mvc.Routing
return null; return null;
} }
/// <inheritdoc />
public async Task RouteAsync(RouteContext context)
{
foreach (var tree in _trees)
{
var tokenizer = new PathTokenizer(context.HttpContext.Request.Path);
var enumerator = tokenizer.GetEnumerator();
var root = tree.Root;
var treeEnumerator = new TreeEnumerator(root, tokenizer);
while (treeEnumerator.MoveNext())
{
var node = treeEnumerator.Current;
foreach (var item in node.Matches)
{
var values = item.TemplateMatcher.Match(context.HttpContext.Request.Path);
if (values == null)
{
continue;
}
var match = new TemplateMatch(item, values);
var oldRouteData = context.RouteData;
var newRouteData = new RouteData(oldRouteData);
newRouteData.Routers.Add(match.Entry.Target);
MergeValues(newRouteData.Values, match.Values);
if (!RouteConstraintMatcher.Match(
match.Entry.Constraints,
newRouteData.Values,
context.HttpContext,
this,
RouteDirection.IncomingRequest,
_constraintLogger))
{
return;
}
_logger.MatchedRouteName(match.Entry.RouteName, match.Entry.RouteTemplate.Template);
context.RouteData = newRouteData;
try
{
await match.Entry.Target.RouteAsync(context);
}
finally
{
if (!context.IsHandled)
{
// Restore the original values to prevent polluting the route data.
context.RouteData = oldRouteData;
}
}
if (context.IsHandled)
{
return;
}
}
}
}
}
private struct TreeEnumerator : IEnumerator<UrlMatchingNode>
{
private readonly Stack<UrlMatchingNode> _stack;
private readonly PathTokenizer _tokenizer;
private int _segmentIndex;
public TreeEnumerator(UrlMatchingNode root, PathTokenizer tokenizer)
{
_stack = new Stack<UrlMatchingNode>();
_tokenizer = tokenizer;
Current = null;
_segmentIndex = -1;
_stack.Push(root);
}
public UrlMatchingNode Current { get; private set; }
object IEnumerator.Current => Current;
public void Dispose()
{
}
public bool MoveNext()
{
if (_stack == null)
{
return false;
}
while (_stack.Count > 0)
{
var next = _stack.Pop();
if (++_segmentIndex >= _tokenizer.Count)
{
_segmentIndex--;
if (next.Matches.Count > 0)
{
Current = next;
return true;
}
}
if (_tokenizer.Count == 0)
{
continue;
}
if (next.CatchAlls != null)
{
_stack.Push(next.CatchAlls);
}
if (next.ConstrainedCatchAlls != null)
{
_stack.Push(next.ConstrainedCatchAlls);
}
if (next.Parameters != null)
{
_stack.Push(next.Parameters);
}
if (next.ConstrainedParameters != null)
{
_stack.Push(next.ConstrainedParameters);
}
if (next.Literals.Count > 0)
{
UrlMatchingNode node;
if (next.Literals.TryGetValue(_tokenizer[_segmentIndex].Value, out node))
{
_stack.Push(node);
}
}
}
return false;
}
public void Reset()
{
_stack.Clear();
Current = null;
_segmentIndex = -1;
}
}
private static void MergeValues(
IDictionary<string, object> destination,
IDictionary<string, object> values)
{
foreach (var kvp in values)
{
if (kvp.Value != null)
{
// This will replace the original value for the specified key.
// Values from the matched route will take preference over previous
// data in the route context.
destination[kvp.Key] = kvp.Value;
}
}
}
private struct TemplateMatch : IEquatable<TemplateMatch>
{
public TemplateMatch(TreeRouteMatchingEntry entry, IDictionary<string, object> values)
{
Entry = entry;
Values = values;
}
public TreeRouteMatchingEntry Entry { get; }
public IDictionary<string, object> Values { get; }
public override bool Equals(object obj)
{
if (obj is TemplateMatch)
{
return Equals((TemplateMatch)obj);
}
return false;
}
public bool Equals(TemplateMatch other)
{
return
object.ReferenceEquals(Entry, other.Entry) &&
object.ReferenceEquals(Values, other.Values);
}
public override int GetHashCode()
{
var hash = new HashCodeCombiner();
hash.Add(Entry);
hash.Add(Values);
return hash.CombinedHash;
}
public static bool operator ==(TemplateMatch left, TemplateMatch right)
{
return left.Equals(right);
}
public static bool operator !=(TemplateMatch left, TemplateMatch right)
{
return !left.Equals(right);
}
}
private VirtualPathData GetVirtualPathForNamedRoute(VirtualPathContext context) private VirtualPathData GetVirtualPathForNamedRoute(VirtualPathContext context)
{ {
AttributeRouteLinkGenerationEntry entry; TreeRouteLinkGenerationEntry entry;
if (_namedEntries.TryGetValue(context.RouteName, out entry)) if (_namedEntries.TryGetValue(context.RouteName, out entry))
{ {
var path = GenerateVirtualPath(context, entry); var path = GenerateVirtualPath(context, entry);
@ -230,7 +389,7 @@ namespace Microsoft.AspNet.Mvc.Routing
return null; 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 // 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 // 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); return new VirtualPathData(this, path);
} }
private static void MergeValues(
IDictionary<string, object> destination,
IDictionary<string, object> values)
{
foreach (var kvp in values)
{
if (kvp.Value != null)
{
// This will replace the original value for the specified key.
// Values from the matched route will take preference over previous
// data in the route context.
destination[kvp.Key] = kvp.Value;
}
}
}
private 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);
}
} }
} }

View File

@ -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<TreeRouteMatchingEntry>();
Literals = new Dictionary<string, UrlMatchingNode>(StringComparer.OrdinalIgnoreCase);
}
public int Length { get; }
// These entries are sorted by precedence then template
public List<TreeRouteMatchingEntry> Matches { get; }
public Dictionary<string, UrlMatchingNode> Literals { get; }
public UrlMatchingNode ConstrainedParameters { get; set; }
public UrlMatchingNode Parameters { get; set; }
public UrlMatchingNode ConstrainedCatchAlls { get; set; }
public UrlMatchingNode CatchAlls { get; set; }
}
}

View File

@ -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);
}
}

View File

@ -19,6 +19,10 @@
"version": "1.0.0-*", "version": "1.0.0-*",
"type": "build" "type": "build"
}, },
"Microsoft.Extensions.HashCodeCombiner.Sources": {
"type": "build",
"version": "1.0.0-*"
},
"Microsoft.Extensions.Logging.Abstractions": "1.0.0-*", "Microsoft.Extensions.Logging.Abstractions": "1.0.0-*",
"Microsoft.Extensions.MemoryPool": "1.0.0-*", "Microsoft.Extensions.MemoryPool": "1.0.0-*",
"Microsoft.Extensions.PropertyActivator.Sources": { "Microsoft.Extensions.PropertyActivator.Sources": {

View File

@ -16,7 +16,7 @@ namespace Microsoft.AspNet.Mvc.Internal.Routing
public void SelectSingleEntry_NoCriteria() public void SelectSingleEntry_NoCriteria()
{ {
// Arrange // Arrange
var entries = new List<AttributeRouteLinkGenerationEntry>(); var entries = new List<TreeRouteLinkGenerationEntry>();
var entry = CreateEntry(new { }); var entry = CreateEntry(new { });
entries.Add(entry); entries.Add(entry);
@ -36,7 +36,7 @@ namespace Microsoft.AspNet.Mvc.Internal.Routing
public void SelectSingleEntry_MultipleCriteria() public void SelectSingleEntry_MultipleCriteria()
{ {
// Arrange // Arrange
var entries = new List<AttributeRouteLinkGenerationEntry>(); var entries = new List<TreeRouteLinkGenerationEntry>();
var entry = CreateEntry(new { controller = "Store", action = "Buy" }); var entry = CreateEntry(new { controller = "Store", action = "Buy" });
entries.Add(entry); entries.Add(entry);
@ -56,7 +56,7 @@ namespace Microsoft.AspNet.Mvc.Internal.Routing
public void SelectSingleEntry_MultipleCriteria_AmbientValues() public void SelectSingleEntry_MultipleCriteria_AmbientValues()
{ {
// Arrange // Arrange
var entries = new List<AttributeRouteLinkGenerationEntry>(); var entries = new List<TreeRouteLinkGenerationEntry>();
var entry = CreateEntry(new { controller = "Store", action = "Buy" }); var entry = CreateEntry(new { controller = "Store", action = "Buy" });
entries.Add(entry); entries.Add(entry);
@ -78,7 +78,7 @@ namespace Microsoft.AspNet.Mvc.Internal.Routing
public void SelectSingleEntry_MultipleCriteria_Replaced() public void SelectSingleEntry_MultipleCriteria_Replaced()
{ {
// Arrange // Arrange
var entries = new List<AttributeRouteLinkGenerationEntry>(); var entries = new List<TreeRouteLinkGenerationEntry>();
var entry = CreateEntry(new { controller = "Store", action = "Buy" }); var entry = CreateEntry(new { controller = "Store", action = "Buy" });
entries.Add(entry); entries.Add(entry);
@ -102,7 +102,7 @@ namespace Microsoft.AspNet.Mvc.Internal.Routing
public void SelectSingleEntry_MultipleCriteria_AmbientValue_Ignored() public void SelectSingleEntry_MultipleCriteria_AmbientValue_Ignored()
{ {
// Arrange // Arrange
var entries = new List<AttributeRouteLinkGenerationEntry>(); var entries = new List<TreeRouteLinkGenerationEntry>();
var entry = CreateEntry(new { controller = "Store", action = (string)null }); var entry = CreateEntry(new { controller = "Store", action = (string)null });
entries.Add(entry); entries.Add(entry);
@ -126,7 +126,7 @@ namespace Microsoft.AspNet.Mvc.Internal.Routing
public void SelectSingleEntry_MultipleCriteria_NoMatch() public void SelectSingleEntry_MultipleCriteria_NoMatch()
{ {
// Arrange // Arrange
var entries = new List<AttributeRouteLinkGenerationEntry>(); var entries = new List<TreeRouteLinkGenerationEntry>();
var entry = CreateEntry(new { controller = "Store", action = "Buy" }); var entry = CreateEntry(new { controller = "Store", action = "Buy" });
entries.Add(entry); entries.Add(entry);
@ -146,7 +146,7 @@ namespace Microsoft.AspNet.Mvc.Internal.Routing
public void SelectSingleEntry_MultipleCriteria_AmbientValue_NoMatch() public void SelectSingleEntry_MultipleCriteria_AmbientValue_NoMatch()
{ {
// Arrange // Arrange
var entries = new List<AttributeRouteLinkGenerationEntry>(); var entries = new List<TreeRouteLinkGenerationEntry>();
var entry = CreateEntry(new { controller = "Store", action = "Buy" }); var entry = CreateEntry(new { controller = "Store", action = "Buy" });
entries.Add(entry); entries.Add(entry);
@ -168,7 +168,7 @@ namespace Microsoft.AspNet.Mvc.Internal.Routing
public void SelectMultipleEntries_OneDoesntMatch() public void SelectMultipleEntries_OneDoesntMatch()
{ {
// Arrange // Arrange
var entries = new List<AttributeRouteLinkGenerationEntry>(); var entries = new List<TreeRouteLinkGenerationEntry>();
var entry1 = CreateEntry(new { controller = "Store", action = "Buy" }); var entry1 = CreateEntry(new { controller = "Store", action = "Buy" });
entries.Add(entry1); entries.Add(entry1);
@ -193,7 +193,7 @@ namespace Microsoft.AspNet.Mvc.Internal.Routing
public void SelectMultipleEntries_BothMatch_CriteriaSubset() public void SelectMultipleEntries_BothMatch_CriteriaSubset()
{ {
// Arrange // Arrange
var entries = new List<AttributeRouteLinkGenerationEntry>(); var entries = new List<TreeRouteLinkGenerationEntry>();
var entry1 = CreateEntry(new { controller = "Store", action = "Buy" }); var entry1 = CreateEntry(new { controller = "Store", action = "Buy" });
entries.Add(entry1); entries.Add(entry1);
@ -219,7 +219,7 @@ namespace Microsoft.AspNet.Mvc.Internal.Routing
public void SelectMultipleEntries_BothMatch_NonOverlappingCriteria() public void SelectMultipleEntries_BothMatch_NonOverlappingCriteria()
{ {
// Arrange // Arrange
var entries = new List<AttributeRouteLinkGenerationEntry>(); var entries = new List<TreeRouteLinkGenerationEntry>();
var entry1 = CreateEntry(new { controller = "Store", action = "Buy" }); var entry1 = CreateEntry(new { controller = "Store", action = "Buy" });
entries.Add(entry1); entries.Add(entry1);
@ -244,7 +244,7 @@ namespace Microsoft.AspNet.Mvc.Internal.Routing
public void SelectMultipleEntries_BothMatch_OrderedByOrder() public void SelectMultipleEntries_BothMatch_OrderedByOrder()
{ {
// Arrange // Arrange
var entries = new List<AttributeRouteLinkGenerationEntry>(); var entries = new List<TreeRouteLinkGenerationEntry>();
var entry1 = CreateEntry(new { controller = "Store", action = "Buy" }); var entry1 = CreateEntry(new { controller = "Store", action = "Buy" });
entry1.GenerationPrecedence = 0; entry1.GenerationPrecedence = 0;
@ -271,7 +271,7 @@ namespace Microsoft.AspNet.Mvc.Internal.Routing
public void SelectMultipleEntries_BothMatch_OrderedByPrecedence() public void SelectMultipleEntries_BothMatch_OrderedByPrecedence()
{ {
// Arrange // Arrange
var entries = new List<AttributeRouteLinkGenerationEntry>(); var entries = new List<TreeRouteLinkGenerationEntry>();
var entry1 = CreateEntry(new { controller = "Store", action = "Buy" }); var entry1 = CreateEntry(new { controller = "Store", action = "Buy" });
entry1.GenerationPrecedence = 1; entry1.GenerationPrecedence = 1;
@ -297,7 +297,7 @@ namespace Microsoft.AspNet.Mvc.Internal.Routing
public void SelectMultipleEntries_BothMatch_OrderedByTemplate() public void SelectMultipleEntries_BothMatch_OrderedByTemplate()
{ {
// Arrange // Arrange
var entries = new List<AttributeRouteLinkGenerationEntry>(); var entries = new List<TreeRouteLinkGenerationEntry>();
var entry1 = CreateEntry(new { controller = "Store", action = "Buy" }); var entry1 = CreateEntry(new { controller = "Store", action = "Buy" });
entry1.TemplateText = "a"; entry1.TemplateText = "a";
@ -318,9 +318,9 @@ namespace Microsoft.AspNet.Mvc.Internal.Routing
Assert.Equal(entries, matches); 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); entry.RequiredLinkValues = new RouteValueDictionary(requiredValues);
return entry; return entry;
} }

View File

@ -16,7 +16,7 @@ using Xunit;
namespace Microsoft.AspNet.Mvc.Routing namespace Microsoft.AspNet.Mvc.Routing
{ {
public class InnerAttributeRouteTest public class TreeRouterTest
{ {
[Theory] [Theory]
[InlineData("template/5", "template/{parameter:int}")] [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. // try to route the request, the route with a higher precedence gets tried first.
var matchingRoutes = new[] { secondRoute, firstRoute }; var matchingRoutes = new[] { secondRoute, firstRoute };
var linkGenerationEntries = Enumerable.Empty<AttributeRouteLinkGenerationEntry>(); var linkGenerationEntries = Enumerable.Empty<TreeRouteLinkGenerationEntry>();
var route = CreateAttributeRoute(next.Object, matchingRoutes, linkGenerationEntries); var route = CreateAttributeRoute(next.Object, matchingRoutes, linkGenerationEntries);
@ -102,7 +102,7 @@ namespace Microsoft.AspNet.Mvc.Routing
// relative order gets tried first. // relative order gets tried first.
var matchingRoutes = new[] { firstRoute, secondRoute }; var matchingRoutes = new[] { firstRoute, secondRoute };
var linkGenerationEntries = Enumerable.Empty<AttributeRouteLinkGenerationEntry>(); var linkGenerationEntries = Enumerable.Empty<TreeRouteLinkGenerationEntry>();
var route = CreateAttributeRoute(next.Object, matchingRoutes, linkGenerationEntries); 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. // we try to route the request, the route with the higher relative order gets tried first.
var matchingRoutes = new[] { firstRoute, secondRoute }; var matchingRoutes = new[] { firstRoute, secondRoute };
var linkGenerationEntries = Enumerable.Empty<AttributeRouteLinkGenerationEntry>(); var linkGenerationEntries = Enumerable.Empty<TreeRouteLinkGenerationEntry>();
var route = CreateAttributeRoute(next.Object, matchingRoutes, linkGenerationEntries); 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. // we try to route the request, the route with the higher template order gets tried first.
var matchingRoutes = new[] { secondRoute, firstRoute }; var matchingRoutes = new[] { secondRoute, firstRoute };
var linkGenerationEntries = Enumerable.Empty<AttributeRouteLinkGenerationEntry>(); var linkGenerationEntries = Enumerable.Empty<TreeRouteLinkGenerationEntry>();
var route = CreateAttributeRoute(next.Object, matchingRoutes, linkGenerationEntries); 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. // try to route the request, the route with a higher precedence gets tried first.
var matchingRoutes = new[] { firstRoute }; var matchingRoutes = new[] { firstRoute };
var linkGenerationEntries = Enumerable.Empty<AttributeRouteLinkGenerationEntry>(); var linkGenerationEntries = Enumerable.Empty<TreeRouteLinkGenerationEntry>();
var route = CreateAttributeRoute(next.Object, matchingRoutes, linkGenerationEntries); 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 // 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. // try to route the request, the route with a higher precedence gets tried first.
var matchingEntries = new[] { firstRoute }; var matchingEntries = new[] { firstRoute };
var linkGenerationEntries = Enumerable.Empty<AttributeRouteLinkGenerationEntry>(); var linkGenerationEntries = Enumerable.Empty<TreeRouteLinkGenerationEntry>();
var route = CreateAttributeRoute(next.Object, matchingEntries, linkGenerationEntries); var route = CreateAttributeRoute(next.Object, matchingEntries, linkGenerationEntries);
var context = CreateRouteContext(request); 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 // 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. // try to route the request, the route with a higher precedence gets tried first.
var matchingEntries = new[] { firstRoute }; var matchingEntries = new[] { firstRoute };
var linkGenerationEntries = Enumerable.Empty<AttributeRouteLinkGenerationEntry>(); var linkGenerationEntries = Enumerable.Empty<TreeRouteLinkGenerationEntry>();
var route = CreateAttributeRoute(next.Object, matchingEntries, linkGenerationEntries); var route = CreateAttributeRoute(next.Object, matchingEntries, linkGenerationEntries);
var context = CreateRouteContext(request); var context = CreateRouteContext(request);
@ -547,7 +547,7 @@ namespace Microsoft.AspNet.Mvc.Routing
}) })
.Returns((VirtualPathData)null); .Returns((VirtualPathData)null);
var matchingRoutes = Enumerable.Empty<AttributeRouteMatchingEntry>(); var matchingRoutes = Enumerable.Empty<TreeRouteMatchingEntry>();
var firstEntry = CreateGenerationEntry(firstTemplate, requiredValues: null); var firstEntry = CreateGenerationEntry(firstTemplate, requiredValues: null);
var secondEntry = CreateGenerationEntry(secondTemplate, requiredValues: null, order: 0); var secondEntry = CreateGenerationEntry(secondTemplate, requiredValues: null, order: 0);
@ -598,7 +598,7 @@ namespace Microsoft.AspNet.Mvc.Routing
}) })
.Returns((VirtualPathData)null); .Returns((VirtualPathData)null);
var matchingRoutes = Enumerable.Empty<AttributeRouteMatchingEntry>(); var matchingRoutes = Enumerable.Empty<TreeRouteMatchingEntry>();
var entry = CreateGenerationEntry(template, requiredValues: null); var entry = CreateGenerationEntry(template, requiredValues: null);
@ -658,7 +658,7 @@ namespace Microsoft.AspNet.Mvc.Routing
}) })
.Returns((VirtualPathData)null); .Returns((VirtualPathData)null);
var matchingRoutes = Enumerable.Empty<AttributeRouteMatchingEntry>(); var matchingRoutes = Enumerable.Empty<TreeRouteMatchingEntry>();
var firstRoute = CreateGenerationEntry(firstTemplate, requiredValues: null, order: 1); var firstRoute = CreateGenerationEntry(firstTemplate, requiredValues: null, order: 1);
var secondRoute = CreateGenerationEntry(secondTemplate, requiredValues: null, order: 0); var secondRoute = CreateGenerationEntry(secondTemplate, requiredValues: null, order: 0);
@ -704,7 +704,7 @@ namespace Microsoft.AspNet.Mvc.Routing
}) })
.Returns((VirtualPathData)null); .Returns((VirtualPathData)null);
var matchingRoutes = Enumerable.Empty<AttributeRouteMatchingEntry>(); var matchingRoutes = Enumerable.Empty<TreeRouteMatchingEntry>();
var firstRoute = CreateGenerationEntry(firstTemplate, requiredValues: null, order: 1); var firstRoute = CreateGenerationEntry(firstTemplate, requiredValues: null, order: 1);
var secondRoute = CreateGenerationEntry(secondTemplate, requiredValues: null, order: 0); var secondRoute = CreateGenerationEntry(secondTemplate, requiredValues: null, order: 0);
@ -749,7 +749,7 @@ namespace Microsoft.AspNet.Mvc.Routing
}) })
.Returns((VirtualPathData)null); .Returns((VirtualPathData)null);
var matchingRoutes = Enumerable.Empty<AttributeRouteMatchingEntry>(); var matchingRoutes = Enumerable.Empty<TreeRouteMatchingEntry>();
var firstRoute = CreateGenerationEntry(firstTemplate, requiredValues: null, order: 0); var firstRoute = CreateGenerationEntry(firstTemplate, requiredValues: null, order: 0);
var secondRoute = CreateGenerationEntry(secondTemplate, requiredValues: null, order: 0); var secondRoute = CreateGenerationEntry(secondTemplate, requiredValues: null, order: 0);
@ -778,7 +778,7 @@ namespace Microsoft.AspNet.Mvc.Routing
{ {
get get
{ {
var data = new TheoryData<IEnumerable<AttributeRouteLinkGenerationEntry>>(); var data = new TheoryData<IEnumerable<TreeRouteLinkGenerationEntry>>();
data.Add(new[] data.Add(new[]
{ {
CreateGenerationEntry("template", null, 0, "NamedEntry"), CreateGenerationEntry("template", null, 0, "NamedEntry"),
@ -806,9 +806,9 @@ namespace Microsoft.AspNet.Mvc.Routing
} }
[Theory] [Theory]
[MemberData(nameof(InnerAttributeRouteTest.NamedEntriesWithDifferentTemplates))] [MemberData(nameof(TreeRouterTest.NamedEntriesWithDifferentTemplates))]
public void AttributeRoute_CreateAttributeRoute_ThrowsIfDifferentEntriesHaveTheSameName( public void AttributeRoute_CreateAttributeRoute_ThrowsIfDifferentEntriesHaveTheSameName(
IEnumerable<AttributeRouteLinkGenerationEntry> namedEntries) IEnumerable<TreeRouteLinkGenerationEntry> namedEntries)
{ {
// Arrange // Arrange
string expectedExceptionMessage = "Two or more routes named 'NamedEntry' have different templates." + string expectedExceptionMessage = "Two or more routes named 'NamedEntry' have different templates." +
@ -817,18 +817,19 @@ namespace Microsoft.AspNet.Mvc.Routing
var next = new Mock<IRouter>().Object; var next = new Mock<IRouter>().Object;
var matchingEntries = Enumerable.Empty<AttributeRouteMatchingEntry>();
// Act // Act
var builder = new TreeRouteBuilder(next, NullLogger.Instance, NullLogger.Instance);
var exception = Assert.Throws<ArgumentException>( var exception = Assert.Throws<ArgumentException>(
"linkGenerationEntries", "linkGenerationEntries",
() => new InnerAttributeRoute( () =>
next, {
matchingEntries, foreach (var entry in namedEntries)
namedEntries, {
NullLogger.Instance, builder.Add(entry);
NullLogger.Instance, }
version: 1));
return builder.Build(version: 1);
});
Assert.Equal(expectedExceptionMessage, exception.Message, StringComparer.OrdinalIgnoreCase); Assert.Equal(expectedExceptionMessage, exception.Message, StringComparer.OrdinalIgnoreCase);
} }
@ -837,7 +838,7 @@ namespace Microsoft.AspNet.Mvc.Routing
{ {
get get
{ {
var data = new TheoryData<IEnumerable<AttributeRouteLinkGenerationEntry>>(); var data = new TheoryData<IEnumerable<TreeRouteLinkGenerationEntry>>();
data.Add(new[] data.Add(new[]
{ {
@ -866,9 +867,9 @@ namespace Microsoft.AspNet.Mvc.Routing
} }
[Theory] [Theory]
[MemberData(nameof(InnerAttributeRouteTest.NamedEntriesWithTheSameTemplate))] [MemberData(nameof(TreeRouterTest.NamedEntriesWithTheSameTemplate))]
public void AttributeRoute_GeneratesLink_ForMultipleNamedEntriesWithTheSameTemplate( public void AttributeRoute_GeneratesLink_ForMultipleNamedEntriesWithTheSameTemplate(
IEnumerable<AttributeRouteLinkGenerationEntry> namedEntries) IEnumerable<TreeRouteLinkGenerationEntry> namedEntries)
{ {
// Arrange // Arrange
var expectedLink = new PathString( var expectedLink = new PathString(
@ -885,7 +886,7 @@ namespace Microsoft.AspNet.Mvc.Routing
}) })
.Returns((VirtualPathData)null); .Returns((VirtualPathData)null);
var matchingEntries = Enumerable.Empty<AttributeRouteMatchingEntry>(); var matchingEntries = Enumerable.Empty<TreeRouteMatchingEntry>();
var route = CreateAttributeRoute(next.Object, matchingEntries, namedEntries); var route = CreateAttributeRoute(next.Object, matchingEntries, namedEntries);
@ -926,7 +927,7 @@ namespace Microsoft.AspNet.Mvc.Routing
// if it were an unnamed route. // if it were an unnamed route.
var linkGenerationEntries = new[] { namedEntry, unnamedEntry }; var linkGenerationEntries = new[] { namedEntry, unnamedEntry };
var matchingEntries = Enumerable.Empty<AttributeRouteMatchingEntry>(); var matchingEntries = Enumerable.Empty<TreeRouteMatchingEntry>();
var route = CreateAttributeRoute(next.Object, matchingEntries, linkGenerationEntries); var route = CreateAttributeRoute(next.Object, matchingEntries, linkGenerationEntries);
@ -966,7 +967,7 @@ namespace Microsoft.AspNet.Mvc.Routing
// if it were an unnamed route. // if it were an unnamed route.
var linkGenerationEntries = new[] { namedEntry, unnamedEntry }; var linkGenerationEntries = new[] { namedEntry, unnamedEntry };
var matchingEntries = Enumerable.Empty<AttributeRouteMatchingEntry>(); var matchingEntries = Enumerable.Empty<TreeRouteMatchingEntry>();
var route = CreateAttributeRoute(next.Object, matchingEntries, linkGenerationEntries); var route = CreateAttributeRoute(next.Object, matchingEntries, linkGenerationEntries);
@ -1006,7 +1007,7 @@ namespace Microsoft.AspNet.Mvc.Routing
// if it were an unnamed route. // if it were an unnamed route.
var linkGenerationEntries = new[] { namedEntry, unnamedEntry }; var linkGenerationEntries = new[] { namedEntry, unnamedEntry };
var matchingEntries = Enumerable.Empty<AttributeRouteMatchingEntry>(); var matchingEntries = Enumerable.Empty<TreeRouteMatchingEntry>();
var route = CreateAttributeRoute(next.Object, matchingEntries, linkGenerationEntries); var route = CreateAttributeRoute(next.Object, matchingEntries, linkGenerationEntries);
@ -1048,7 +1049,7 @@ namespace Microsoft.AspNet.Mvc.Routing
// if it were an unnamed route. // if it were an unnamed route.
var linkGenerationEntries = new[] { namedEntry, unnamedEntry }; var linkGenerationEntries = new[] { namedEntry, unnamedEntry };
var matchingEntries = Enumerable.Empty<AttributeRouteMatchingEntry>(); var matchingEntries = Enumerable.Empty<TreeRouteMatchingEntry>();
var route = CreateAttributeRoute(next.Object, matchingEntries, linkGenerationEntries); var route = CreateAttributeRoute(next.Object, matchingEntries, linkGenerationEntries);
@ -1796,12 +1797,12 @@ namespace Microsoft.AspNet.Mvc.Routing
name); 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 routeGroup = string.Format("{0}&&{1}", order, template);
var entry = new AttributeRouteMatchingEntry(); var entry = new TreeRouteMatchingEntry();
entry.Target = router; entry.Target = router;
entry.RouteTemplate = template; entry.RouteTemplate = TemplateParser.Parse(template);
var parsedRouteTemplate = TemplateParser.Parse(template); var parsedRouteTemplate = TemplateParser.Parse(template);
entry.TemplateMatcher = new TemplateMatcher( entry.TemplateMatcher = new TemplateMatcher(
parsedRouteTemplate, parsedRouteTemplate,
@ -1812,7 +1813,7 @@ namespace Microsoft.AspNet.Mvc.Routing
return entry; return entry;
} }
private static AttributeRouteLinkGenerationEntry CreateGenerationEntry( private static TreeRouteLinkGenerationEntry CreateGenerationEntry(
string template, string template,
object requiredValues, object requiredValues,
int order = 0, int order = 0,
@ -1820,7 +1821,7 @@ namespace Microsoft.AspNet.Mvc.Routing
{ {
var constraintResolver = CreateConstraintResolver(); var constraintResolver = CreateConstraintResolver();
var entry = new AttributeRouteLinkGenerationEntry(); var entry = new TreeRouteLinkGenerationEntry();
entry.TemplateText = template; entry.TemplateText = template;
entry.Template = TemplateParser.Parse(template); entry.Template = TemplateParser.Parse(template);
@ -1858,7 +1859,7 @@ namespace Microsoft.AspNet.Mvc.Routing
return entry; return entry;
} }
private AttributeRouteMatchingEntry CreateMatchingEntry(string template) private TreeRouteMatchingEntry CreateMatchingEntry(string template)
{ {
var mockConstraint = new Mock<IRouteConstraint>(); var mockConstraint = new Mock<IRouteConstraint>();
mockConstraint.Setup(c => c.Match( mockConstraint.Setup(c => c.Match(
@ -1874,9 +1875,9 @@ namespace Microsoft.AspNet.Mvc.Routing
It.IsAny<string>())) It.IsAny<string>()))
.Returns(mockConstraint.Object); .Returns(mockConstraint.Object);
var entry = new AttributeRouteMatchingEntry(); var entry = new TreeRouteMatchingEntry();
entry.Target = new StubRouter(); entry.Target = new StubRouter();
entry.RouteTemplate = template; entry.RouteTemplate = TemplateParser.Parse(template);
return entry; return entry;
} }
@ -1895,52 +1896,61 @@ namespace Microsoft.AspNet.Mvc.Routing
return new DefaultInlineConstraintResolver(optionsMock.Object); return new DefaultInlineConstraintResolver(optionsMock.Object);
} }
private static InnerAttributeRoute CreateAttributeRoute(AttributeRouteLinkGenerationEntry entry) private static TreeRouter CreateAttributeRoute(TreeRouteLinkGenerationEntry entry)
{ {
return CreateAttributeRoute(new StubRouter(), 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 }); return CreateAttributeRoute(next, new[] { entry });
} }
private static InnerAttributeRoute CreateAttributeRoute(params AttributeRouteLinkGenerationEntry[] entries) private static TreeRouter CreateAttributeRoute(params TreeRouteLinkGenerationEntry[] entries)
{ {
return CreateAttributeRoute(new StubRouter(), 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( return CreateAttributeRoute(
next, next,
Enumerable.Empty<AttributeRouteMatchingEntry>(), Enumerable.Empty<TreeRouteMatchingEntry>(),
entries); entries);
} }
private static InnerAttributeRoute CreateAttributeRoute(IRouter next, params AttributeRouteMatchingEntry[] entries) private static TreeRouter CreateAttributeRoute(IRouter next, params TreeRouteMatchingEntry[] entries)
{ {
return CreateAttributeRoute( return CreateAttributeRoute(
next, next,
entries, entries,
Enumerable.Empty<AttributeRouteLinkGenerationEntry>()); Enumerable.Empty<TreeRouteLinkGenerationEntry>());
} }
private static InnerAttributeRoute CreateAttributeRoute( private static TreeRouter CreateAttributeRoute(
IRouter next, IRouter next,
IEnumerable<AttributeRouteMatchingEntry> matchingEntries, IEnumerable<TreeRouteMatchingEntry> matchingEntries,
IEnumerable<AttributeRouteLinkGenerationEntry> generationEntries) IEnumerable<TreeRouteLinkGenerationEntry> generationEntries)
{ {
return new InnerAttributeRoute( var builder = new TreeRouteBuilder(
next, next,
matchingEntries,
generationEntries,
NullLogger.Instance, NullLogger.Instance,
NullLogger.Instance, NullLogger.Instance);
version: 1);
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<VirtualPathContext> virtualPathCallback, Action<VirtualPathContext> virtualPathCallback,
string firstTemplate, string firstTemplate,
string secondTemplate) string secondTemplate)
@ -1949,7 +1959,7 @@ namespace Microsoft.AspNet.Mvc.Routing
next.Setup(n => n.GetVirtualPath(It.IsAny<VirtualPathContext>())).Callback<VirtualPathContext>(virtualPathCallback) next.Setup(n => n.GetVirtualPath(It.IsAny<VirtualPathContext>())).Callback<VirtualPathContext>(virtualPathCallback)
.Returns((VirtualPathData)null); .Returns((VirtualPathData)null);
var matchingRoutes = Enumerable.Empty<AttributeRouteMatchingEntry>(); var matchingRoutes = Enumerable.Empty<TreeRouteMatchingEntry>();
var firstEntry = CreateGenerationEntry(firstTemplate, requiredValues: null); var firstEntry = CreateGenerationEntry(firstTemplate, requiredValues: null);
var secondEntry = CreateGenerationEntry(secondTemplate, requiredValues: null); var secondEntry = CreateGenerationEntry(secondTemplate, requiredValues: null);
@ -1959,19 +1969,23 @@ namespace Microsoft.AspNet.Mvc.Routing
new[] { secondEntry, firstEntry }); new[] { secondEntry, firstEntry });
} }
private static InnerAttributeRoute CreateRoutingAttributeRoute( private static TreeRouter CreateRoutingAttributeRoute(
ILoggerFactory loggerFactory = null, ILoggerFactory loggerFactory = null,
params AttributeRouteMatchingEntry[] entries) params TreeRouteMatchingEntry[] entries)
{ {
loggerFactory = loggerFactory ?? NullLoggerFactory.Instance; loggerFactory = loggerFactory ?? NullLoggerFactory.Instance;
return new InnerAttributeRoute( var builder = new TreeRouteBuilder(
new StubRouter(), new StubRouter(),
entries,
Enumerable.Empty<AttributeRouteLinkGenerationEntry>(),
loggerFactory.CreateLogger<AttributeRoute>(), loggerFactory.CreateLogger<AttributeRoute>(),
loggerFactory.CreateLogger(typeof(RouteConstraintMatcher).FullName), loggerFactory.CreateLogger(typeof(RouteConstraintMatcher).FullName));
version: 1);
foreach (var entry in entries)
{
builder.Add(entry);
}
return builder.Build(version: 1);
} }
private static IReadOnlyDictionary<string, IRouteConstraint> GetRouteConstriants( private static IReadOnlyDictionary<string, IRouteConstraint> GetRouteConstriants(