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 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;

View File

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

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
{
/// <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)
{

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
{
/// <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; }
}
}

View File

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

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.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;

View File

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

View File

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

View File

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

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