TreeRouter cleanup
This commit is contained in:
parent
ea2d30ff49
commit
e8ce0e7523
|
|
@ -5,6 +5,7 @@ using System;
|
|||
using System.Text.Encodings.Web;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.AspNetCore.Routing.Internal;
|
||||
using Microsoft.AspNetCore.Routing.Tree;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.ObjectPool;
|
||||
|
||||
|
|
@ -36,6 +37,10 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
return provider.Create<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));
|
||||
|
||||
return services;
|
||||
|
|
|
|||
|
|
@ -11,23 +11,23 @@ namespace Microsoft.AspNetCore.Routing.Internal
|
|||
// A decision tree that matches link generation entries based on route data.
|
||||
public class LinkGenerationDecisionTree
|
||||
{
|
||||
private readonly DecisionTreeNode<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,
|
||||
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
|
||||
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);
|
||||
results.Sort(LinkGenerationMatchComparer.Instance);
|
||||
results.Sort(OutboundMatchResultComparer.Instance);
|
||||
return results;
|
||||
}
|
||||
|
||||
|
|
@ -60,16 +60,16 @@ namespace Microsoft.AspNetCore.Routing.Internal
|
|||
//
|
||||
// The decision tree uses a tree data structure to execute these rules across all candidates at once.
|
||||
private void Walk(
|
||||
List<LinkGenerationMatch> results,
|
||||
List<OutboundMatchResult> results,
|
||||
VirtualPathContext context,
|
||||
DecisionTreeNode<TreeRouteLinkGenerationEntry> node,
|
||||
DecisionTreeNode<OutboundMatch> node,
|
||||
bool isFallbackPath)
|
||||
{
|
||||
// Any entries in node.Matches have had all their required values satisfied, so add them
|
||||
// to the results.
|
||||
for (var i = 0; i < node.Matches.Count; i++)
|
||||
{
|
||||
results.Add(new LinkGenerationMatch(node.Matches[i], isFallbackPath));
|
||||
results.Add(new OutboundMatchResult(node.Matches[i], isFallbackPath));
|
||||
}
|
||||
|
||||
for (var i = 0; i < node.Criteria.Count; i++)
|
||||
|
|
@ -80,7 +80,7 @@ namespace Microsoft.AspNetCore.Routing.Internal
|
|||
object value;
|
||||
if (context.Values.TryGetValue(key, out value))
|
||||
{
|
||||
DecisionTreeNode<TreeRouteLinkGenerationEntry> branch;
|
||||
DecisionTreeNode<OutboundMatch> branch;
|
||||
if (criterion.Branches.TryGetValue(value ?? string.Empty, out branch))
|
||||
{
|
||||
Walk(results, context, branch, isFallbackPath);
|
||||
|
|
@ -91,7 +91,7 @@ namespace Microsoft.AspNetCore.Routing.Internal
|
|||
// If a value wasn't explicitly supplied, match BOTH the ambient value and the empty value
|
||||
// if an ambient value was supplied. The path explored with the empty value is considered
|
||||
// the fallback path.
|
||||
DecisionTreeNode<TreeRouteLinkGenerationEntry> branch;
|
||||
DecisionTreeNode<OutboundMatch> branch;
|
||||
if (context.AmbientValues.TryGetValue(key, out value) &&
|
||||
!criterion.Branches.Comparer.Equals(value, string.Empty))
|
||||
{
|
||||
|
|
@ -109,19 +109,19 @@ namespace Microsoft.AspNetCore.Routing.Internal
|
|||
}
|
||||
}
|
||||
|
||||
private class AttributeRouteLinkGenerationEntryClassifier : IClassifier<TreeRouteLinkGenerationEntry>
|
||||
private class OutboundMatchClassifier : IClassifier<OutboundMatch>
|
||||
{
|
||||
public AttributeRouteLinkGenerationEntryClassifier()
|
||||
public OutboundMatchClassifier()
|
||||
{
|
||||
ValueComparer = new RouteValueEqualityComparer();
|
||||
}
|
||||
|
||||
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);
|
||||
foreach (var kvp in item.RequiredLinkValues)
|
||||
foreach (var kvp in item.Entry.RequiredLinkValues)
|
||||
{
|
||||
results.Add(kvp.Key, new DecisionCriterionValue(kvp.Value ?? string.Empty));
|
||||
}
|
||||
|
|
@ -130,22 +130,22 @@ namespace Microsoft.AspNetCore.Routing.Internal
|
|||
}
|
||||
}
|
||||
|
||||
private class LinkGenerationMatchComparer : IComparer<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.
|
||||
if (x.Entry.Order != y.Entry.Order)
|
||||
if (x.Match.Entry.Order != y.Match.Entry.Order)
|
||||
{
|
||||
return x.Entry.Order.CompareTo(y.Entry.Order);
|
||||
return x.Match.Entry.Order.CompareTo(y.Match.Entry.Order);
|
||||
}
|
||||
|
||||
if (x.Entry.GenerationPrecedence != y.Entry.GenerationPrecedence)
|
||||
if (x.Match.Entry.Precedence != y.Match.Entry.Precedence)
|
||||
{
|
||||
// Reversed because higher is better
|
||||
return y.Entry.GenerationPrecedence.CompareTo(x.Entry.GenerationPrecedence);
|
||||
return y.Match.Entry.Precedence.CompareTo(x.Match.Entry.Precedence);
|
||||
}
|
||||
|
||||
if (x.IsFallbackMatch != y.IsFallbackMatch)
|
||||
|
|
@ -154,7 +154,9 @@ namespace Microsoft.AspNetCore.Routing.Internal
|
|||
return x.IsFallbackMatch.CompareTo(y.IsFallbackMatch);
|
||||
}
|
||||
|
||||
return StringComparer.Ordinal.Compare(x.Entry.Template.TemplateText, y.Entry.Template.TemplateText);
|
||||
return StringComparer.Ordinal.Compare(
|
||||
x.Match.Entry.RouteTemplate.TemplateText,
|
||||
y.Match.Entry.RouteTemplate.TemplateText);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; } }
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@ using System.Linq;
|
|||
namespace Microsoft.AspNetCore.Routing.Template
|
||||
{
|
||||
/// <summary>
|
||||
/// Computes precedence for an attribute route template.
|
||||
/// Computes precedence for a route template.
|
||||
/// </summary>
|
||||
public static class RoutePrecedence
|
||||
{
|
||||
|
|
@ -17,7 +17,7 @@ namespace Microsoft.AspNetCore.Routing.Template
|
|||
// /api/template/{id} == 1.13
|
||||
// /api/{id:int} == 1.2
|
||||
// /api/template/{id:int} == 1.12
|
||||
public static decimal ComputeMatched(RouteTemplate template)
|
||||
public static decimal ComputeInbound(RouteTemplate template)
|
||||
{
|
||||
// Each precedence digit corresponds to one decimal place. For example, 3 segments with precedences 2, 1,
|
||||
// and 4 results in a combined precedence of 2.14 (decimal).
|
||||
|
|
@ -27,7 +27,7 @@ namespace Microsoft.AspNetCore.Routing.Template
|
|||
{
|
||||
var segment = template.Segments[i];
|
||||
|
||||
var digit = ComputeMatchDigit(segment);
|
||||
var digit = ComputeInboundPrecedenceDigit(segment);
|
||||
Debug.Assert(digit >= 0 && digit < 10);
|
||||
|
||||
precedence += decimal.Divide(digit, (decimal)Math.Pow(10, i));
|
||||
|
|
@ -41,7 +41,7 @@ namespace Microsoft.AspNetCore.Routing.Template
|
|||
// /api/template/{id} == 5.53
|
||||
// /api/{id:int} == 5.4
|
||||
// /api/template/{id:int} == 5.54
|
||||
public static decimal ComputeGenerated(RouteTemplate template)
|
||||
public static decimal ComputeOutbound(RouteTemplate template)
|
||||
{
|
||||
// Each precedence digit corresponds to one decimal place. For example, 3 segments with precedences 2, 1,
|
||||
// and 4 results in a combined precedence of 2.14 (decimal).
|
||||
|
|
@ -51,7 +51,7 @@ namespace Microsoft.AspNetCore.Routing.Template
|
|||
{
|
||||
var segment = template.Segments[i];
|
||||
|
||||
var digit = ComputeGenerationDigit(segment);
|
||||
var digit = ComputeOutboundPrecedenceDigit(segment);
|
||||
Debug.Assert(digit >= 0 && digit < 10);
|
||||
|
||||
precedence += decimal.Divide(digit, (decimal)Math.Pow(10, i));
|
||||
|
|
@ -66,7 +66,7 @@ namespace Microsoft.AspNetCore.Routing.Template
|
|||
// 3 - Unconstrained parameter segements
|
||||
// 2 - Constrained wildcard parameter segments
|
||||
// 1 - Unconstrained wildcard parameter segments
|
||||
private static int ComputeGenerationDigit(TemplateSegment segment)
|
||||
private static int ComputeOutboundPrecedenceDigit(TemplateSegment segment)
|
||||
{
|
||||
if(segment.Parts.Count > 1)
|
||||
{
|
||||
|
|
@ -98,7 +98,7 @@ namespace Microsoft.AspNetCore.Routing.Template
|
|||
// 3 - Unconstrained parameter segments
|
||||
// 4 - Constrained wildcard parameter segments
|
||||
// 5 - Unconstrained wildcard parameter segments
|
||||
private static int ComputeMatchDigit(TemplateSegment segment)
|
||||
private static int ComputeInboundPrecedenceDigit(TemplateSegment segment)
|
||||
{
|
||||
if (segment.Parts.Count > 1)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -7,16 +7,11 @@ using Microsoft.AspNetCore.Routing.Template;
|
|||
namespace Microsoft.AspNetCore.Routing.Tree
|
||||
{
|
||||
/// <summary>
|
||||
/// Used to build a <see cref="TreeRouter"/>. Represents an individual URL-generating route that will be
|
||||
/// aggregated into the <see cref="TreeRouter"/>.
|
||||
/// Used to build a <see cref="TreeRouter"/>. Represents a URL template that will be used to generate
|
||||
/// outgoing URLs.
|
||||
/// </summary>
|
||||
public class TreeRouteLinkGenerationEntry
|
||||
public class OutboundRouteEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="TemplateBinder"/>.
|
||||
/// </summary>
|
||||
public TemplateBinder Binder { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the route constraints.
|
||||
/// </summary>
|
||||
|
|
@ -25,37 +20,43 @@ namespace Microsoft.AspNetCore.Routing.Tree
|
|||
/// <summary>
|
||||
/// Gets or sets the route defaults.
|
||||
/// </summary>
|
||||
public IDictionary<string, object> Defaults { get; set; }
|
||||
public RouteValueDictionary Defaults { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the order of the template.
|
||||
/// 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 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>
|
||||
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>
|
||||
/// Gets or sets the name of the route.
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the route group.
|
||||
/// </summary>
|
||||
public string RouteGroup { get; set; }
|
||||
public string RouteName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the set of values that must be present for link genration.
|
||||
/// </summary>
|
||||
public IDictionary<string, object> RequiredLinkValues { get; set; }
|
||||
public RouteValueDictionary RequiredLinkValues { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="Template"/>.
|
||||
/// Gets or sets the <see cref="RouteTemplate"/>.
|
||||
/// </summary>
|
||||
public RouteTemplate Template { get; set; }
|
||||
public RouteTemplate RouteTemplate { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,47 +1,195 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Text.Encodings.Web;
|
||||
using Microsoft.AspNetCore.Routing.Internal;
|
||||
using Microsoft.AspNetCore.Routing.Template;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.ObjectPool;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Tree
|
||||
{
|
||||
public class TreeRouteBuilder
|
||||
{
|
||||
private readonly IRouter _target;
|
||||
private readonly List<TreeRouteLinkGenerationEntry> _generatingEntries;
|
||||
private readonly List<TreeRouteMatchingEntry> _matchingEntries;
|
||||
|
||||
private readonly ILogger _logger;
|
||||
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;
|
||||
_generatingEntries = new List<TreeRouteLinkGenerationEntry>();
|
||||
_matchingEntries = new List<TreeRouteMatchingEntry>();
|
||||
if (loggerFactory == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(loggerFactory));
|
||||
}
|
||||
|
||||
if (urlEncoder == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(urlEncoder));
|
||||
}
|
||||
|
||||
if (objectPool == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(objectPool));
|
||||
}
|
||||
|
||||
if (constraintResolver == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(constraintResolver));
|
||||
}
|
||||
|
||||
_urlEncoder = urlEncoder;
|
||||
_objectPool = objectPool;
|
||||
_constraintResolver = constraintResolver;
|
||||
|
||||
_logger = loggerFactory.CreateLogger<TreeRouter>();
|
||||
_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)
|
||||
{
|
||||
var trees = new Dictionary<int, UrlMatchingTree>();
|
||||
|
||||
foreach (var entry in _matchingEntries)
|
||||
foreach (var entry in InboundEntries)
|
||||
{
|
||||
UrlMatchingTree tree;
|
||||
if (!trees.TryGetValue(entry.Order, out tree))
|
||||
|
|
@ -54,9 +202,10 @@ namespace Microsoft.AspNetCore.Routing.Tree
|
|||
}
|
||||
|
||||
return new TreeRouter(
|
||||
_target,
|
||||
trees.Values.OrderBy(tree => tree.Order).ToArray(),
|
||||
_generatingEntries,
|
||||
OutboundEntries,
|
||||
_urlEncoder,
|
||||
_objectPool,
|
||||
_logger,
|
||||
_constraintLogger,
|
||||
version);
|
||||
|
|
@ -64,14 +213,16 @@ namespace Microsoft.AspNetCore.Routing.Tree
|
|||
|
||||
public void Clear()
|
||||
{
|
||||
_generatingEntries.Clear();
|
||||
_matchingEntries.Clear();
|
||||
InboundEntries.Clear();
|
||||
OutboundEntries.Clear();
|
||||
}
|
||||
|
||||
private void AddEntryToTree(UrlMatchingTree tree, TreeRouteMatchingEntry entry)
|
||||
private void AddEntryToTree(UrlMatchingTree tree, InboundRouteEntry entry)
|
||||
{
|
||||
var current = tree.Root;
|
||||
|
||||
var matcher = new TemplateMatcher(entry.RouteTemplate, entry.Defaults);
|
||||
|
||||
for (var i = 0; i < entry.RouteTemplate.Segments.Count; i++)
|
||||
{
|
||||
var segment = entry.RouteTemplate.Segments[i];
|
||||
|
|
@ -104,7 +255,7 @@ namespace Microsoft.AspNetCore.Routing.Tree
|
|||
|
||||
if (part.IsParameter && (part.IsOptional || part.IsCatchAll))
|
||||
{
|
||||
current.Matches.Add(entry);
|
||||
current.Matches.Add(new InboundMatch() { Entry = entry, TemplateMatcher = matcher });
|
||||
}
|
||||
|
||||
if (part.IsParameter && part.InlineConstraints.Any() && !part.IsCatchAll)
|
||||
|
|
@ -154,11 +305,11 @@ namespace Microsoft.AspNetCore.Routing.Tree
|
|||
Debug.Fail("We shouldn't get here.");
|
||||
}
|
||||
|
||||
current.Matches.Add(entry);
|
||||
current.Matches.Add(new InboundMatch() { Entry = entry, TemplateMatcher = matcher });
|
||||
current.Matches.Sort((x, y) =>
|
||||
{
|
||||
var result = x.Precedence.CompareTo(y.Precedence);
|
||||
return result == 0 ? x.RouteTemplate.TemplateText.CompareTo(y.RouteTemplate.TemplateText) : result;
|
||||
var result = x.Entry.Precedence.CompareTo(y.Entry.Precedence);
|
||||
return result == 0 ? x.Entry.RouteTemplate.TemplateText.CompareTo(y.Entry.RouteTemplate.TemplateText) : result;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -4,12 +4,13 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Routing.Internal;
|
||||
using Microsoft.AspNetCore.Routing.Logging;
|
||||
using Microsoft.Extensions.Internal;
|
||||
using Microsoft.AspNetCore.Routing.Template;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.ObjectPool;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Tree
|
||||
{
|
||||
|
|
@ -22,10 +23,9 @@ namespace Microsoft.AspNetCore.Routing.Tree
|
|||
// group of action descriptors.
|
||||
public static readonly string RouteGroupKey = "!__route_group";
|
||||
|
||||
private readonly IRouter _next;
|
||||
private readonly LinkGenerationDecisionTree _linkGenerationTree;
|
||||
private readonly UrlMatchingTree[] _trees;
|
||||
private readonly IDictionary<string, TreeRouteLinkGenerationEntry> _namedEntries;
|
||||
private readonly IDictionary<string, OutboundMatch> _namedEntries;
|
||||
|
||||
private readonly ILogger _logger;
|
||||
private readonly ILogger _constraintLogger;
|
||||
|
|
@ -33,26 +33,23 @@ namespace Microsoft.AspNetCore.Routing.Tree
|
|||
/// <summary>
|
||||
/// Creates a new <see cref="TreeRouter"/>.
|
||||
/// </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="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="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,
|
||||
UrlMatchingTree[] trees,
|
||||
IEnumerable<TreeRouteLinkGenerationEntry> linkGenerationEntries,
|
||||
IEnumerable<OutboundRouteEntry> linkGenerationEntries,
|
||||
UrlEncoder urlEncoder,
|
||||
ObjectPool<UriBuildingContext> objectPool,
|
||||
ILogger routeLogger,
|
||||
ILogger constraintLogger,
|
||||
int version)
|
||||
{
|
||||
if (next == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(next));
|
||||
}
|
||||
|
||||
if (trees == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(trees));
|
||||
|
|
@ -63,6 +60,16 @@ namespace Microsoft.AspNetCore.Routing.Tree
|
|||
throw new ArgumentNullException(nameof(linkGenerationEntries));
|
||||
}
|
||||
|
||||
if (urlEncoder == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(urlEncoder));
|
||||
}
|
||||
|
||||
if (objectPool == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(objectPool));
|
||||
}
|
||||
|
||||
if (routeLogger == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(routeLogger));
|
||||
|
|
@ -73,43 +80,49 @@ namespace Microsoft.AspNetCore.Routing.Tree
|
|||
throw new ArgumentNullException(nameof(constraintLogger));
|
||||
}
|
||||
|
||||
_next = next;
|
||||
_trees = trees;
|
||||
_logger = routeLogger;
|
||||
_constraintLogger = constraintLogger;
|
||||
|
||||
var namedEntries = new Dictionary<string, TreeRouteLinkGenerationEntry>(
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
_namedEntries = new Dictionary<string, OutboundMatch>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var outboundMatches = new List<OutboundMatch>();
|
||||
|
||||
foreach (var entry in linkGenerationEntries)
|
||||
{
|
||||
|
||||
var binder = new TemplateBinder(urlEncoder, objectPool, entry.RouteTemplate, entry.Defaults);
|
||||
var outboundMatch = new OutboundMatch() { Entry = entry, TemplateBinder = binder };
|
||||
outboundMatches.Add(outboundMatch);
|
||||
|
||||
// Skip unnamed entries
|
||||
if (entry.Name == null)
|
||||
if (entry.RouteName == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// We only need to keep one AttributeRouteLinkGenerationEntry per route template
|
||||
// We only need to keep one OutboundMatch per route template
|
||||
// so in case two entries have the same name and the same template we only keep
|
||||
// the first entry.
|
||||
TreeRouteLinkGenerationEntry namedEntry = null;
|
||||
if (namedEntries.TryGetValue(entry.Name, out namedEntry) &&
|
||||
!namedEntry.Template.TemplateText.Equals(entry.Template.TemplateText, StringComparison.OrdinalIgnoreCase))
|
||||
OutboundMatch namedMatch;
|
||||
if (_namedEntries.TryGetValue(entry.RouteName, out namedMatch) &&
|
||||
!string.Equals(
|
||||
namedMatch.Entry.RouteTemplate.TemplateText,
|
||||
entry.RouteTemplate.TemplateText,
|
||||
StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
Resources.FormatAttributeRoute_DifferentLinkGenerationEntries_SameName(entry.Name),
|
||||
Resources.FormatAttributeRoute_DifferentLinkGenerationEntries_SameName(entry.RouteName),
|
||||
nameof(linkGenerationEntries));
|
||||
}
|
||||
else if (namedEntry == null)
|
||||
else if (namedMatch == null)
|
||||
{
|
||||
namedEntries.Add(entry.Name, entry);
|
||||
_namedEntries.Add(entry.RouteName, outboundMatch);
|
||||
}
|
||||
}
|
||||
|
||||
_namedEntries = namedEntries;
|
||||
|
||||
// The decision tree will take care of ordering for these entries.
|
||||
_linkGenerationTree = new LinkGenerationDecisionTree(linkGenerationEntries.ToArray());
|
||||
_linkGenerationTree = new LinkGenerationDecisionTree(outboundMatches.ToArray());
|
||||
|
||||
Version = version;
|
||||
}
|
||||
|
|
@ -145,7 +158,7 @@ namespace Microsoft.AspNetCore.Routing.Tree
|
|||
|
||||
for (var i = 0; i < matches.Count; i++)
|
||||
{
|
||||
var path = GenerateVirtualPath(context, matches[i].Entry);
|
||||
var path = GenerateVirtualPath(context, matches[i].Match.Entry, matches[i].Match.TemplateBinder);
|
||||
if (path != null)
|
||||
{
|
||||
return path;
|
||||
|
|
@ -175,7 +188,9 @@ namespace Microsoft.AspNetCore.Routing.Tree
|
|||
var node = treeEnumerator.Current;
|
||||
foreach (var item in node.Matches)
|
||||
{
|
||||
if (!item.TemplateMatcher.TryMatch(context.HttpContext.Request.Path, context.RouteData.Values))
|
||||
var entry = item.Entry;
|
||||
var matcher = item.TemplateMatcher;
|
||||
if (!matcher.TryMatch(context.HttpContext.Request.Path, context.RouteData.Values))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
|
@ -183,7 +198,7 @@ namespace Microsoft.AspNetCore.Routing.Tree
|
|||
try
|
||||
{
|
||||
if (!RouteConstraintMatcher.Match(
|
||||
item.Constraints,
|
||||
entry.Constraints,
|
||||
context.RouteData.Values,
|
||||
context.HttpContext,
|
||||
this,
|
||||
|
|
@ -193,10 +208,10 @@ namespace Microsoft.AspNetCore.Routing.Tree
|
|||
continue;
|
||||
}
|
||||
|
||||
_logger.MatchedRoute(item.RouteName, item.RouteTemplate.TemplateText);
|
||||
_logger.MatchedRoute(entry.RouteName, entry.RouteTemplate.TemplateText);
|
||||
context.RouteData.Routers.Add(entry.Handler);
|
||||
|
||||
context.RouteData.Routers.Add(item.Target);
|
||||
await item.Target.RouteAsync(context);
|
||||
await entry.Handler.RouteAsync(context);
|
||||
if (context.Handler != null)
|
||||
{
|
||||
return;
|
||||
|
|
@ -318,10 +333,10 @@ namespace Microsoft.AspNetCore.Routing.Tree
|
|||
|
||||
private VirtualPathData GetVirtualPathForNamedRoute(VirtualPathContext context)
|
||||
{
|
||||
TreeRouteLinkGenerationEntry entry;
|
||||
if (_namedEntries.TryGetValue(context.RouteName, out entry))
|
||||
OutboundMatch match;
|
||||
if (_namedEntries.TryGetValue(context.RouteName, out match))
|
||||
{
|
||||
var path = GenerateVirtualPath(context, entry);
|
||||
var path = GenerateVirtualPath(context, match.Entry, match.TemplateBinder);
|
||||
if (path != null)
|
||||
{
|
||||
return path;
|
||||
|
|
@ -330,7 +345,10 @@ namespace Microsoft.AspNetCore.Routing.Tree
|
|||
return null;
|
||||
}
|
||||
|
||||
private VirtualPathData GenerateVirtualPath(VirtualPathContext context, TreeRouteLinkGenerationEntry entry)
|
||||
private VirtualPathData GenerateVirtualPath(
|
||||
VirtualPathContext context,
|
||||
OutboundRouteEntry entry,
|
||||
TemplateBinder binder)
|
||||
{
|
||||
// In attribute the context includes the values that are used to select this entry - typically
|
||||
// these will be the standard 'action', 'controller' and maybe 'area' tokens. However, we don't
|
||||
|
|
@ -349,7 +367,7 @@ namespace Microsoft.AspNetCore.Routing.Tree
|
|||
{
|
||||
if (entry.RequiredLinkValues.ContainsKey(kvp.Key))
|
||||
{
|
||||
var parameter = entry.Template.GetParameter(kvp.Key);
|
||||
var parameter = entry.RouteTemplate.GetParameter(kvp.Key);
|
||||
|
||||
if (parameter == null)
|
||||
{
|
||||
|
|
@ -360,7 +378,7 @@ namespace Microsoft.AspNetCore.Routing.Tree
|
|||
inputValues.Add(kvp.Key, kvp.Value);
|
||||
}
|
||||
|
||||
var bindingResult = entry.Binder.GetValues(context.AmbientValues, inputValues);
|
||||
var bindingResult = binder.GetValues(context.AmbientValues, inputValues);
|
||||
if (bindingResult == null)
|
||||
{
|
||||
// A required parameter in the template didn't get a value.
|
||||
|
|
@ -381,7 +399,7 @@ namespace Microsoft.AspNetCore.Routing.Tree
|
|||
return null;
|
||||
}
|
||||
|
||||
var pathData = _next.GetVirtualPath(context);
|
||||
var pathData = entry.Handler.GetVirtualPath(context);
|
||||
if (pathData != null)
|
||||
{
|
||||
// If path is non-null then the target router short-circuited, we don't expect this
|
||||
|
|
@ -389,7 +407,7 @@ namespace Microsoft.AspNetCore.Routing.Tree
|
|||
return pathData;
|
||||
}
|
||||
|
||||
var path = entry.Binder.BindValues(bindingResult.AcceptedValues);
|
||||
var path = binder.BindValues(bindingResult.AcceptedValues);
|
||||
if (path == null)
|
||||
{
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ namespace Microsoft.AspNetCore.Routing.Tree
|
|||
{
|
||||
Length = length;
|
||||
|
||||
Matches = new List<TreeRouteMatchingEntry>();
|
||||
Matches = new List<InboundMatch>();
|
||||
Literals = new Dictionary<string, UrlMatchingNode>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
|
|
@ -21,7 +21,7 @@ namespace Microsoft.AspNetCore.Routing.Tree
|
|||
public bool IsCatchAll { get; set; }
|
||||
|
||||
// These entries are sorted by precedence then template
|
||||
public List<TreeRouteMatchingEntry> Matches { get; }
|
||||
public List<InboundMatch> Matches { get; }
|
||||
|
||||
public Dictionary<string, UrlMatchingNode> Literals { get; }
|
||||
|
||||
|
|
|
|||
|
|
@ -16,9 +16,9 @@ namespace Microsoft.AspNetCore.Routing.Internal.Routing
|
|||
public void SelectSingleEntry_NoCriteria()
|
||||
{
|
||||
// Arrange
|
||||
var entries = new List<TreeRouteLinkGenerationEntry>();
|
||||
var entries = new List<OutboundMatch>();
|
||||
|
||||
var entry = CreateEntry(new { });
|
||||
var entry = CreateMatch(new { });
|
||||
entries.Add(entry);
|
||||
|
||||
var tree = new LinkGenerationDecisionTree(entries);
|
||||
|
|
@ -29,16 +29,16 @@ namespace Microsoft.AspNetCore.Routing.Internal.Routing
|
|||
var matches = tree.GetMatches(context);
|
||||
|
||||
// Assert
|
||||
Assert.Same(entry, Assert.Single(matches).Entry);
|
||||
Assert.Same(entry, Assert.Single(matches).Match);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectSingleEntry_MultipleCriteria()
|
||||
{
|
||||
// Arrange
|
||||
var entries = new List<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);
|
||||
|
||||
var tree = new LinkGenerationDecisionTree(entries);
|
||||
|
|
@ -49,16 +49,16 @@ namespace Microsoft.AspNetCore.Routing.Internal.Routing
|
|||
var matches = tree.GetMatches(context);
|
||||
|
||||
// Assert
|
||||
Assert.Same(entry, Assert.Single(matches).Entry);
|
||||
Assert.Same(entry, Assert.Single(matches).Match);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectSingleEntry_MultipleCriteria_AmbientValues()
|
||||
{
|
||||
// Arrange
|
||||
var entries = new List<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);
|
||||
|
||||
var tree = new LinkGenerationDecisionTree(entries);
|
||||
|
|
@ -70,7 +70,7 @@ namespace Microsoft.AspNetCore.Routing.Internal.Routing
|
|||
|
||||
// Assert
|
||||
var match = Assert.Single(matches);
|
||||
Assert.Same(entry, match.Entry);
|
||||
Assert.Same(entry, match.Match);
|
||||
Assert.False(match.IsFallbackMatch);
|
||||
}
|
||||
|
||||
|
|
@ -78,9 +78,9 @@ namespace Microsoft.AspNetCore.Routing.Internal.Routing
|
|||
public void SelectSingleEntry_MultipleCriteria_Replaced()
|
||||
{
|
||||
// Arrange
|
||||
var entries = new List<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);
|
||||
|
||||
var tree = new LinkGenerationDecisionTree(entries);
|
||||
|
|
@ -94,7 +94,7 @@ namespace Microsoft.AspNetCore.Routing.Internal.Routing
|
|||
|
||||
// Assert
|
||||
var match = Assert.Single(matches);
|
||||
Assert.Same(entry, match.Entry);
|
||||
Assert.Same(entry, match.Match);
|
||||
Assert.False(match.IsFallbackMatch);
|
||||
}
|
||||
|
||||
|
|
@ -102,9 +102,9 @@ namespace Microsoft.AspNetCore.Routing.Internal.Routing
|
|||
public void SelectSingleEntry_MultipleCriteria_AmbientValue_Ignored()
|
||||
{
|
||||
// Arrange
|
||||
var entries = new List<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);
|
||||
|
||||
var tree = new LinkGenerationDecisionTree(entries);
|
||||
|
|
@ -118,7 +118,7 @@ namespace Microsoft.AspNetCore.Routing.Internal.Routing
|
|||
|
||||
// Assert
|
||||
var match = Assert.Single(matches);
|
||||
Assert.Same(entry, match.Entry);
|
||||
Assert.Same(entry, match.Match);
|
||||
Assert.True(match.IsFallbackMatch);
|
||||
}
|
||||
|
||||
|
|
@ -126,9 +126,9 @@ namespace Microsoft.AspNetCore.Routing.Internal.Routing
|
|||
public void SelectSingleEntry_MultipleCriteria_NoMatch()
|
||||
{
|
||||
// Arrange
|
||||
var entries = new List<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);
|
||||
|
||||
var tree = new LinkGenerationDecisionTree(entries);
|
||||
|
|
@ -146,9 +146,9 @@ namespace Microsoft.AspNetCore.Routing.Internal.Routing
|
|||
public void SelectSingleEntry_MultipleCriteria_AmbientValue_NoMatch()
|
||||
{
|
||||
// Arrange
|
||||
var entries = new List<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);
|
||||
|
||||
var tree = new LinkGenerationDecisionTree(entries);
|
||||
|
|
@ -168,12 +168,12 @@ namespace Microsoft.AspNetCore.Routing.Internal.Routing
|
|||
public void SelectMultipleEntries_OneDoesntMatch()
|
||||
{
|
||||
// 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);
|
||||
|
||||
var entry2 = CreateEntry(new { controller = "Store", action = "Cart" });
|
||||
var entry2 = CreateMatch(new { controller = "Store", action = "Cart" });
|
||||
entries.Add(entry2);
|
||||
|
||||
var tree = new LinkGenerationDecisionTree(entries);
|
||||
|
|
@ -186,20 +186,20 @@ namespace Microsoft.AspNetCore.Routing.Internal.Routing
|
|||
var matches = tree.GetMatches(context);
|
||||
|
||||
// Assert
|
||||
Assert.Same(entry1, Assert.Single(matches).Entry);
|
||||
Assert.Same(entry1, Assert.Single(matches).Match);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectMultipleEntries_BothMatch_CriteriaSubset()
|
||||
{
|
||||
// Arrange
|
||||
var entries = new List<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);
|
||||
|
||||
var entry2 = CreateEntry(new { controller = "Store" });
|
||||
entry2.Order = 1;
|
||||
var entry2 = CreateMatch(new { controller = "Store" });
|
||||
entry2.Entry.Order = 1;
|
||||
entries.Add(entry2);
|
||||
|
||||
var tree = new LinkGenerationDecisionTree(entries);
|
||||
|
|
@ -209,7 +209,7 @@ namespace Microsoft.AspNetCore.Routing.Internal.Routing
|
|||
ambientValues: new { controller = "Store", action = "Buy" });
|
||||
|
||||
// Act
|
||||
var matches = tree.GetMatches(context).Select(m => m.Entry).ToList();
|
||||
var matches = tree.GetMatches(context).Select(m => m.Match).ToList();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(entries, matches);
|
||||
|
|
@ -219,13 +219,13 @@ namespace Microsoft.AspNetCore.Routing.Internal.Routing
|
|||
public void SelectMultipleEntries_BothMatch_NonOverlappingCriteria()
|
||||
{
|
||||
// Arrange
|
||||
var entries = new List<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);
|
||||
|
||||
var entry2 = CreateEntry(new { slug = "1234" });
|
||||
entry2.Order = 1;
|
||||
var entry2 = CreateMatch(new { slug = "1234" });
|
||||
entry2.Entry.Order = 1;
|
||||
entries.Add(entry2);
|
||||
|
||||
var tree = new LinkGenerationDecisionTree(entries);
|
||||
|
|
@ -233,7 +233,7 @@ namespace Microsoft.AspNetCore.Routing.Internal.Routing
|
|||
var context = CreateContext(new { controller = "Store", action = "Buy", slug = "1234" });
|
||||
|
||||
// Act
|
||||
var matches = tree.GetMatches(context).Select(m => m.Entry).ToList();
|
||||
var matches = tree.GetMatches(context).Select(m => m.Match).ToList();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(entries, matches);
|
||||
|
|
@ -244,15 +244,15 @@ namespace Microsoft.AspNetCore.Routing.Internal.Routing
|
|||
public void SelectMultipleEntries_BothMatch_OrderedByOrder()
|
||||
{
|
||||
// Arrange
|
||||
var entries = new List<TreeRouteLinkGenerationEntry>();
|
||||
var entries = new List<OutboundMatch>();
|
||||
|
||||
var entry1 = CreateEntry(new { controller = "Store", action = "Buy" });
|
||||
entry1.GenerationPrecedence = 0;
|
||||
var entry1 = CreateMatch(new { controller = "Store", action = "Buy" });
|
||||
entry1.Entry.Precedence = 0;
|
||||
entries.Add(entry1);
|
||||
|
||||
var entry2 = CreateEntry(new { controller = "Store", action = "Buy" });
|
||||
entry2.Order = 1;
|
||||
entry2.GenerationPrecedence = 1;
|
||||
var entry2 = CreateMatch(new { controller = "Store", action = "Buy" });
|
||||
entry2.Entry.Order = 1;
|
||||
entry2.Entry.Precedence = 1;
|
||||
entries.Add(entry2);
|
||||
|
||||
var tree = new LinkGenerationDecisionTree(entries);
|
||||
|
|
@ -260,7 +260,7 @@ namespace Microsoft.AspNetCore.Routing.Internal.Routing
|
|||
var context = CreateContext(new { controller = "Store", action = "Buy" });
|
||||
|
||||
// Act
|
||||
var matches = tree.GetMatches(context).Select(m => m.Entry).ToList();
|
||||
var matches = tree.GetMatches(context).Select(m => m.Match).ToList();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(entries, matches);
|
||||
|
|
@ -271,14 +271,14 @@ namespace Microsoft.AspNetCore.Routing.Internal.Routing
|
|||
public void SelectMultipleEntries_BothMatch_OrderedByPrecedence()
|
||||
{
|
||||
// Arrange
|
||||
var entries = new List<TreeRouteLinkGenerationEntry>();
|
||||
var entries = new List<OutboundMatch>();
|
||||
|
||||
var entry1 = CreateEntry(new { controller = "Store", action = "Buy" });
|
||||
entry1.GenerationPrecedence = 1;
|
||||
var entry1 = CreateMatch(new { controller = "Store", action = "Buy" });
|
||||
entry1.Entry.Precedence = 1;
|
||||
entries.Add(entry1);
|
||||
|
||||
var entry2 = CreateEntry(new { controller = "Store", action = "Buy" });
|
||||
entry2.GenerationPrecedence = 0;
|
||||
var entry2 = CreateMatch(new { controller = "Store", action = "Buy" });
|
||||
entry2.Entry.Precedence = 0;
|
||||
entries.Add(entry2);
|
||||
|
||||
var tree = new LinkGenerationDecisionTree(entries);
|
||||
|
|
@ -286,7 +286,7 @@ namespace Microsoft.AspNetCore.Routing.Internal.Routing
|
|||
var context = CreateContext(new { controller = "Store", action = "Buy" });
|
||||
|
||||
// Act
|
||||
var matches = tree.GetMatches(context).Select(m => m.Entry).ToList();
|
||||
var matches = tree.GetMatches(context).Select(m => m.Match).ToList();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(entries, matches);
|
||||
|
|
@ -297,14 +297,14 @@ namespace Microsoft.AspNetCore.Routing.Internal.Routing
|
|||
public void SelectMultipleEntries_BothMatch_OrderedByTemplate()
|
||||
{
|
||||
// Arrange
|
||||
var entries = new List<TreeRouteLinkGenerationEntry>();
|
||||
var entries = new List<OutboundMatch>();
|
||||
|
||||
var entry1 = CreateEntry(new { controller = "Store", action = "Buy" });
|
||||
entry1.Template = TemplateParser.Parse("a");
|
||||
var entry1 = CreateMatch(new { controller = "Store", action = "Buy" });
|
||||
entry1.Entry.RouteTemplate = TemplateParser.Parse("a");
|
||||
entries.Add(entry1);
|
||||
|
||||
var entry2 = CreateEntry(new { controller = "Store", action = "Buy" });
|
||||
entry2.Template = TemplateParser.Parse("b");
|
||||
var entry2 = CreateMatch(new { controller = "Store", action = "Buy" });
|
||||
entry2.Entry.RouteTemplate = TemplateParser.Parse("b");
|
||||
entries.Add(entry2);
|
||||
|
||||
var tree = new LinkGenerationDecisionTree(entries);
|
||||
|
|
@ -312,17 +312,18 @@ namespace Microsoft.AspNetCore.Routing.Internal.Routing
|
|||
var context = CreateContext(new { controller = "Store", action = "Buy" });
|
||||
|
||||
// Act
|
||||
var matches = tree.GetMatches(context).Select(m => m.Entry).ToList();
|
||||
var matches = tree.GetMatches(context).Select(m => m.Match).ToList();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(entries, matches);
|
||||
}
|
||||
|
||||
private TreeRouteLinkGenerationEntry CreateEntry(object requiredValues)
|
||||
private OutboundMatch CreateMatch(object requiredValues)
|
||||
{
|
||||
var entry = new TreeRouteLinkGenerationEntry();
|
||||
entry.RequiredLinkValues = new RouteValueDictionary(requiredValues);
|
||||
return entry;
|
||||
var match = new OutboundMatch();
|
||||
match.Entry = new OutboundRouteEntry();
|
||||
match.Entry.RequiredLinkValues = new RouteValueDictionary(requiredValues);
|
||||
return match;
|
||||
}
|
||||
|
||||
private VirtualPathContext CreateContext(object values, object ambientValues = null)
|
||||
|
|
|
|||
|
|
@ -102,11 +102,11 @@ namespace Microsoft.AspNetCore.Routing.Template
|
|||
|
||||
private static decimal ComputeMatched(string template)
|
||||
{
|
||||
return Compute(template, RoutePrecedence.ComputeMatched);
|
||||
return Compute(template, RoutePrecedence.ComputeInbound);
|
||||
}
|
||||
private static decimal ComputeGenerated(string template)
|
||||
{
|
||||
return Compute(template, RoutePrecedence.ComputeGenerated);
|
||||
return Compute(template, RoutePrecedence.ComputeOutbound);
|
||||
}
|
||||
|
||||
private static decimal Compute(string template, Func<RouteTemplate, decimal> func)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue