TreeRouter cleanup

This commit is contained in:
Ryan Nowak 2016-04-19 12:26:09 -07:00
parent ea2d30ff49
commit e8ce0e7523
17 changed files with 808 additions and 896 deletions

View File

@ -5,6 +5,7 @@ using System;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Internal; using Microsoft.AspNetCore.Routing.Internal;
using Microsoft.AspNetCore.Routing.Tree;
using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.ObjectPool; using Microsoft.Extensions.ObjectPool;
@ -36,6 +37,10 @@ namespace Microsoft.Extensions.DependencyInjection
return provider.Create<UriBuildingContext>(new UriBuilderContextPooledObjectPolicy(encoder)); return provider.Create<UriBuildingContext>(new UriBuilderContextPooledObjectPolicy(encoder));
}); });
// The TreeRouteBuilder is a builder for creating routes, it should stay transient because it's
// stateful.
services.TryAddTransient<TreeRouteBuilder>();
services.TryAddSingleton(typeof(RoutingMarkerService)); services.TryAddSingleton(typeof(RoutingMarkerService));
return services; return services;

View File

@ -11,23 +11,23 @@ namespace Microsoft.AspNetCore.Routing.Internal
// 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<TreeRouteLinkGenerationEntry> _root; private readonly DecisionTreeNode<OutboundMatch> _root;
public LinkGenerationDecisionTree(IReadOnlyList<TreeRouteLinkGenerationEntry> entries) public LinkGenerationDecisionTree(IReadOnlyList<OutboundMatch> entries)
{ {
_root = DecisionTreeBuilder<TreeRouteLinkGenerationEntry>.GenerateTree( _root = DecisionTreeBuilder<OutboundMatch>.GenerateTree(
entries, entries,
new AttributeRouteLinkGenerationEntryClassifier()); new OutboundMatchClassifier());
} }
public IList<LinkGenerationMatch> GetMatches(VirtualPathContext context) public IList<OutboundMatchResult> GetMatches(VirtualPathContext context)
{ {
// Perf: Avoid allocation for List if there aren't any Matches or Criteria // Perf: Avoid allocation for List if there aren't any Matches or Criteria
if (_root.Matches.Count > 0 || _root.Criteria.Count > 0) if (_root.Matches.Count > 0 || _root.Criteria.Count > 0)
{ {
var results = new List<LinkGenerationMatch>(); var results = new List<OutboundMatchResult>();
Walk(results, context, _root, isFallbackPath: false); Walk(results, context, _root, isFallbackPath: false);
results.Sort(LinkGenerationMatchComparer.Instance); results.Sort(OutboundMatchResultComparer.Instance);
return results; return results;
} }
@ -60,16 +60,16 @@ namespace Microsoft.AspNetCore.Routing.Internal
// //
// The decision tree uses a tree data structure to execute these rules across all candidates at once. // The decision tree uses a tree data structure to execute these rules across all candidates at once.
private void Walk( private void Walk(
List<LinkGenerationMatch> results, List<OutboundMatchResult> results,
VirtualPathContext context, VirtualPathContext context,
DecisionTreeNode<TreeRouteLinkGenerationEntry> node, DecisionTreeNode<OutboundMatch> 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
// to the results. // to the results.
for (var i = 0; i < node.Matches.Count; i++) for (var i = 0; i < node.Matches.Count; i++)
{ {
results.Add(new LinkGenerationMatch(node.Matches[i], isFallbackPath)); results.Add(new OutboundMatchResult(node.Matches[i], isFallbackPath));
} }
for (var i = 0; i < node.Criteria.Count; i++) for (var i = 0; i < node.Criteria.Count; i++)
@ -80,7 +80,7 @@ namespace Microsoft.AspNetCore.Routing.Internal
object value; object value;
if (context.Values.TryGetValue(key, out value)) if (context.Values.TryGetValue(key, out value))
{ {
DecisionTreeNode<TreeRouteLinkGenerationEntry> branch; DecisionTreeNode<OutboundMatch> 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);
@ -91,7 +91,7 @@ namespace Microsoft.AspNetCore.Routing.Internal
// If a value wasn't explicitly supplied, match BOTH the ambient value and the empty value // If 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<TreeRouteLinkGenerationEntry> branch; DecisionTreeNode<OutboundMatch> 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))
{ {
@ -109,19 +109,19 @@ namespace Microsoft.AspNetCore.Routing.Internal
} }
} }
private class AttributeRouteLinkGenerationEntryClassifier : IClassifier<TreeRouteLinkGenerationEntry> private class OutboundMatchClassifier : IClassifier<OutboundMatch>
{ {
public AttributeRouteLinkGenerationEntryClassifier() public OutboundMatchClassifier()
{ {
ValueComparer = new RouteValueEqualityComparer(); ValueComparer = new RouteValueEqualityComparer();
} }
public IEqualityComparer<object> ValueComparer { get; private set; } public IEqualityComparer<object> ValueComparer { get; private set; }
public IDictionary<string, DecisionCriterionValue> GetCriteria(TreeRouteLinkGenerationEntry item) public IDictionary<string, DecisionCriterionValue> GetCriteria(OutboundMatch 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.Entry.RequiredLinkValues)
{ {
results.Add(kvp.Key, new DecisionCriterionValue(kvp.Value ?? string.Empty)); results.Add(kvp.Key, new DecisionCriterionValue(kvp.Value ?? string.Empty));
} }
@ -130,22 +130,22 @@ namespace Microsoft.AspNetCore.Routing.Internal
} }
} }
private class LinkGenerationMatchComparer : IComparer<LinkGenerationMatch> private class OutboundMatchResultComparer : IComparer<OutboundMatchResult>
{ {
public static readonly LinkGenerationMatchComparer Instance = new LinkGenerationMatchComparer(); public static readonly OutboundMatchResultComparer Instance = new OutboundMatchResultComparer();
public int Compare(LinkGenerationMatch x, LinkGenerationMatch y) public int Compare(OutboundMatchResult x, OutboundMatchResult y)
{ {
// For this comparison lower is better. // For this comparison lower is better.
if (x.Entry.Order != y.Entry.Order) if (x.Match.Entry.Order != y.Match.Entry.Order)
{ {
return x.Entry.Order.CompareTo(y.Entry.Order); return x.Match.Entry.Order.CompareTo(y.Match.Entry.Order);
} }
if (x.Entry.GenerationPrecedence != y.Entry.GenerationPrecedence) if (x.Match.Entry.Precedence != y.Match.Entry.Precedence)
{ {
// Reversed because higher is better // Reversed because higher is better
return y.Entry.GenerationPrecedence.CompareTo(x.Entry.GenerationPrecedence); return y.Match.Entry.Precedence.CompareTo(x.Match.Entry.Precedence);
} }
if (x.IsFallbackMatch != y.IsFallbackMatch) if (x.IsFallbackMatch != y.IsFallbackMatch)
@ -154,7 +154,9 @@ namespace Microsoft.AspNetCore.Routing.Internal
return x.IsFallbackMatch.CompareTo(y.IsFallbackMatch); return x.IsFallbackMatch.CompareTo(y.IsFallbackMatch);
} }
return StringComparer.Ordinal.Compare(x.Entry.Template.TemplateText, y.Entry.Template.TemplateText); return StringComparer.Ordinal.Compare(
x.Match.Entry.RouteTemplate.TemplateText,
y.Match.Entry.RouteTemplate.TemplateText);
} }
} }
} }

View File

@ -1,23 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Routing.Tree;
namespace Microsoft.AspNetCore.Routing.Internal
{
public struct LinkGenerationMatch
{
private readonly bool _isFallbackMatch;
private readonly TreeRouteLinkGenerationEntry _entry;
public LinkGenerationMatch(TreeRouteLinkGenerationEntry entry, bool isFallbackMatch)
{
_entry = entry;
_isFallbackMatch = isFallbackMatch;
}
public TreeRouteLinkGenerationEntry Entry { get { return _entry; } }
public bool IsFallbackMatch { get { return _isFallbackMatch; } }
}
}

View File

@ -0,0 +1,20 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Routing.Tree;
namespace Microsoft.AspNetCore.Routing.Internal
{
public struct OutboundMatchResult
{
public OutboundMatchResult(OutboundMatch match, bool isFallbackMatch)
{
Match = match;
IsFallbackMatch = isFallbackMatch;
}
public OutboundMatch Match { get; }
public bool IsFallbackMatch { get; }
}
}

View File

@ -8,7 +8,7 @@ using System.Linq;
namespace Microsoft.AspNetCore.Routing.Template namespace Microsoft.AspNetCore.Routing.Template
{ {
/// <summary> /// <summary>
/// Computes precedence for an attribute route template. /// Computes precedence for a route template.
/// </summary> /// </summary>
public static class RoutePrecedence public static class RoutePrecedence
{ {
@ -17,7 +17,7 @@ namespace Microsoft.AspNetCore.Routing.Template
// /api/template/{id} == 1.13 // /api/template/{id} == 1.13
// /api/{id:int} == 1.2 // /api/{id:int} == 1.2
// /api/template/{id:int} == 1.12 // /api/template/{id:int} == 1.12
public static decimal ComputeMatched(RouteTemplate template) public static decimal ComputeInbound(RouteTemplate template)
{ {
// Each precedence digit corresponds to one decimal place. For example, 3 segments with precedences 2, 1, // Each precedence digit corresponds to one decimal place. For example, 3 segments with precedences 2, 1,
// and 4 results in a combined precedence of 2.14 (decimal). // and 4 results in a combined precedence of 2.14 (decimal).
@ -27,7 +27,7 @@ namespace Microsoft.AspNetCore.Routing.Template
{ {
var segment = template.Segments[i]; var segment = template.Segments[i];
var digit = ComputeMatchDigit(segment); var digit = ComputeInboundPrecedenceDigit(segment);
Debug.Assert(digit >= 0 && digit < 10); Debug.Assert(digit >= 0 && digit < 10);
precedence += decimal.Divide(digit, (decimal)Math.Pow(10, i)); precedence += decimal.Divide(digit, (decimal)Math.Pow(10, i));
@ -41,7 +41,7 @@ namespace Microsoft.AspNetCore.Routing.Template
// /api/template/{id} == 5.53 // /api/template/{id} == 5.53
// /api/{id:int} == 5.4 // /api/{id:int} == 5.4
// /api/template/{id:int} == 5.54 // /api/template/{id:int} == 5.54
public static decimal ComputeGenerated(RouteTemplate template) public static decimal ComputeOutbound(RouteTemplate template)
{ {
// Each precedence digit corresponds to one decimal place. For example, 3 segments with precedences 2, 1, // Each precedence digit corresponds to one decimal place. For example, 3 segments with precedences 2, 1,
// and 4 results in a combined precedence of 2.14 (decimal). // and 4 results in a combined precedence of 2.14 (decimal).
@ -51,7 +51,7 @@ namespace Microsoft.AspNetCore.Routing.Template
{ {
var segment = template.Segments[i]; var segment = template.Segments[i];
var digit = ComputeGenerationDigit(segment); var digit = ComputeOutboundPrecedenceDigit(segment);
Debug.Assert(digit >= 0 && digit < 10); Debug.Assert(digit >= 0 && digit < 10);
precedence += decimal.Divide(digit, (decimal)Math.Pow(10, i)); precedence += decimal.Divide(digit, (decimal)Math.Pow(10, i));
@ -66,7 +66,7 @@ namespace Microsoft.AspNetCore.Routing.Template
// 3 - Unconstrained parameter segements // 3 - Unconstrained parameter segements
// 2 - Constrained wildcard parameter segments // 2 - Constrained wildcard parameter segments
// 1 - Unconstrained wildcard parameter segments // 1 - Unconstrained wildcard parameter segments
private static int ComputeGenerationDigit(TemplateSegment segment) private static int ComputeOutboundPrecedenceDigit(TemplateSegment segment)
{ {
if(segment.Parts.Count > 1) if(segment.Parts.Count > 1)
{ {
@ -98,7 +98,7 @@ namespace Microsoft.AspNetCore.Routing.Template
// 3 - Unconstrained parameter segments // 3 - Unconstrained parameter segments
// 4 - Constrained wildcard parameter segments // 4 - Constrained wildcard parameter segments
// 5 - Unconstrained wildcard parameter segments // 5 - Unconstrained wildcard parameter segments
private static int ComputeMatchDigit(TemplateSegment segment) private static int ComputeInboundPrecedenceDigit(TemplateSegment segment)
{ {
if (segment.Parts.Count > 1) if (segment.Parts.Count > 1)
{ {

View File

@ -0,0 +1,14 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Routing.Template;
namespace Microsoft.AspNetCore.Routing.Tree
{
public class InboundMatch
{
public InboundRouteEntry Entry { get; set; }
public TemplateMatcher TemplateMatcher { get; set; }
}
}

View File

@ -0,0 +1,56 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using Microsoft.AspNetCore.Routing.Template;
namespace Microsoft.AspNetCore.Routing.Tree
{
/// <summary>
/// Used to build an <see cref="TreeRouter"/>. Represents a URL template tha will be used to match incoming
/// request URLs.
/// </summary>
public class InboundRouteEntry
{
/// <summary>
/// Gets or sets the route constraints.
/// </summary>
public IDictionary<string, IRouteConstraint> Constraints { get; set; }
/// <summary>
/// Gets or sets the route defaults.
/// </summary>
public RouteValueDictionary Defaults { get; set; }
/// <summary>
/// Gets or sets the <see cref="IRouter"/> to invoke when this entry matches.
/// </summary>
public IRouter Handler { get; set; }
/// <summary>
/// Gets or sets the order of the entry.
/// </summary>
/// <remarks>
/// Entries are ordered first by <see cref="Order"/> (ascending) then by <see cref="Precedence"/> (descending).
/// </remarks>
public int Order { get; set; }
/// <summary>
/// Gets or sets the precedence of the entry.
/// </summary>
/// <remarks>
/// Entries are ordered first by <see cref="Order"/> (ascending) then by <see cref="Precedence"/> (descending).
/// </remarks>
public decimal Precedence { get; set; }
/// <summary>
/// Gets or sets the name of the route.
/// </summary>
public string RouteName { get; set; }
/// <summary>
/// Gets or sets the <see cref="RouteTemplate"/>.
/// </summary>
public RouteTemplate RouteTemplate { get; set; }
}
}

View File

@ -0,0 +1,14 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Routing.Template;
namespace Microsoft.AspNetCore.Routing.Tree
{
public class OutboundMatch
{
public OutboundRouteEntry Entry { get; set; }
public TemplateBinder TemplateBinder { get; set; }
}
}

View File

@ -7,16 +7,11 @@ using Microsoft.AspNetCore.Routing.Template;
namespace Microsoft.AspNetCore.Routing.Tree namespace Microsoft.AspNetCore.Routing.Tree
{ {
/// <summary> /// <summary>
/// Used to build a <see cref="TreeRouter"/>. Represents an individual URL-generating route that will be /// Used to build a <see cref="TreeRouter"/>. Represents a URL template that will be used to generate
/// aggregated into the <see cref="TreeRouter"/>. /// outgoing URLs.
/// </summary> /// </summary>
public class TreeRouteLinkGenerationEntry public class OutboundRouteEntry
{ {
/// <summary>
/// Gets or sets the <see cref="TemplateBinder"/>.
/// </summary>
public TemplateBinder Binder { get; set; }
/// <summary> /// <summary>
/// Gets or sets the route constraints. /// Gets or sets the route constraints.
/// </summary> /// </summary>
@ -25,37 +20,43 @@ namespace Microsoft.AspNetCore.Routing.Tree
/// <summary> /// <summary>
/// Gets or sets the route defaults. /// Gets or sets the route defaults.
/// </summary> /// </summary>
public IDictionary<string, object> Defaults { get; set; } public RouteValueDictionary Defaults { get; set; }
/// <summary> /// <summary>
/// Gets or sets the order of the template. /// The <see cref="IRouter"/> to invoke when this entry matches.
/// </summary> /// </summary>
public IRouter Handler { get; set; }
/// <summary>
/// Gets or sets the order of the entry.
/// </summary>
/// <remarks>
/// Entries are ordered first by <see cref="Order"/> (ascending) then by <see cref="Precedence"/> (descending).
/// </remarks>
public int Order { get; set; } public int Order { get; set; }
/// <summary> /// <summary>
/// Gets or sets the precedence of the template for link generation. A greater value of /// Gets or sets the precedence of the template for link generation. A greater value of
/// <see cref="GenerationPrecedence"/> means that an entry is considered first. /// <see cref="Precedence"/> means that an entry is considered first.
/// </summary> /// </summary>
public decimal GenerationPrecedence { get; set; } /// <remarks>
/// Entries are ordered first by <see cref="Order"/> (ascending) then by <see cref="Precedence"/> (descending).
/// </remarks>
public decimal Precedence { get; set; }
/// <summary> /// <summary>
/// Gets or sets the name of the route. /// Gets or sets the name of the route.
/// </summary> /// </summary>
public string Name { get; set; } public string RouteName { get; set; }
/// <summary>
/// Gets or sets the route group.
/// </summary>
public string RouteGroup { get; set; }
/// <summary> /// <summary>
/// Gets or sets the set of values that must be present for link genration. /// Gets or sets the set of values that must be present for link genration.
/// </summary> /// </summary>
public IDictionary<string, object> RequiredLinkValues { get; set; } public RouteValueDictionary RequiredLinkValues { get; set; }
/// <summary> /// <summary>
/// Gets or sets the <see cref="Template"/>. /// Gets or sets the <see cref="RouteTemplate"/>.
/// </summary> /// </summary>
public RouteTemplate Template { get; set; } public RouteTemplate RouteTemplate { get; set; }
} }
} }

View File

@ -1,47 +1,195 @@
// 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.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Routing.Internal;
using Microsoft.AspNetCore.Routing.Template;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.ObjectPool;
namespace Microsoft.AspNetCore.Routing.Tree namespace Microsoft.AspNetCore.Routing.Tree
{ {
public class TreeRouteBuilder public class TreeRouteBuilder
{ {
private readonly IRouter _target;
private readonly List<TreeRouteLinkGenerationEntry> _generatingEntries;
private readonly List<TreeRouteMatchingEntry> _matchingEntries;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly ILogger _constraintLogger; private readonly ILogger _constraintLogger;
private readonly UrlEncoder _urlEncoder;
private readonly ObjectPool<UriBuildingContext> _objectPool;
private readonly IInlineConstraintResolver _constraintResolver;
public TreeRouteBuilder(IRouter target, ILoggerFactory loggerFactory) public TreeRouteBuilder(
ILoggerFactory loggerFactory,
UrlEncoder urlEncoder,
ObjectPool<UriBuildingContext> objectPool,
IInlineConstraintResolver constraintResolver)
{ {
_target = target; if (loggerFactory == null)
_generatingEntries = new List<TreeRouteLinkGenerationEntry>(); {
_matchingEntries = new List<TreeRouteMatchingEntry>(); throw new ArgumentNullException(nameof(loggerFactory));
}
if (urlEncoder == null)
{
throw new ArgumentNullException(nameof(urlEncoder));
}
if (objectPool == null)
{
throw new ArgumentNullException(nameof(objectPool));
}
if (constraintResolver == null)
{
throw new ArgumentNullException(nameof(constraintResolver));
}
_urlEncoder = urlEncoder;
_objectPool = objectPool;
_constraintResolver = constraintResolver;
_logger = loggerFactory.CreateLogger<TreeRouter>(); _logger = loggerFactory.CreateLogger<TreeRouter>();
_constraintLogger = loggerFactory.CreateLogger(typeof(RouteConstraintMatcher).FullName); _constraintLogger = loggerFactory.CreateLogger(typeof(RouteConstraintMatcher).FullName);
} }
public void Add(TreeRouteLinkGenerationEntry entry) public InboundRouteEntry MapInbound(
IRouter handler,
RouteTemplate routeTemplate,
string routeName,
int order)
{ {
_generatingEntries.Add(entry); if (handler == null)
{
throw new ArgumentNullException(nameof(handler));
}
if (routeTemplate == null)
{
throw new ArgumentNullException(nameof(routeTemplate));
}
var entry = new InboundRouteEntry()
{
Handler = handler,
Order = order,
Precedence = RoutePrecedence.ComputeInbound(routeTemplate),
RouteName = routeName,
RouteTemplate = routeTemplate,
};
var constraintBuilder = new RouteConstraintBuilder(_constraintResolver, routeTemplate.TemplateText);
foreach (var parameter in routeTemplate.Parameters)
{
if (parameter.InlineConstraints != null)
{
if (parameter.IsOptional)
{
constraintBuilder.SetOptional(parameter.Name);
}
foreach (var constraint in parameter.InlineConstraints)
{
constraintBuilder.AddResolvedConstraint(parameter.Name, constraint.Constraint);
}
}
}
entry.Constraints = constraintBuilder.Build();
entry.Defaults = new RouteValueDictionary();
foreach (var parameter in entry.RouteTemplate.Parameters)
{
if (parameter.DefaultValue != null)
{
entry.Defaults.Add(parameter.Name, parameter.DefaultValue);
}
}
InboundEntries.Add(entry);
return entry;
} }
public void Add(TreeRouteMatchingEntry entry) public OutboundRouteEntry MapOutbound(
IRouter handler,
RouteTemplate routeTemplate,
RouteValueDictionary requiredLinkValues,
string routeName,
int order)
{ {
_matchingEntries.Add(entry); if (handler == null)
{
throw new ArgumentNullException(nameof(handler));
}
if (routeTemplate == null)
{
throw new ArgumentNullException(nameof(routeTemplate));
}
if (requiredLinkValues == null)
{
throw new ArgumentNullException(nameof(requiredLinkValues));
}
var entry = new OutboundRouteEntry()
{
Handler = handler,
Order = order,
Precedence = RoutePrecedence.ComputeOutbound(routeTemplate),
RequiredLinkValues = requiredLinkValues,
RouteName = routeName,
RouteTemplate = routeTemplate,
};
var constraintBuilder = new RouteConstraintBuilder(_constraintResolver, routeTemplate.TemplateText);
foreach (var parameter in routeTemplate.Parameters)
{
if (parameter.InlineConstraints != null)
{
if (parameter.IsOptional)
{
constraintBuilder.SetOptional(parameter.Name);
}
foreach (var constraint in parameter.InlineConstraints)
{
constraintBuilder.AddResolvedConstraint(parameter.Name, constraint.Constraint);
}
}
}
entry.Constraints = constraintBuilder.Build();
entry.Defaults = new RouteValueDictionary();
foreach (var parameter in entry.RouteTemplate.Parameters)
{
if (parameter.DefaultValue != null)
{
entry.Defaults.Add(parameter.Name, parameter.DefaultValue);
}
}
OutboundEntries.Add(entry);
return entry;
}
public IList<InboundRouteEntry> InboundEntries { get; } = new List<InboundRouteEntry>();
public IList<OutboundRouteEntry> OutboundEntries { get; } = new List<OutboundRouteEntry>();
public TreeRouter Build()
{
return Build(version: 0);
} }
public TreeRouter Build(int version) public TreeRouter Build(int version)
{ {
var trees = new Dictionary<int, UrlMatchingTree>(); var trees = new Dictionary<int, UrlMatchingTree>();
foreach (var entry in _matchingEntries) foreach (var entry in InboundEntries)
{ {
UrlMatchingTree tree; UrlMatchingTree tree;
if (!trees.TryGetValue(entry.Order, out tree)) if (!trees.TryGetValue(entry.Order, out tree))
@ -54,9 +202,10 @@ namespace Microsoft.AspNetCore.Routing.Tree
} }
return new TreeRouter( return new TreeRouter(
_target,
trees.Values.OrderBy(tree => tree.Order).ToArray(), trees.Values.OrderBy(tree => tree.Order).ToArray(),
_generatingEntries, OutboundEntries,
_urlEncoder,
_objectPool,
_logger, _logger,
_constraintLogger, _constraintLogger,
version); version);
@ -64,14 +213,16 @@ namespace Microsoft.AspNetCore.Routing.Tree
public void Clear() public void Clear()
{ {
_generatingEntries.Clear(); InboundEntries.Clear();
_matchingEntries.Clear(); OutboundEntries.Clear();
} }
private void AddEntryToTree(UrlMatchingTree tree, TreeRouteMatchingEntry entry) private void AddEntryToTree(UrlMatchingTree tree, InboundRouteEntry entry)
{ {
var current = tree.Root; var current = tree.Root;
var matcher = new TemplateMatcher(entry.RouteTemplate, entry.Defaults);
for (var i = 0; i < entry.RouteTemplate.Segments.Count; i++) for (var i = 0; i < entry.RouteTemplate.Segments.Count; i++)
{ {
var segment = entry.RouteTemplate.Segments[i]; var segment = entry.RouteTemplate.Segments[i];
@ -104,7 +255,7 @@ namespace Microsoft.AspNetCore.Routing.Tree
if (part.IsParameter && (part.IsOptional || part.IsCatchAll)) if (part.IsParameter && (part.IsOptional || part.IsCatchAll))
{ {
current.Matches.Add(entry); current.Matches.Add(new InboundMatch() { Entry = entry, TemplateMatcher = matcher });
} }
if (part.IsParameter && part.InlineConstraints.Any() && !part.IsCatchAll) if (part.IsParameter && part.InlineConstraints.Any() && !part.IsCatchAll)
@ -154,11 +305,11 @@ namespace Microsoft.AspNetCore.Routing.Tree
Debug.Fail("We shouldn't get here."); Debug.Fail("We shouldn't get here.");
} }
current.Matches.Add(entry); current.Matches.Add(new InboundMatch() { Entry = entry, TemplateMatcher = matcher });
current.Matches.Sort((x, y) => current.Matches.Sort((x, y) =>
{ {
var result = x.Precedence.CompareTo(y.Precedence); var result = x.Entry.Precedence.CompareTo(y.Entry.Precedence);
return result == 0 ? x.RouteTemplate.TemplateText.CompareTo(y.RouteTemplate.TemplateText) : result; return result == 0 ? x.Entry.RouteTemplate.TemplateText.CompareTo(y.Entry.RouteTemplate.TemplateText) : result;
}); });
} }
} }

View File

@ -1,50 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using Microsoft.AspNetCore.Routing.Template;
namespace Microsoft.AspNetCore.Routing.Tree
{
/// <summary>
/// Used to build an <see cref="TreeRouter"/>. Represents an individual URL-matching route that will be
/// aggregated into the <see cref="TreeRouter"/>.
/// </summary>
public class TreeRouteMatchingEntry
{
/// <summary>
/// The order of the template.
/// </summary>
public int Order { get; set; }
/// <summary>
/// The precedence of the template.
/// </summary>
public decimal Precedence { get; set; }
/// <summary>
/// The <see cref="IRouter"/> to invoke when this entry matches.
/// </summary>
public IRouter Target { get; set; }
/// <summary>
/// The name of the route.
/// </summary>
public string RouteName { get; set; }
/// <summary>
/// The <see cref="RouteTemplate"/>.
/// </summary>
public RouteTemplate RouteTemplate { get; set; }
/// <summary>
/// The <see cref="TemplateMatcher"/>.
/// </summary>
public TemplateMatcher TemplateMatcher { get; set; }
/// <summary>
/// The route constraints.
/// </summary>
public IDictionary<string, IRouteConstraint> Constraints { get; set; }
}
}

View File

@ -4,12 +4,13 @@
using System; using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Text.Encodings.Web;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Routing.Internal; using Microsoft.AspNetCore.Routing.Internal;
using Microsoft.AspNetCore.Routing.Logging; using Microsoft.AspNetCore.Routing.Logging;
using Microsoft.Extensions.Internal; using Microsoft.AspNetCore.Routing.Template;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.ObjectPool;
namespace Microsoft.AspNetCore.Routing.Tree namespace Microsoft.AspNetCore.Routing.Tree
{ {
@ -22,10 +23,9 @@ namespace Microsoft.AspNetCore.Routing.Tree
// group of action descriptors. // group of action descriptors.
public static readonly string RouteGroupKey = "!__route_group"; public static readonly string RouteGroupKey = "!__route_group";
private readonly IRouter _next;
private readonly LinkGenerationDecisionTree _linkGenerationTree; private readonly LinkGenerationDecisionTree _linkGenerationTree;
private readonly UrlMatchingTree[] _trees; private readonly UrlMatchingTree[] _trees;
private readonly IDictionary<string, TreeRouteLinkGenerationEntry> _namedEntries; private readonly IDictionary<string, OutboundMatch> _namedEntries;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly ILogger _constraintLogger; private readonly ILogger _constraintLogger;
@ -33,26 +33,23 @@ namespace Microsoft.AspNetCore.Routing.Tree
/// <summary> /// <summary>
/// Creates a new <see cref="TreeRouter"/>. /// Creates a new <see cref="TreeRouter"/>.
/// </summary> /// </summary>
/// <param name="next">The next router. Invoked when a route entry matches.</param>
/// <param name="trees">The list of <see cref="UrlMatchingTree"/> that contains the route entries.</param> /// <param name="trees">The list of <see cref="UrlMatchingTree"/> that contains the route entries.</param>
/// <param name="linkGenerationEntries">The set of <see cref="TreeRouteLinkGenerationEntry"/>.</param> /// <param name="linkGenerationEntries">The set of <see cref="OutboundRouteEntry"/>.</param>
/// <param name="urlEncoder">The <see cref="UrlEncoder"/>.</param>
/// <param name="objectPool">The <see cref="ObjectPool{T}"/>.</param>
/// <param name="routeLogger">The <see cref="ILogger"/> instance.</param> /// <param name="routeLogger">The <see cref="ILogger"/> instance.</param>
/// <param name="constraintLogger">The <see cref="ILogger"/> instance used /// <param name="constraintLogger">The <see cref="ILogger"/> instance used
/// in <see cref="RouteConstraintMatcher"/>.</param> /// in <see cref="RouteConstraintMatcher"/>.</param>
/// <param name="version">The version of this route.</param> /// <param name="version">The version of this route.</param>
public TreeRouter( public TreeRouter(
IRouter next,
UrlMatchingTree[] trees, UrlMatchingTree[] trees,
IEnumerable<TreeRouteLinkGenerationEntry> linkGenerationEntries, IEnumerable<OutboundRouteEntry> linkGenerationEntries,
UrlEncoder urlEncoder,
ObjectPool<UriBuildingContext> objectPool,
ILogger routeLogger, ILogger routeLogger,
ILogger constraintLogger, ILogger constraintLogger,
int version) int version)
{ {
if (next == null)
{
throw new ArgumentNullException(nameof(next));
}
if (trees == null) if (trees == null)
{ {
throw new ArgumentNullException(nameof(trees)); throw new ArgumentNullException(nameof(trees));
@ -63,6 +60,16 @@ namespace Microsoft.AspNetCore.Routing.Tree
throw new ArgumentNullException(nameof(linkGenerationEntries)); throw new ArgumentNullException(nameof(linkGenerationEntries));
} }
if (urlEncoder == null)
{
throw new ArgumentNullException(nameof(urlEncoder));
}
if (objectPool == null)
{
throw new ArgumentNullException(nameof(objectPool));
}
if (routeLogger == null) if (routeLogger == null)
{ {
throw new ArgumentNullException(nameof(routeLogger)); throw new ArgumentNullException(nameof(routeLogger));
@ -73,43 +80,49 @@ namespace Microsoft.AspNetCore.Routing.Tree
throw new ArgumentNullException(nameof(constraintLogger)); throw new ArgumentNullException(nameof(constraintLogger));
} }
_next = next;
_trees = trees; _trees = trees;
_logger = routeLogger; _logger = routeLogger;
_constraintLogger = constraintLogger; _constraintLogger = constraintLogger;
var namedEntries = new Dictionary<string, TreeRouteLinkGenerationEntry>( _namedEntries = new Dictionary<string, OutboundMatch>(StringComparer.OrdinalIgnoreCase);
StringComparer.OrdinalIgnoreCase);
var outboundMatches = new List<OutboundMatch>();
foreach (var entry in linkGenerationEntries) foreach (var entry in linkGenerationEntries)
{ {
var binder = new TemplateBinder(urlEncoder, objectPool, entry.RouteTemplate, entry.Defaults);
var outboundMatch = new OutboundMatch() { Entry = entry, TemplateBinder = binder };
outboundMatches.Add(outboundMatch);
// Skip unnamed entries // Skip unnamed entries
if (entry.Name == null) if (entry.RouteName == null)
{ {
continue; continue;
} }
// We only need to keep one AttributeRouteLinkGenerationEntry per route template // We only need to keep one OutboundMatch per route template
// so in case two entries have the same name and the same template we only keep // so in case two entries have the same name and the same template we only keep
// the first entry. // the first entry.
TreeRouteLinkGenerationEntry namedEntry = null; OutboundMatch namedMatch;
if (namedEntries.TryGetValue(entry.Name, out namedEntry) && if (_namedEntries.TryGetValue(entry.RouteName, out namedMatch) &&
!namedEntry.Template.TemplateText.Equals(entry.Template.TemplateText, StringComparison.OrdinalIgnoreCase)) !string.Equals(
namedMatch.Entry.RouteTemplate.TemplateText,
entry.RouteTemplate.TemplateText,
StringComparison.OrdinalIgnoreCase))
{ {
throw new ArgumentException( throw new ArgumentException(
Resources.FormatAttributeRoute_DifferentLinkGenerationEntries_SameName(entry.Name), Resources.FormatAttributeRoute_DifferentLinkGenerationEntries_SameName(entry.RouteName),
nameof(linkGenerationEntries)); nameof(linkGenerationEntries));
} }
else if (namedEntry == null) else if (namedMatch == null)
{ {
namedEntries.Add(entry.Name, entry); _namedEntries.Add(entry.RouteName, outboundMatch);
} }
} }
_namedEntries = namedEntries;
// The decision tree will take care of ordering for these entries. // The decision tree will take care of ordering for these entries.
_linkGenerationTree = new LinkGenerationDecisionTree(linkGenerationEntries.ToArray()); _linkGenerationTree = new LinkGenerationDecisionTree(outboundMatches.ToArray());
Version = version; Version = version;
} }
@ -145,7 +158,7 @@ namespace Microsoft.AspNetCore.Routing.Tree
for (var i = 0; i < matches.Count; i++) for (var i = 0; i < matches.Count; i++)
{ {
var path = GenerateVirtualPath(context, matches[i].Entry); var path = GenerateVirtualPath(context, matches[i].Match.Entry, matches[i].Match.TemplateBinder);
if (path != null) if (path != null)
{ {
return path; return path;
@ -175,7 +188,9 @@ namespace Microsoft.AspNetCore.Routing.Tree
var node = treeEnumerator.Current; var node = treeEnumerator.Current;
foreach (var item in node.Matches) foreach (var item in node.Matches)
{ {
if (!item.TemplateMatcher.TryMatch(context.HttpContext.Request.Path, context.RouteData.Values)) var entry = item.Entry;
var matcher = item.TemplateMatcher;
if (!matcher.TryMatch(context.HttpContext.Request.Path, context.RouteData.Values))
{ {
continue; continue;
} }
@ -183,7 +198,7 @@ namespace Microsoft.AspNetCore.Routing.Tree
try try
{ {
if (!RouteConstraintMatcher.Match( if (!RouteConstraintMatcher.Match(
item.Constraints, entry.Constraints,
context.RouteData.Values, context.RouteData.Values,
context.HttpContext, context.HttpContext,
this, this,
@ -193,10 +208,10 @@ namespace Microsoft.AspNetCore.Routing.Tree
continue; continue;
} }
_logger.MatchedRoute(item.RouteName, item.RouteTemplate.TemplateText); _logger.MatchedRoute(entry.RouteName, entry.RouteTemplate.TemplateText);
context.RouteData.Routers.Add(entry.Handler);
context.RouteData.Routers.Add(item.Target); await entry.Handler.RouteAsync(context);
await item.Target.RouteAsync(context);
if (context.Handler != null) if (context.Handler != null)
{ {
return; return;
@ -318,10 +333,10 @@ namespace Microsoft.AspNetCore.Routing.Tree
private VirtualPathData GetVirtualPathForNamedRoute(VirtualPathContext context) private VirtualPathData GetVirtualPathForNamedRoute(VirtualPathContext context)
{ {
TreeRouteLinkGenerationEntry entry; OutboundMatch match;
if (_namedEntries.TryGetValue(context.RouteName, out entry)) if (_namedEntries.TryGetValue(context.RouteName, out match))
{ {
var path = GenerateVirtualPath(context, entry); var path = GenerateVirtualPath(context, match.Entry, match.TemplateBinder);
if (path != null) if (path != null)
{ {
return path; return path;
@ -330,7 +345,10 @@ namespace Microsoft.AspNetCore.Routing.Tree
return null; return null;
} }
private VirtualPathData GenerateVirtualPath(VirtualPathContext context, TreeRouteLinkGenerationEntry entry) private VirtualPathData GenerateVirtualPath(
VirtualPathContext context,
OutboundRouteEntry entry,
TemplateBinder binder)
{ {
// In attribute the context includes the values that are used to select this entry - typically // 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
@ -349,7 +367,7 @@ namespace Microsoft.AspNetCore.Routing.Tree
{ {
if (entry.RequiredLinkValues.ContainsKey(kvp.Key)) if (entry.RequiredLinkValues.ContainsKey(kvp.Key))
{ {
var parameter = entry.Template.GetParameter(kvp.Key); var parameter = entry.RouteTemplate.GetParameter(kvp.Key);
if (parameter == null) if (parameter == null)
{ {
@ -360,7 +378,7 @@ namespace Microsoft.AspNetCore.Routing.Tree
inputValues.Add(kvp.Key, kvp.Value); inputValues.Add(kvp.Key, kvp.Value);
} }
var bindingResult = entry.Binder.GetValues(context.AmbientValues, inputValues); var bindingResult = binder.GetValues(context.AmbientValues, inputValues);
if (bindingResult == null) if (bindingResult == null)
{ {
// A required parameter in the template didn't get a value. // A required parameter in the template didn't get a value.
@ -381,7 +399,7 @@ namespace Microsoft.AspNetCore.Routing.Tree
return null; return null;
} }
var pathData = _next.GetVirtualPath(context); var pathData = entry.Handler.GetVirtualPath(context);
if (pathData != null) if (pathData != null)
{ {
// If path is non-null then the target router short-circuited, we don't expect this // If path is non-null then the target router short-circuited, we don't expect this
@ -389,7 +407,7 @@ namespace Microsoft.AspNetCore.Routing.Tree
return pathData; return pathData;
} }
var path = entry.Binder.BindValues(bindingResult.AcceptedValues); var path = binder.BindValues(bindingResult.AcceptedValues);
if (path == null) if (path == null)
{ {
return null; return null;

View File

@ -12,7 +12,7 @@ namespace Microsoft.AspNetCore.Routing.Tree
{ {
Length = length; Length = length;
Matches = new List<TreeRouteMatchingEntry>(); Matches = new List<InboundMatch>();
Literals = new Dictionary<string, UrlMatchingNode>(StringComparer.OrdinalIgnoreCase); Literals = new Dictionary<string, UrlMatchingNode>(StringComparer.OrdinalIgnoreCase);
} }
@ -21,7 +21,7 @@ namespace Microsoft.AspNetCore.Routing.Tree
public bool IsCatchAll { get; set; } public bool IsCatchAll { get; set; }
// These entries are sorted by precedence then template // These entries are sorted by precedence then template
public List<TreeRouteMatchingEntry> Matches { get; } public List<InboundMatch> Matches { get; }
public Dictionary<string, UrlMatchingNode> Literals { get; } public Dictionary<string, UrlMatchingNode> Literals { get; }

View File

@ -16,9 +16,9 @@ namespace Microsoft.AspNetCore.Routing.Internal.Routing
public void SelectSingleEntry_NoCriteria() public void SelectSingleEntry_NoCriteria()
{ {
// Arrange // Arrange
var entries = new List<TreeRouteLinkGenerationEntry>(); var entries = new List<OutboundMatch>();
var entry = CreateEntry(new { }); var entry = CreateMatch(new { });
entries.Add(entry); entries.Add(entry);
var tree = new LinkGenerationDecisionTree(entries); var tree = new LinkGenerationDecisionTree(entries);
@ -29,16 +29,16 @@ namespace Microsoft.AspNetCore.Routing.Internal.Routing
var matches = tree.GetMatches(context); var matches = tree.GetMatches(context);
// Assert // Assert
Assert.Same(entry, Assert.Single(matches).Entry); Assert.Same(entry, Assert.Single(matches).Match);
} }
[Fact] [Fact]
public void SelectSingleEntry_MultipleCriteria() public void SelectSingleEntry_MultipleCriteria()
{ {
// Arrange // Arrange
var entries = new List<TreeRouteLinkGenerationEntry>(); var entries = new List<OutboundMatch>();
var entry = CreateEntry(new { controller = "Store", action = "Buy" }); var entry = CreateMatch(new { controller = "Store", action = "Buy" });
entries.Add(entry); entries.Add(entry);
var tree = new LinkGenerationDecisionTree(entries); var tree = new LinkGenerationDecisionTree(entries);
@ -49,16 +49,16 @@ namespace Microsoft.AspNetCore.Routing.Internal.Routing
var matches = tree.GetMatches(context); var matches = tree.GetMatches(context);
// Assert // Assert
Assert.Same(entry, Assert.Single(matches).Entry); Assert.Same(entry, Assert.Single(matches).Match);
} }
[Fact] [Fact]
public void SelectSingleEntry_MultipleCriteria_AmbientValues() public void SelectSingleEntry_MultipleCriteria_AmbientValues()
{ {
// Arrange // Arrange
var entries = new List<TreeRouteLinkGenerationEntry>(); var entries = new List<OutboundMatch>();
var entry = CreateEntry(new { controller = "Store", action = "Buy" }); var entry = CreateMatch(new { controller = "Store", action = "Buy" });
entries.Add(entry); entries.Add(entry);
var tree = new LinkGenerationDecisionTree(entries); var tree = new LinkGenerationDecisionTree(entries);
@ -70,7 +70,7 @@ namespace Microsoft.AspNetCore.Routing.Internal.Routing
// Assert // Assert
var match = Assert.Single(matches); var match = Assert.Single(matches);
Assert.Same(entry, match.Entry); Assert.Same(entry, match.Match);
Assert.False(match.IsFallbackMatch); Assert.False(match.IsFallbackMatch);
} }
@ -78,9 +78,9 @@ namespace Microsoft.AspNetCore.Routing.Internal.Routing
public void SelectSingleEntry_MultipleCriteria_Replaced() public void SelectSingleEntry_MultipleCriteria_Replaced()
{ {
// Arrange // Arrange
var entries = new List<TreeRouteLinkGenerationEntry>(); var entries = new List<OutboundMatch>();
var entry = CreateEntry(new { controller = "Store", action = "Buy" }); var entry = CreateMatch(new { controller = "Store", action = "Buy" });
entries.Add(entry); entries.Add(entry);
var tree = new LinkGenerationDecisionTree(entries); var tree = new LinkGenerationDecisionTree(entries);
@ -94,7 +94,7 @@ namespace Microsoft.AspNetCore.Routing.Internal.Routing
// Assert // Assert
var match = Assert.Single(matches); var match = Assert.Single(matches);
Assert.Same(entry, match.Entry); Assert.Same(entry, match.Match);
Assert.False(match.IsFallbackMatch); Assert.False(match.IsFallbackMatch);
} }
@ -102,9 +102,9 @@ namespace Microsoft.AspNetCore.Routing.Internal.Routing
public void SelectSingleEntry_MultipleCriteria_AmbientValue_Ignored() public void SelectSingleEntry_MultipleCriteria_AmbientValue_Ignored()
{ {
// Arrange // Arrange
var entries = new List<TreeRouteLinkGenerationEntry>(); var entries = new List<OutboundMatch>();
var entry = CreateEntry(new { controller = "Store", action = (string)null }); var entry = CreateMatch(new { controller = "Store", action = (string)null });
entries.Add(entry); entries.Add(entry);
var tree = new LinkGenerationDecisionTree(entries); var tree = new LinkGenerationDecisionTree(entries);
@ -118,7 +118,7 @@ namespace Microsoft.AspNetCore.Routing.Internal.Routing
// Assert // Assert
var match = Assert.Single(matches); var match = Assert.Single(matches);
Assert.Same(entry, match.Entry); Assert.Same(entry, match.Match);
Assert.True(match.IsFallbackMatch); Assert.True(match.IsFallbackMatch);
} }
@ -126,9 +126,9 @@ namespace Microsoft.AspNetCore.Routing.Internal.Routing
public void SelectSingleEntry_MultipleCriteria_NoMatch() public void SelectSingleEntry_MultipleCriteria_NoMatch()
{ {
// Arrange // Arrange
var entries = new List<TreeRouteLinkGenerationEntry>(); var entries = new List<OutboundMatch>();
var entry = CreateEntry(new { controller = "Store", action = "Buy" }); var entry = CreateMatch(new { controller = "Store", action = "Buy" });
entries.Add(entry); entries.Add(entry);
var tree = new LinkGenerationDecisionTree(entries); var tree = new LinkGenerationDecisionTree(entries);
@ -146,9 +146,9 @@ namespace Microsoft.AspNetCore.Routing.Internal.Routing
public void SelectSingleEntry_MultipleCriteria_AmbientValue_NoMatch() public void SelectSingleEntry_MultipleCriteria_AmbientValue_NoMatch()
{ {
// Arrange // Arrange
var entries = new List<TreeRouteLinkGenerationEntry>(); var entries = new List<OutboundMatch>();
var entry = CreateEntry(new { controller = "Store", action = "Buy" }); var entry = CreateMatch(new { controller = "Store", action = "Buy" });
entries.Add(entry); entries.Add(entry);
var tree = new LinkGenerationDecisionTree(entries); var tree = new LinkGenerationDecisionTree(entries);
@ -168,12 +168,12 @@ namespace Microsoft.AspNetCore.Routing.Internal.Routing
public void SelectMultipleEntries_OneDoesntMatch() public void SelectMultipleEntries_OneDoesntMatch()
{ {
// Arrange // Arrange
var entries = new List<TreeRouteLinkGenerationEntry>(); var entries = new List<OutboundMatch>();
var entry1 = CreateEntry(new { controller = "Store", action = "Buy" }); var entry1 = CreateMatch(new { controller = "Store", action = "Buy" });
entries.Add(entry1); entries.Add(entry1);
var entry2 = CreateEntry(new { controller = "Store", action = "Cart" }); var entry2 = CreateMatch(new { controller = "Store", action = "Cart" });
entries.Add(entry2); entries.Add(entry2);
var tree = new LinkGenerationDecisionTree(entries); var tree = new LinkGenerationDecisionTree(entries);
@ -186,20 +186,20 @@ namespace Microsoft.AspNetCore.Routing.Internal.Routing
var matches = tree.GetMatches(context); var matches = tree.GetMatches(context);
// Assert // Assert
Assert.Same(entry1, Assert.Single(matches).Entry); Assert.Same(entry1, Assert.Single(matches).Match);
} }
[Fact] [Fact]
public void SelectMultipleEntries_BothMatch_CriteriaSubset() public void SelectMultipleEntries_BothMatch_CriteriaSubset()
{ {
// Arrange // Arrange
var entries = new List<TreeRouteLinkGenerationEntry>(); var entries = new List<OutboundMatch>();
var entry1 = CreateEntry(new { controller = "Store", action = "Buy" }); var entry1 = CreateMatch(new { controller = "Store", action = "Buy" });
entries.Add(entry1); entries.Add(entry1);
var entry2 = CreateEntry(new { controller = "Store" }); var entry2 = CreateMatch(new { controller = "Store" });
entry2.Order = 1; entry2.Entry.Order = 1;
entries.Add(entry2); entries.Add(entry2);
var tree = new LinkGenerationDecisionTree(entries); var tree = new LinkGenerationDecisionTree(entries);
@ -209,7 +209,7 @@ namespace Microsoft.AspNetCore.Routing.Internal.Routing
ambientValues: new { controller = "Store", action = "Buy" }); ambientValues: new { controller = "Store", action = "Buy" });
// Act // Act
var matches = tree.GetMatches(context).Select(m => m.Entry).ToList(); var matches = tree.GetMatches(context).Select(m => m.Match).ToList();
// Assert // Assert
Assert.Equal(entries, matches); Assert.Equal(entries, matches);
@ -219,13 +219,13 @@ namespace Microsoft.AspNetCore.Routing.Internal.Routing
public void SelectMultipleEntries_BothMatch_NonOverlappingCriteria() public void SelectMultipleEntries_BothMatch_NonOverlappingCriteria()
{ {
// Arrange // Arrange
var entries = new List<TreeRouteLinkGenerationEntry>(); var entries = new List<OutboundMatch>();
var entry1 = CreateEntry(new { controller = "Store", action = "Buy" }); var entry1 = CreateMatch(new { controller = "Store", action = "Buy" });
entries.Add(entry1); entries.Add(entry1);
var entry2 = CreateEntry(new { slug = "1234" }); var entry2 = CreateMatch(new { slug = "1234" });
entry2.Order = 1; entry2.Entry.Order = 1;
entries.Add(entry2); entries.Add(entry2);
var tree = new LinkGenerationDecisionTree(entries); var tree = new LinkGenerationDecisionTree(entries);
@ -233,7 +233,7 @@ namespace Microsoft.AspNetCore.Routing.Internal.Routing
var context = CreateContext(new { controller = "Store", action = "Buy", slug = "1234" }); var context = CreateContext(new { controller = "Store", action = "Buy", slug = "1234" });
// Act // Act
var matches = tree.GetMatches(context).Select(m => m.Entry).ToList(); var matches = tree.GetMatches(context).Select(m => m.Match).ToList();
// Assert // Assert
Assert.Equal(entries, matches); Assert.Equal(entries, matches);
@ -244,15 +244,15 @@ namespace Microsoft.AspNetCore.Routing.Internal.Routing
public void SelectMultipleEntries_BothMatch_OrderedByOrder() public void SelectMultipleEntries_BothMatch_OrderedByOrder()
{ {
// Arrange // Arrange
var entries = new List<TreeRouteLinkGenerationEntry>(); var entries = new List<OutboundMatch>();
var entry1 = CreateEntry(new { controller = "Store", action = "Buy" }); var entry1 = CreateMatch(new { controller = "Store", action = "Buy" });
entry1.GenerationPrecedence = 0; entry1.Entry.Precedence = 0;
entries.Add(entry1); entries.Add(entry1);
var entry2 = CreateEntry(new { controller = "Store", action = "Buy" }); var entry2 = CreateMatch(new { controller = "Store", action = "Buy" });
entry2.Order = 1; entry2.Entry.Order = 1;
entry2.GenerationPrecedence = 1; entry2.Entry.Precedence = 1;
entries.Add(entry2); entries.Add(entry2);
var tree = new LinkGenerationDecisionTree(entries); var tree = new LinkGenerationDecisionTree(entries);
@ -260,7 +260,7 @@ namespace Microsoft.AspNetCore.Routing.Internal.Routing
var context = CreateContext(new { controller = "Store", action = "Buy" }); var context = CreateContext(new { controller = "Store", action = "Buy" });
// Act // Act
var matches = tree.GetMatches(context).Select(m => m.Entry).ToList(); var matches = tree.GetMatches(context).Select(m => m.Match).ToList();
// Assert // Assert
Assert.Equal(entries, matches); Assert.Equal(entries, matches);
@ -271,14 +271,14 @@ namespace Microsoft.AspNetCore.Routing.Internal.Routing
public void SelectMultipleEntries_BothMatch_OrderedByPrecedence() public void SelectMultipleEntries_BothMatch_OrderedByPrecedence()
{ {
// Arrange // Arrange
var entries = new List<TreeRouteLinkGenerationEntry>(); var entries = new List<OutboundMatch>();
var entry1 = CreateEntry(new { controller = "Store", action = "Buy" }); var entry1 = CreateMatch(new { controller = "Store", action = "Buy" });
entry1.GenerationPrecedence = 1; entry1.Entry.Precedence = 1;
entries.Add(entry1); entries.Add(entry1);
var entry2 = CreateEntry(new { controller = "Store", action = "Buy" }); var entry2 = CreateMatch(new { controller = "Store", action = "Buy" });
entry2.GenerationPrecedence = 0; entry2.Entry.Precedence = 0;
entries.Add(entry2); entries.Add(entry2);
var tree = new LinkGenerationDecisionTree(entries); var tree = new LinkGenerationDecisionTree(entries);
@ -286,7 +286,7 @@ namespace Microsoft.AspNetCore.Routing.Internal.Routing
var context = CreateContext(new { controller = "Store", action = "Buy" }); var context = CreateContext(new { controller = "Store", action = "Buy" });
// Act // Act
var matches = tree.GetMatches(context).Select(m => m.Entry).ToList(); var matches = tree.GetMatches(context).Select(m => m.Match).ToList();
// Assert // Assert
Assert.Equal(entries, matches); Assert.Equal(entries, matches);
@ -297,14 +297,14 @@ namespace Microsoft.AspNetCore.Routing.Internal.Routing
public void SelectMultipleEntries_BothMatch_OrderedByTemplate() public void SelectMultipleEntries_BothMatch_OrderedByTemplate()
{ {
// Arrange // Arrange
var entries = new List<TreeRouteLinkGenerationEntry>(); var entries = new List<OutboundMatch>();
var entry1 = CreateEntry(new { controller = "Store", action = "Buy" }); var entry1 = CreateMatch(new { controller = "Store", action = "Buy" });
entry1.Template = TemplateParser.Parse("a"); entry1.Entry.RouteTemplate = TemplateParser.Parse("a");
entries.Add(entry1); entries.Add(entry1);
var entry2 = CreateEntry(new { controller = "Store", action = "Buy" }); var entry2 = CreateMatch(new { controller = "Store", action = "Buy" });
entry2.Template = TemplateParser.Parse("b"); entry2.Entry.RouteTemplate = TemplateParser.Parse("b");
entries.Add(entry2); entries.Add(entry2);
var tree = new LinkGenerationDecisionTree(entries); var tree = new LinkGenerationDecisionTree(entries);
@ -312,17 +312,18 @@ namespace Microsoft.AspNetCore.Routing.Internal.Routing
var context = CreateContext(new { controller = "Store", action = "Buy" }); var context = CreateContext(new { controller = "Store", action = "Buy" });
// Act // Act
var matches = tree.GetMatches(context).Select(m => m.Entry).ToList(); var matches = tree.GetMatches(context).Select(m => m.Match).ToList();
// Assert // Assert
Assert.Equal(entries, matches); Assert.Equal(entries, matches);
} }
private TreeRouteLinkGenerationEntry CreateEntry(object requiredValues) private OutboundMatch CreateMatch(object requiredValues)
{ {
var entry = new TreeRouteLinkGenerationEntry(); var match = new OutboundMatch();
entry.RequiredLinkValues = new RouteValueDictionary(requiredValues); match.Entry = new OutboundRouteEntry();
return entry; match.Entry.RequiredLinkValues = new RouteValueDictionary(requiredValues);
return match;
} }
private VirtualPathContext CreateContext(object values, object ambientValues = null) private VirtualPathContext CreateContext(object values, object ambientValues = null)

View File

@ -102,11 +102,11 @@ namespace Microsoft.AspNetCore.Routing.Template
private static decimal ComputeMatched(string template) private static decimal ComputeMatched(string template)
{ {
return Compute(template, RoutePrecedence.ComputeMatched); return Compute(template, RoutePrecedence.ComputeInbound);
} }
private static decimal ComputeGenerated(string template) private static decimal ComputeGenerated(string template)
{ {
return Compute(template, RoutePrecedence.ComputeGenerated); return Compute(template, RoutePrecedence.ComputeOutbound);
} }
private static decimal Compute(string template, Func<RouteTemplate, decimal> func) private static decimal Compute(string template, Func<RouteTemplate, decimal> func)

View File

@ -0,0 +1,88 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Routing.Internal;
using Microsoft.AspNetCore.Routing.Template;
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.Logging.Testing;
using Microsoft.Extensions.ObjectPool;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Routing.Tree
{
public class TreeRouteBuilderTest
{
[Fact]
public void TreeRouter_BuildThrows_RoutesWithTheSameNameAndDifferentTemplates()
{
// Arrange
var builder = CreateBuilder();
var message = "Two or more routes named 'Get_Products' have different templates.";
builder.MapOutbound(
Mock.Of<IRouter>(),
TemplateParser.Parse("api/Products"),
new RouteValueDictionary(),
"Get_Products",
order: 0);
builder.MapOutbound(
Mock.Of<IRouter>(),
TemplateParser.Parse("Products/Index"),
new RouteValueDictionary(),
"Get_Products",
order: 0);
// Act & Assert
ExceptionAssert.ThrowsArgument(() =>
{
builder.Build();
}, "linkGenerationEntries", message);
}
[Fact]
public void TreeRouter_BuildDoesNotThrow_RoutesWithTheSameNameAndSameTemplates()
{
// Arrange
var builder = CreateBuilder();
builder.MapOutbound(
Mock.Of<IRouter>(),
TemplateParser.Parse("api/Products"),
new RouteValueDictionary(),
"Get_Products",
order: 0);
builder.MapOutbound(
Mock.Of<IRouter>(),
TemplateParser.Parse("api/products"),
new RouteValueDictionary(),
"Get_Products",
order: 0);
// Act & Assert (does not throw)
builder.Build();
}
private static TreeRouteBuilder CreateBuilder()
{
var objectPoolProvider = new DefaultObjectPoolProvider();
var objectPolicy = new UriBuilderContextPooledObjectPolicy(UrlEncoder.Default);
var objectPool = objectPoolProvider.Create<UriBuildingContext>(objectPolicy);
var constraintResolver = Mock.Of<IInlineConstraintResolver>();
var builder = new TreeRouteBuilder(
NullLoggerFactory.Instance,
UrlEncoder.Default,
objectPool,
constraintResolver);
return builder;
}
}
}