Moved AttributeRouting from MVC

This commit is contained in:
Ajay Bhargav Baaskaran 2015-11-13 16:03:09 -08:00
parent f6616db53d
commit 4d69ad0db2
33 changed files with 4512 additions and 9 deletions

View File

@ -12,6 +12,12 @@
"Microsoft.AspNet.Routing": { }
}
},
"adx-nonshipping": {
"rules": [],
"packages": {
"Microsoft.AspNet.Routing.DecisionTree.Sources": { }
}
},
"Default": { // Rules to run for packages not listed in any other set.
"rules": [
"AssemblyHasDocumentFileRule",

View File

@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 14
VisualStudioVersion = 14.0.22115.0
VisualStudioVersion = 14.0.24711.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{0E966C37-7334-4D96-AAF6-9F49FBD166E3}"
EndProject
@ -20,6 +20,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
global.json = global.json
EndProjectSection
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Routing.DecisionTree.Sources", "src\Microsoft.AspNet.Routing.DecisionTree.Sources\Microsoft.AspNet.Routing.DecisionTree.Sources.xproj", "{ABD5AA59-6000-4A3D-A54F-4B636F725AE8}"
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Routing.DecisionTree.Sources.Tests", "test\Microsoft.AspNet.Routing.DecisionTree.Sources.Tests\Microsoft.AspNet.Routing.DecisionTree.Sources.Tests.xproj", "{09C2933C-23AC-41B7-994D-E8A5184A629C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -60,6 +64,30 @@ Global
{DB94E647-C73A-4F52-A126-AA7544CCF33B}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{DB94E647-C73A-4F52-A126-AA7544CCF33B}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{DB94E647-C73A-4F52-A126-AA7544CCF33B}.Release|x86.ActiveCfg = Release|Any CPU
{ABD5AA59-6000-4A3D-A54F-4B636F725AE8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{ABD5AA59-6000-4A3D-A54F-4B636F725AE8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{ABD5AA59-6000-4A3D-A54F-4B636F725AE8}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{ABD5AA59-6000-4A3D-A54F-4B636F725AE8}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{ABD5AA59-6000-4A3D-A54F-4B636F725AE8}.Debug|x86.ActiveCfg = Debug|Any CPU
{ABD5AA59-6000-4A3D-A54F-4B636F725AE8}.Debug|x86.Build.0 = Debug|Any CPU
{ABD5AA59-6000-4A3D-A54F-4B636F725AE8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{ABD5AA59-6000-4A3D-A54F-4B636F725AE8}.Release|Any CPU.Build.0 = Release|Any CPU
{ABD5AA59-6000-4A3D-A54F-4B636F725AE8}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{ABD5AA59-6000-4A3D-A54F-4B636F725AE8}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{ABD5AA59-6000-4A3D-A54F-4B636F725AE8}.Release|x86.ActiveCfg = Release|Any CPU
{ABD5AA59-6000-4A3D-A54F-4B636F725AE8}.Release|x86.Build.0 = Release|Any CPU
{09C2933C-23AC-41B7-994D-E8A5184A629C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{09C2933C-23AC-41B7-994D-E8A5184A629C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{09C2933C-23AC-41B7-994D-E8A5184A629C}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{09C2933C-23AC-41B7-994D-E8A5184A629C}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{09C2933C-23AC-41B7-994D-E8A5184A629C}.Debug|x86.ActiveCfg = Debug|Any CPU
{09C2933C-23AC-41B7-994D-E8A5184A629C}.Debug|x86.Build.0 = Debug|Any CPU
{09C2933C-23AC-41B7-994D-E8A5184A629C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{09C2933C-23AC-41B7-994D-E8A5184A629C}.Release|Any CPU.Build.0 = Release|Any CPU
{09C2933C-23AC-41B7-994D-E8A5184A629C}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{09C2933C-23AC-41B7-994D-E8A5184A629C}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{09C2933C-23AC-41B7-994D-E8A5184A629C}.Release|x86.ActiveCfg = Release|Any CPU
{09C2933C-23AC-41B7-994D-E8A5184A629C}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -68,5 +96,7 @@ Global
{1EE54D32-6CED-4206-ACF5-3DC1DD39D228} = {0E966C37-7334-4D96-AAF6-9F49FBD166E3}
{636D79ED-7B32-487C-BDA5-D2A1AAA97371} = {95359B4B-4C85-4B44-A75B-0621905C4CF6}
{DB94E647-C73A-4F52-A126-AA7544CCF33B} = {C3ADD55B-B9C7-4061-8AD4-6A70D1AE3B2E}
{ABD5AA59-6000-4A3D-A54F-4B636F725AE8} = {0E966C37-7334-4D96-AAF6-9F49FBD166E3}
{09C2933C-23AC-41B7-994D-E8A5184A629C} = {95359B4B-4C85-4B44-A75B-0621905C4CF6}
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,16 @@
// 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;
namespace Microsoft.AspNet.Routing.DecisionTree
{
internal class DecisionCriterion<TItem>
{
public string Key { get; set; }
public Dictionary<object, DecisionTreeNode<TItem>> Branches { get; set; }
public DecisionTreeNode<TItem> Fallback { get; set; }
}
}

View File

@ -0,0 +1,27 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNet.Routing.DecisionTree
{
internal struct DecisionCriterionValue
{
private readonly bool _isCatchAll;
private readonly object _value;
public DecisionCriterionValue(object value, bool isCatchAll)
{
_value = value;
_isCatchAll = isCatchAll;
}
public bool IsCatchAll
{
get { return _isCatchAll; }
}
public object Value
{
get { return _value; }
}
}
}

View File

@ -0,0 +1,34 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
namespace Microsoft.AspNet.Routing.DecisionTree
{
internal class DecisionCriterionValueEqualityComparer : IEqualityComparer<DecisionCriterionValue>
{
public DecisionCriterionValueEqualityComparer(IEqualityComparer<object> innerComparer)
{
InnerComparer = innerComparer;
}
public IEqualityComparer<object> InnerComparer { get; private set; }
public bool Equals(DecisionCriterionValue x, DecisionCriterionValue y)
{
return x.IsCatchAll == y.IsCatchAll || InnerComparer.Equals(x.Value, y.Value);
}
public int GetHashCode(DecisionCriterionValue obj)
{
if (obj.IsCatchAll)
{
return 0;
}
else
{
return InnerComparer.GetHashCode(obj.Value);
}
}
}
}

View File

@ -0,0 +1,234 @@
// 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;
namespace Microsoft.AspNet.Routing.DecisionTree
{
// This code generates a minimal tree of decision criteria that map known categorical data
// (key-value-pairs) to a set of inputs. Action Selection is the best example of how this
// can be used, so the comments here will describe the process from the point-of-view,
// though the decision tree is generally applicable to like-problems.
//
// Care has been taken here to keep the performance of building the data-structure at a
// reasonable level, as this has an impact on startup cost for action selection. Additionally
// we want to hold on to the minimal amount of memory needed once we've built the tree.
//
// Ex:
// Given actions like the following, create a decision tree that will help action
// selection work efficiently.
//
// Given any set of route data it should be possible to traverse the tree using the
// presence our route data keys (like action), and whether or not they match any of
// the known values for that route data key, to find the set of actions that match
// the route data.
//
// Actions:
//
// { controller = "Home", action = "Index" }
// { controller = "Products", action = "Index" }
// { controller = "Products", action = "Buy" }
// { area = "Admin", controller = "Users", action = "AddUser" }
//
// The generated tree looks like this (json-like-notation):
//
// {
// action : {
// "AddUser" : {
// controller : {
// "Users" : {
// area : {
// "Admin" : match { area = "Admin", controller = "Users", action = "AddUser" }
// }
// }
// }
// },
// "Buy" : {
// controller : {
// "Products" : {
// area : {
// null : match { controller = "Products", action = "Buy" }
// }
// }
// }
// },
// "Index" : {
// controller : {
// "Home" : {
// area : {
// null : match { controller = "Home", action = "Index" }
// }
// }
// "Products" : {
// area : {
// "null" : match { controller = "Products", action = "Index" }
// }
// }
// }
// }
// }
// }
internal static class DecisionTreeBuilder<TItem>
{
public static DecisionTreeNode<TItem> GenerateTree(IReadOnlyList<TItem> items, IClassifier<TItem> classifier)
{
var itemDescriptors = new List<ItemDescriptor<TItem>>();
for (var i = 0; i < items.Count; i++)
{
itemDescriptors.Add(new ItemDescriptor<TItem>()
{
Criteria = classifier.GetCriteria(items[i]),
Index = i,
Item = items[i],
});
}
var comparer = new DecisionCriterionValueEqualityComparer(classifier.ValueComparer);
return GenerateNode(
new TreeBuilderContext(),
comparer,
itemDescriptors);
}
private static DecisionTreeNode<TItem> GenerateNode(
TreeBuilderContext context,
DecisionCriterionValueEqualityComparer comparer,
IList<ItemDescriptor<TItem>> items)
{
// The extreme use of generics here is intended to reduce the number of intermediate
// allocations of wrapper classes. Performance testing found that building these trees allocates
// significant memory that we can avoid and that it has a real impact on startup.
var criteria = new Dictionary<string, Criterion>(StringComparer.OrdinalIgnoreCase);
// Matches are items that have no remaining criteria - at this point in the tree
// they are considered accepted.
var matches = new List<TItem>();
// For each item in the working set, we want to map it to it's possible criteria-branch
// pairings, then reduce that tree to the minimal set.
foreach (var item in items)
{
var unsatisfiedCriteria = 0;
foreach (var kvp in item.Criteria)
{
// context.CurrentCriteria is the logical 'stack' of criteria that we've already processed
// on this branch of the tree.
if (context.CurrentCriteria.Contains(kvp.Key))
{
continue;
}
unsatisfiedCriteria++;
Criterion criterion;
if (!criteria.TryGetValue(kvp.Key, out criterion))
{
criterion = new Criterion(comparer);
criteria.Add(kvp.Key, criterion);
}
List<ItemDescriptor<TItem>> branch;
if (!criterion.TryGetValue(kvp.Value, out branch))
{
branch = new List<ItemDescriptor<TItem>>();
criterion.Add(kvp.Value, branch);
}
branch.Add(item);
}
// If all of the criteria on item are satisfied by the 'stack' then this item is a match.
if (unsatisfiedCriteria == 0)
{
matches.Add(item.Item);
}
}
// Iterate criteria in order of branchiness to determine which one to explore next. If a criterion
// has no 'new' matches under it then we can just eliminate that part of the tree.
var reducedCriteria = new List<DecisionCriterion<TItem>>();
foreach (var criterion in criteria.OrderByDescending(c => c.Value.Count))
{
var reducedBranches = new Dictionary<object, DecisionTreeNode<TItem>>(comparer.InnerComparer);
DecisionTreeNode<TItem> fallback = null;
foreach (var branch in criterion.Value)
{
var reducedItems = new List<ItemDescriptor<TItem>>();
foreach (var item in branch.Value)
{
if (context.MatchedItems.Add(item))
{
reducedItems.Add(item);
}
}
if (reducedItems.Count > 0)
{
var childContext = new TreeBuilderContext(context);
childContext.CurrentCriteria.Add(criterion.Key);
var newBranch = GenerateNode(childContext, comparer, branch.Value);
if (branch.Key.IsCatchAll)
{
fallback = newBranch;
}
else
{
reducedBranches.Add(branch.Key.Value, newBranch);
}
}
}
if (reducedBranches.Count > 0 || fallback != null)
{
var newCriterion = new DecisionCriterion<TItem>()
{
Key = criterion.Key,
Branches = reducedBranches,
Fallback = fallback,
};
reducedCriteria.Add(newCriterion);
}
}
return new DecisionTreeNode<TItem>()
{
Criteria = reducedCriteria.ToList(),
Matches = matches,
};
}
private class TreeBuilderContext
{
public TreeBuilderContext()
{
CurrentCriteria = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
MatchedItems = new HashSet<ItemDescriptor<TItem>>();
}
public TreeBuilderContext(TreeBuilderContext other)
{
CurrentCriteria = new HashSet<string>(other.CurrentCriteria, StringComparer.OrdinalIgnoreCase);
MatchedItems = new HashSet<ItemDescriptor<TItem>>();
}
public HashSet<string> CurrentCriteria { get; private set; }
public HashSet<ItemDescriptor<TItem>> MatchedItems { get; private set; }
}
// Subclass just to give a logical name to a mess of generics
private class Criterion : Dictionary<DecisionCriterionValue, List<ItemDescriptor<TItem>>>
{
public Criterion(DecisionCriterionValueEqualityComparer comparer)
: base(comparer)
{
}
}
}
}

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 System.Collections.Generic;
namespace Microsoft.AspNet.Routing.DecisionTree
{
// Data structure representing a node in a decision tree. These are created in DecisionTreeBuilder
// and walked to find a set of items matching some input criteria.
internal class DecisionTreeNode<TItem>
{
// The list of matches for the current node. This represents a set of items that have had all
// of their criteria matched if control gets to this point in the tree.
public IList<TItem> Matches { get; set; }
// Additional criteria that further branch out from this node. Walk these to fine more items
// matching the input data.
public IList<DecisionCriterion<TItem>> Criteria { 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 System.Collections.Generic;
namespace Microsoft.AspNet.Routing.DecisionTree
{
internal interface IClassifier<TItem>
{
IDictionary<string, DecisionCriterionValue> GetCriteria(TItem item);
IEqualityComparer<object> ValueComparer { get; }
}
}

View File

@ -0,0 +1,16 @@
// 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;
namespace Microsoft.AspNet.Routing.DecisionTree
{
internal class ItemDescriptor<TItem>
{
public IDictionary<string, DecisionCriterionValue> Criteria { get; set; }
public int Index { get; set; }
public TItem Item { get; set; }
}
}

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.Props" Condition="'$(VSToolsPath)' != ''" />
<PropertyGroup Label="Globals">
<ProjectGuid>abd5aa59-6000-4a3d-a54f-4b636f725ae8</ProjectGuid>
<RootNamespace>Microsoft.AspNet.Routing.DecisionTree</RootNamespace>
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">..\..\artifacts\obj\$(MSBuildProjectName)</BaseIntermediateOutputPath>
<OutputPath Condition="'$(OutputPath)'=='' ">..\..\artifacts\bin\$(MSBuildProjectName)\</OutputPath>
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>

View File

@ -0,0 +1,23 @@
{
"description": "Components for building a DecisionTree.",
"version": "1.0.0-*",
"repository": {
"type": "git",
"url": "git://github.com/aspnet/routing"
},
"compilationOptions": {
"warningsAsErrors": true,
"keyFile": "../../tools/Key.snk"
},
"shared": "**/*.cs",
"frameworks": {
"net451": { },
"dotnet5.4": {
"dependencies": {
"System.Collections": "4.0.10",
"System.Linq": "4.0.0",
"System.Runtime": "4.0.20"
}
}
}
}

View File

@ -0,0 +1,165 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Microsoft.Extensions.Logging;
namespace Microsoft.AspNet.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;
public TreeRouteBuilder(IRouter target, ILoggerFactory loggerFactory)
{
_target = target;
_generatingEntries = new List<TreeRouteLinkGenerationEntry>();
_matchingEntries = new List<TreeRouteMatchingEntry>();
_logger = loggerFactory.CreateLogger<TreeRouter>();
_constraintLogger = loggerFactory.CreateLogger(typeof(RouteConstraintMatcher).FullName);
}
public void Add(TreeRouteLinkGenerationEntry entry)
{
_generatingEntries.Add(entry);
}
public void Add(TreeRouteMatchingEntry entry)
{
_matchingEntries.Add(entry);
}
public TreeRouter Build(int version)
{
var trees = new Dictionary<int, UrlMatchingTree>();
foreach (var entry in _matchingEntries)
{
UrlMatchingTree tree;
if (!trees.TryGetValue(entry.Order, out tree))
{
tree = new UrlMatchingTree(entry.Order);
trees.Add(entry.Order, tree);
}
AddEntryToTree(tree, entry);
}
return new TreeRouter(
_target,
trees.Values.OrderBy(tree => tree.Order).ToArray(),
_generatingEntries,
_logger,
_constraintLogger,
version);
}
public void Clear()
{
_generatingEntries.Clear();
_matchingEntries.Clear();
}
private void AddEntryToTree(UrlMatchingTree tree, TreeRouteMatchingEntry entry)
{
var current = tree.Root;
for (var i = 0; i < entry.RouteTemplate.Segments.Count; i++)
{
var segment = entry.RouteTemplate.Segments[i];
if (!segment.IsSimple)
{
// Treat complex segments as a constrained parameter
if (current.ConstrainedParameters == null)
{
current.ConstrainedParameters = new UrlMatchingNode(length: i + 1);
}
current = current.ConstrainedParameters;
continue;
}
Debug.Assert(segment.Parts.Count == 1);
var part = segment.Parts[0];
if (part.IsLiteral)
{
UrlMatchingNode next;
if (!current.Literals.TryGetValue(part.Text, out next))
{
next = new UrlMatchingNode(length: i + 1);
current.Literals.Add(part.Text, next);
}
current = next;
continue;
}
if (part.IsParameter && (part.IsOptional || part.IsCatchAll))
{
current.Matches.Add(entry);
}
if (part.IsParameter && part.InlineConstraints.Any() && !part.IsCatchAll)
{
if (current.ConstrainedParameters == null)
{
current.ConstrainedParameters = new UrlMatchingNode(length: i + 1);
}
current = current.ConstrainedParameters;
continue;
}
if (part.IsParameter && !part.IsCatchAll)
{
if (current.Parameters == null)
{
current.Parameters = new UrlMatchingNode(length: i + 1);
}
current = current.Parameters;
continue;
}
if (part.IsParameter && part.InlineConstraints.Any() && part.IsCatchAll)
{
if (current.ConstrainedCatchAlls == null)
{
current.ConstrainedCatchAlls = new UrlMatchingNode(length: i + 1);
}
current = current.ConstrainedCatchAlls;
continue;
}
if (part.IsParameter && part.IsCatchAll)
{
if (current.CatchAlls == null)
{
current.CatchAlls = new UrlMatchingNode(length: i + 1);
}
current = current.CatchAlls;
continue;
}
Debug.Fail("We shouldn't get here.");
}
current.Matches.Add(entry);
current.Matches.Sort((x, y) =>
{
var result = x.Precedence.CompareTo(y.Precedence);
return result == 0 ? x.RouteTemplate.TemplateText.CompareTo(y.RouteTemplate.TemplateText) : result;
});
}
}
}

View File

@ -0,0 +1,60 @@
// 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.AspNet.Routing.Template;
namespace Microsoft.AspNet.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"/>.
/// </summary>
public class TreeRouteLinkGenerationEntry
{
/// <summary>
/// The <see cref="TemplateBinder"/>.
/// </summary>
public TemplateBinder Binder { get; set; }
/// <summary>
/// The route constraints.
/// </summary>
public IReadOnlyDictionary<string, IRouteConstraint> Constraints { get; set; }
/// <summary>
/// The route defaults.
/// </summary>
public IReadOnlyDictionary<string, object> Defaults { get; set; }
/// <summary>
/// The order of the template.
/// </summary>
public int Order { get; set; }
/// <summary>
/// The precedence of the template for link generation. Greater number means higher precedence.
/// </summary>
public decimal GenerationPrecedence { get; set; }
/// <summary>
/// The name of the route.
/// </summary>
public string Name { get; set; }
/// <summary>
/// The route group.
/// </summary>
public string RouteGroup { get; set; }
/// <summary>
/// The set of values that must be present for link genration.
/// </summary>
public IDictionary<string, object> RequiredLinkValues { get; set; }
/// <summary>
/// The <see cref="Template"/>.
/// </summary>
public RouteTemplate Template { get; set; }
}
}

View File

@ -0,0 +1,50 @@
// 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.AspNet.Routing.Template;
namespace Microsoft.AspNet.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 IReadOnlyDictionary<string, IRouteConstraint> Constraints { get; set; }
}
}

View File

@ -0,0 +1,478 @@
// 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;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNet.Routing.Internal;
using Microsoft.AspNet.Routing.Logging;
using Microsoft.Extensions.Internal;
using Microsoft.Extensions.Logging;
namespace Microsoft.AspNet.Routing.Tree
{
/// <summary>
/// An <see cref="IRouter"/> implementation for attribute routing.
/// </summary>
public class TreeRouter : IRouter
{
// Key used by routing and action selection to match an attribute route entry to a
// 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 ILogger _logger;
private readonly ILogger _constraintLogger;
/// <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="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,
ILogger routeLogger,
ILogger constraintLogger,
int version)
{
if (next == null)
{
throw new ArgumentNullException(nameof(next));
}
if (trees == null)
{
throw new ArgumentNullException(nameof(trees));
}
if (linkGenerationEntries == null)
{
throw new ArgumentNullException(nameof(linkGenerationEntries));
}
if (routeLogger == null)
{
throw new ArgumentNullException(nameof(routeLogger));
}
if (constraintLogger == null)
{
throw new ArgumentNullException(nameof(constraintLogger));
}
_next = next;
_trees = trees;
_logger = routeLogger;
_constraintLogger = constraintLogger;
var namedEntries = new Dictionary<string, TreeRouteLinkGenerationEntry>(
StringComparer.OrdinalIgnoreCase);
foreach (var entry in linkGenerationEntries)
{
// Skip unnamed entries
if (entry.Name == null)
{
continue;
}
// We only need to keep one AttributeRouteLinkGenerationEntry per route template
// so in case two entries have the same name and the same template we only keep
// the first entry.
TreeRouteLinkGenerationEntry namedEntry = null;
if (namedEntries.TryGetValue(entry.Name, out namedEntry) &&
!namedEntry.Template.TemplateText.Equals(entry.Template.TemplateText, StringComparison.OrdinalIgnoreCase))
{
throw new ArgumentException(
Resources.FormatAttributeRoute_DifferentLinkGenerationEntries_SameName(entry.Name),
nameof(linkGenerationEntries));
}
else if (namedEntry == null)
{
namedEntries.Add(entry.Name, entry);
}
}
_namedEntries = namedEntries;
// The decision tree will take care of ordering for these entries.
_linkGenerationTree = new LinkGenerationDecisionTree(linkGenerationEntries.ToArray());
Version = version;
}
/// <summary>
/// Gets the version of this route.
/// </summary>
public int Version { get; }
/// <inheritdoc />
public VirtualPathData GetVirtualPath(VirtualPathContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
// If it's a named route we will try to generate a link directly and
// if we can't, we will not try to generate it using an unnamed route.
if (context.RouteName != null)
{
return GetVirtualPathForNamedRoute(context);
}
// The decision tree will give us back all entries that match the provided route data in the correct
// order. We just need to iterate them and use the first one that can generate a link.
var matches = _linkGenerationTree.GetMatches(context);
foreach (var match in matches)
{
var path = GenerateVirtualPath(context, match.Entry);
if (path != null)
{
context.IsBound = true;
return path;
}
}
return null;
}
/// <inheritdoc />
public async Task RouteAsync(RouteContext context)
{
foreach (var tree in _trees)
{
var tokenizer = new PathTokenizer(context.HttpContext.Request.Path);
var enumerator = tokenizer.GetEnumerator();
var root = tree.Root;
var treeEnumerator = new TreeEnumerator(root, tokenizer);
while (treeEnumerator.MoveNext())
{
var node = treeEnumerator.Current;
foreach (var item in node.Matches)
{
var values = item.TemplateMatcher.Match(context.HttpContext.Request.Path);
if (values == null)
{
continue;
}
var match = new TemplateMatch(item, values);
var oldRouteData = context.RouteData;
var newRouteData = new RouteData(oldRouteData);
newRouteData.Routers.Add(match.Entry.Target);
MergeValues(newRouteData.Values, match.Values);
if (!RouteConstraintMatcher.Match(
match.Entry.Constraints,
newRouteData.Values,
context.HttpContext,
this,
RouteDirection.IncomingRequest,
_constraintLogger))
{
return;
}
_logger.MatchedRouteName(match.Entry.RouteName, match.Entry.RouteTemplate.TemplateText);
context.RouteData = newRouteData;
try
{
await match.Entry.Target.RouteAsync(context);
}
finally
{
if (!context.IsHandled)
{
// Restore the original values to prevent polluting the route data.
context.RouteData = oldRouteData;
}
}
if (context.IsHandled)
{
return;
}
}
}
}
}
private struct TreeEnumerator : IEnumerator<UrlMatchingNode>
{
private readonly Stack<UrlMatchingNode> _stack;
private readonly PathTokenizer _tokenizer;
private int _segmentIndex;
public TreeEnumerator(UrlMatchingNode root, PathTokenizer tokenizer)
{
_stack = new Stack<UrlMatchingNode>();
_tokenizer = tokenizer;
Current = null;
_segmentIndex = -1;
_stack.Push(root);
}
public UrlMatchingNode Current { get; private set; }
object IEnumerator.Current => Current;
public void Dispose()
{
}
public bool MoveNext()
{
if (_stack == null)
{
return false;
}
while (_stack.Count > 0)
{
var next = _stack.Pop();
if (++_segmentIndex >= _tokenizer.Count)
{
_segmentIndex--;
if (next.Matches.Count > 0)
{
Current = next;
return true;
}
}
if (_tokenizer.Count == 0)
{
continue;
}
if (next.CatchAlls != null)
{
_stack.Push(next.CatchAlls);
}
if (next.ConstrainedCatchAlls != null)
{
_stack.Push(next.ConstrainedCatchAlls);
}
if (next.Parameters != null)
{
_stack.Push(next.Parameters);
}
if (next.ConstrainedParameters != null)
{
_stack.Push(next.ConstrainedParameters);
}
if (next.Literals.Count > 0)
{
UrlMatchingNode node;
if (next.Literals.TryGetValue(_tokenizer[_segmentIndex].Value, out node))
{
_stack.Push(node);
}
}
}
return false;
}
public void Reset()
{
_stack.Clear();
Current = null;
_segmentIndex = -1;
}
}
private static void MergeValues(
IDictionary<string, object> destination,
IDictionary<string, object> values)
{
foreach (var kvp in values)
{
if (kvp.Value != null)
{
// This will replace the original value for the specified key.
// Values from the matched route will take preference over previous
// data in the route context.
destination[kvp.Key] = kvp.Value;
}
}
}
private struct TemplateMatch : IEquatable<TemplateMatch>
{
public TemplateMatch(TreeRouteMatchingEntry entry, IDictionary<string, object> values)
{
Entry = entry;
Values = values;
}
public TreeRouteMatchingEntry Entry { get; }
public IDictionary<string, object> Values { get; }
public override bool Equals(object obj)
{
if (obj is TemplateMatch)
{
return Equals((TemplateMatch)obj);
}
return false;
}
public bool Equals(TemplateMatch other)
{
return
object.ReferenceEquals(Entry, other.Entry) &&
object.ReferenceEquals(Values, other.Values);
}
public override int GetHashCode()
{
var hash = new HashCodeCombiner();
hash.Add(Entry);
hash.Add(Values);
return hash.CombinedHash;
}
public static bool operator ==(TemplateMatch left, TemplateMatch right)
{
return left.Equals(right);
}
public static bool operator !=(TemplateMatch left, TemplateMatch right)
{
return !left.Equals(right);
}
}
private VirtualPathData GetVirtualPathForNamedRoute(VirtualPathContext context)
{
TreeRouteLinkGenerationEntry entry;
if (_namedEntries.TryGetValue(context.RouteName, out entry))
{
var path = GenerateVirtualPath(context, entry);
if (path != null)
{
context.IsBound = true;
return path;
}
}
return null;
}
private VirtualPathData GenerateVirtualPath(VirtualPathContext context, TreeRouteLinkGenerationEntry entry)
{
// 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
// want to pass these to the link generation code, or else they will end up as query parameters.
//
// So, we need to exclude from here any values that are 'required link values', but aren't
// parameters in the template.
//
// Ex:
// template: api/Products/{action}
// required values: { id = "5", action = "Buy", Controller = "CoolProducts" }
//
// result: { id = "5", action = "Buy" }
var inputValues = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
foreach (var kvp in context.Values)
{
if (entry.RequiredLinkValues.ContainsKey(kvp.Key))
{
var parameter = entry.Template.Parameters
.FirstOrDefault(p => string.Equals(p.Name, kvp.Key, StringComparison.OrdinalIgnoreCase));
if (parameter == null)
{
continue;
}
}
inputValues.Add(kvp.Key, kvp.Value);
}
var bindingResult = entry.Binder.GetValues(context.AmbientValues, inputValues);
if (bindingResult == null)
{
// A required parameter in the template didn't get a value.
return null;
}
var matched = RouteConstraintMatcher.Match(
entry.Constraints,
bindingResult.CombinedValues,
context.Context,
this,
RouteDirection.UrlGeneration,
_constraintLogger);
if (!matched)
{
// A constraint rejected this link.
return null;
}
// These values are used to signal to the next route what we would produce if we round-tripped
// (generate a link and then parse). In MVC the 'next route' is typically the MvcRouteHandler.
var providedValues = new Dictionary<string, object>(
bindingResult.AcceptedValues,
StringComparer.OrdinalIgnoreCase);
providedValues.Add(RouteGroupKey, entry.RouteGroup);
var childContext = new VirtualPathContext(context.Context, context.AmbientValues, context.Values)
{
ProvidedValues = providedValues,
};
var pathData = _next.GetVirtualPath(childContext);
if (pathData != null)
{
// If path is non-null then the target router short-circuited, we don't expect this
// in typical MVC scenarios.
return pathData;
}
else if (!childContext.IsBound)
{
// The target router has rejected these values. We don't expect this in typical MVC scenarios.
return null;
}
var path = entry.Binder.BindValues(bindingResult.AcceptedValues);
if (path == null)
{
return null;
}
return new VirtualPathData(this, path);
}
}
}

View File

@ -0,0 +1,34 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
namespace Microsoft.AspNet.Routing.Tree
{
public class UrlMatchingNode
{
public UrlMatchingNode(int length)
{
Length = length;
Matches = new List<TreeRouteMatchingEntry>();
Literals = new Dictionary<string, UrlMatchingNode>(StringComparer.OrdinalIgnoreCase);
}
public int Length { get; }
// These entries are sorted by precedence then template
public List<TreeRouteMatchingEntry> Matches { get; }
public Dictionary<string, UrlMatchingNode> Literals { get; }
public UrlMatchingNode ConstrainedParameters { get; set; }
public UrlMatchingNode Parameters { get; set; }
public UrlMatchingNode ConstrainedCatchAlls { get; set; }
public UrlMatchingNode CatchAlls { get; set; }
}
}

View File

@ -0,0 +1,17 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNet.Routing.Tree
{
public class UrlMatchingTree
{
public UrlMatchingTree(int order)
{
Order = order;
}
public int Order { get; }
public UrlMatchingNode Root { get; } = new UrlMatchingNode(length: 0);
}
}

View File

@ -0,0 +1,155 @@
// 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 Microsoft.AspNet.Routing.DecisionTree;
using Microsoft.AspNet.Routing.Tree;
namespace Microsoft.AspNet.Routing.Internal
{
// A decision tree that matches link generation entries based on route data.
public class LinkGenerationDecisionTree
{
private readonly DecisionTreeNode<TreeRouteLinkGenerationEntry> _root;
public LinkGenerationDecisionTree(IReadOnlyList<TreeRouteLinkGenerationEntry> entries)
{
_root = DecisionTreeBuilder<TreeRouteLinkGenerationEntry>.GenerateTree(
entries,
new AttributeRouteLinkGenerationEntryClassifier());
}
public IList<LinkGenerationMatch> GetMatches(VirtualPathContext context)
{
var results = new List<LinkGenerationMatch>();
Walk(results, context, _root, isFallbackPath: false);
results.Sort(LinkGenerationMatchComparer.Instance);
return results;
}
// We need to recursively walk the decision tree based on the provided route data
// (context.Values + context.AmbientValues) to find all entries that match. This process is
// virtually identical to action selection.
//
// Each entry has a collection of 'required link values' that must be satisfied. These are
// key-value pairs that make up the decision tree.
//
// A 'require link value' is considered satisfied IF:
// 1. The value in context.Values matches the required value OR
// 2. There is no value in context.Values and the value in context.AmbientValues matches OR
// 3. The required value is 'null' and there is no value in context.Values.
//
// Ex:
// entry requires { area = null, controller = Store, action = Buy }
// context.Values = { controller = Store, action = Buy }
// context.AmbientValues = { area = Help, controller = AboutStore, action = HowToBuyThings }
//
// In this case the entry is a match. The 'controller' and 'action' are both supplied by context.Values,
// and the 'area' is satisfied because there's NOT a value in context.Values. It's OK to ignore ambient
// values in link generation.
//
// If another entry existed like { area = Help, controller = Store, action = Buy }, this would also
// match.
//
// The decision tree uses a tree data structure to execute these rules across all candidates at once.
private void Walk(
List<LinkGenerationMatch> results,
VirtualPathContext context,
DecisionTreeNode<TreeRouteLinkGenerationEntry> 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));
}
for (var i = 0; i < node.Criteria.Count; i++)
{
var criterion = node.Criteria[i];
var key = criterion.Key;
object value;
if (context.Values.TryGetValue(key, out value))
{
DecisionTreeNode<TreeRouteLinkGenerationEntry> branch;
if (criterion.Branches.TryGetValue(value ?? string.Empty, out branch))
{
Walk(results, context, branch, isFallbackPath);
}
}
else
{
// 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;
if (context.AmbientValues.TryGetValue(key, out value) &&
!criterion.Branches.Comparer.Equals(value, string.Empty))
{
if (criterion.Branches.TryGetValue(value, out branch))
{
Walk(results, context, branch, isFallbackPath);
}
}
if (criterion.Branches.TryGetValue(string.Empty, out branch))
{
Walk(results, context, branch, isFallbackPath: true);
}
}
}
}
private class AttributeRouteLinkGenerationEntryClassifier : IClassifier<TreeRouteLinkGenerationEntry>
{
public AttributeRouteLinkGenerationEntryClassifier()
{
ValueComparer = new RouteValueEqualityComparer();
}
public IEqualityComparer<object> ValueComparer { get; private set; }
public IDictionary<string, DecisionCriterionValue> GetCriteria(TreeRouteLinkGenerationEntry item)
{
var results = new Dictionary<string, DecisionCriterionValue>(StringComparer.OrdinalIgnoreCase);
foreach (var kvp in item.RequiredLinkValues)
{
results.Add(kvp.Key, new DecisionCriterionValue(kvp.Value ?? string.Empty, isCatchAll: false));
}
return results;
}
}
private class LinkGenerationMatchComparer : IComparer<LinkGenerationMatch>
{
public static readonly LinkGenerationMatchComparer Instance = new LinkGenerationMatchComparer();
public int Compare(LinkGenerationMatch x, LinkGenerationMatch y)
{
// For this comparison lower is better.
if (x.Entry.Order != y.Entry.Order)
{
return x.Entry.Order.CompareTo(y.Entry.Order);
}
if (x.Entry.GenerationPrecedence != y.Entry.GenerationPrecedence)
{
// Reversed because higher is better
return y.Entry.GenerationPrecedence.CompareTo(x.Entry.GenerationPrecedence);
}
if (x.IsFallbackMatch != y.IsFallbackMatch)
{
// A fallback match is worse than a non-fallback
return x.IsFallbackMatch.CompareTo(y.IsFallbackMatch);
}
return StringComparer.Ordinal.Compare(x.Entry.Template.TemplateText, y.Entry.Template.TemplateText);
}
}
}
}

View File

@ -0,0 +1,23 @@
// 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.AspNet.Routing.Tree;
namespace Microsoft.AspNet.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,29 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.Extensions.Logging;
namespace Microsoft.AspNet.Routing.Logging
{
internal static class TreeRouterLoggerExtensions
{
private static readonly Action<ILogger, string, string, Exception> _matchedRouteName;
static TreeRouterLoggerExtensions()
{
_matchedRouteName = LoggerMessage.Define<string, string>(
LogLevel.Verbose,
1,
"Request successfully matched the route with name '{RouteName}' and template '{RouteTemplate}'.");
}
public static void MatchedRouteName(
this ILogger logger,
string routeName,
string routeTemplate)
{
_matchedRouteName(logger, routeName, routeTemplate, null);
}
}
}

View File

@ -410,6 +410,22 @@ namespace Microsoft.AspNet.Routing
return string.Format(CultureInfo.CurrentCulture, GetString("TemplateRoute_OptionalParameterHasTobeTheLast"), p0, p1, p2);
}
/// <summary>
/// Two or more routes named '{0}' have different templates.
/// </summary>
internal static string AttributeRoute_DifferentLinkGenerationEntries_SameName
{
get { return GetString("AttributeRoute_DifferentLinkGenerationEntries_SameName"); }
}
/// <summary>
/// Two or more routes named '{0}' have different templates.
/// </summary>
internal static string FormatAttributeRoute_DifferentLinkGenerationEntries_SameName(object p0)
{
return string.Format(CultureInfo.CurrentCulture, GetString("AttributeRoute_DifferentLinkGenerationEntries_SameName"), p0);
}
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);

View File

@ -192,4 +192,7 @@
<data name="TemplateRoute_OptionalParameterHasTobeTheLast" xml:space="preserve">
<value>An optional parameter must be at the end of the segment. In the segment '{0}', optional parameter '{1}' is followed by '{2}'.</value>
</data>
<data name="AttributeRoute_DifferentLinkGenerationEntries_SameName" xml:space="preserve">
<value>Two or more routes named '{0}' have different templates.</value>
</data>
</root>

View File

@ -0,0 +1,53 @@
// 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.Globalization;
namespace Microsoft.AspNet.Routing
{
/// <summary>
/// An <see cref="IEqualityComparer{object}"/> implementation that compares objects as-if
/// they were route value strings.
/// </summary>
/// <remarks>
/// Values that are are not strings are converted to strings using
/// <c>Convert.ToString(x, CultureInfo.InvariantCulture)</c>. <c>null</c> values are converted
/// to the empty string.
///
/// strings are compared using <see cref="StringComparison.OrdinalIgnoreCase"/>.
/// </remarks>
public class RouteValueEqualityComparer : IEqualityComparer<object>
{
/// <inheritdoc />
public new bool Equals(object x, object y)
{
var stringX = x as string ?? Convert.ToString(x, CultureInfo.InvariantCulture);
var stringY = y as string ?? Convert.ToString(y, CultureInfo.InvariantCulture);
if (string.IsNullOrEmpty(stringX) && string.IsNullOrEmpty(stringY))
{
return true;
}
else
{
return string.Equals(stringX, stringY, StringComparison.OrdinalIgnoreCase);
}
}
/// <inheritdoc />
public int GetHashCode(object obj)
{
var stringObj = obj as string ?? Convert.ToString(obj, CultureInfo.InvariantCulture);
if (string.IsNullOrEmpty(stringObj))
{
return StringComparer.OrdinalIgnoreCase.GetHashCode(string.Empty);
}
else
{
return StringComparer.OrdinalIgnoreCase.GetHashCode(stringObj);
}
}
}
}

View File

@ -0,0 +1,131 @@
// 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.Diagnostics;
using System.Linq;
namespace Microsoft.AspNet.Routing.Template
{
/// <summary>
/// Computes precedence for an attribute route template.
/// </summary>
public static class RoutePrecedence
{
// Compute the precedence for matching a provided url
// e.g.: /api/template == 1.1
// /api/template/{id} == 1.13
// /api/{id:int} == 1.2
// /api/template/{id:int} == 1.12
public static decimal ComputeMatched(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).
var precedence = 0m;
for (var i = 0; i < template.Segments.Count; i++)
{
var segment = template.Segments[i];
var digit = ComputeMatchDigit(segment);
Debug.Assert(digit >= 0 && digit < 10);
precedence += decimal.Divide(digit, (decimal)Math.Pow(10, i));
}
return precedence;
}
// Compute the precedence for generating a url
// e.g.: /api/template == 5.5
// /api/template/{id} == 5.53
// /api/{id:int} == 5.4
// /api/template/{id:int} == 5.54
public static decimal ComputeGenerated(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).
var precedence = 0m;
for (var i = 0; i < template.Segments.Count; i++)
{
var segment = template.Segments[i];
var digit = ComputeGenerationDigit(segment);
Debug.Assert(digit >= 0 && digit < 10);
precedence += decimal.Divide(digit, (decimal)Math.Pow(10, i));
}
return precedence;
}
// Segments have the following order:
// 5 - Literal segments
// 4 - Multi-part segments && Constrained parameter segments
// 3 - Unconstrained parameter segements
// 2 - Constrained wildcard parameter segments
// 1 - Unconstrained wildcard parameter segments
private static int ComputeGenerationDigit(TemplateSegment segment)
{
if(segment.Parts.Count > 1)
{
return 4;
}
var part = segment.Parts[0];
if(part.IsLiteral)
{
return 5;
}
else
{
Debug.Assert(part.IsParameter);
var digit = part.IsCatchAll ? 1 : 3;
if (part.InlineConstraints != null && part.InlineConstraints.Any())
{
digit++;
}
return digit;
}
}
// Segments have the following order:
// 1 - Literal segments
// 2 - Constrained parameter segments / Multi-part segments
// 3 - Unconstrained parameter segments
// 4 - Constrained wildcard parameter segments
// 5 - Unconstrained wildcard parameter segments
private static int ComputeMatchDigit(TemplateSegment segment)
{
if (segment.Parts.Count > 1)
{
// Multi-part segments should appear after literal segments and along with parameter segments
return 2;
}
var part = segment.Parts[0];
// Literal segments always go first
if (part.IsLiteral)
{
return 1;
}
else
{
Debug.Assert(part.IsParameter);
var digit = part.IsCatchAll ? 5 : 3;
// If there is a route constraint for the parameter, reduce order by 1
// Constrained parameters end up with order 2, Constrained catch alls end up with order 4
if (part.InlineConstraints != null && part.InlineConstraints.Any())
{
digit--;
}
return digit;
}
}
}
}

View File

@ -20,7 +20,7 @@ namespace Microsoft.AspNet.Routing.Template
throw new ArgumentNullException(nameof(segments));
}
Template = template;
TemplateText = template;
Segments = segments;
@ -39,7 +39,7 @@ namespace Microsoft.AspNet.Routing.Template
}
}
public string Template { get; }
public string TemplateText { get; }
public IList<TemplatePart> Parameters { get; }

View File

@ -9,11 +9,19 @@
"warningsAsErrors": true,
"keyFile": "../../tools/Key.snk"
},
"dependencies": {
"Microsoft.AspNet.Http.Extensions": "1.0.0-*",
"Microsoft.Extensions.Logging.Abstractions": "1.0.0-*",
"Microsoft.Extensions.OptionsModel": "1.0.0-*"
},
"dependencies": {
"Microsoft.AspNet.Http.Extensions": "1.0.0-*",
"Microsoft.AspNet.Routing.DecisionTree.Sources": {
"type": "build",
"version": "1.0.0-*"
},
"Microsoft.Extensions.HashCodeCombiner.Sources": {
"type": "build",
"version": "1.0.0-*"
},
"Microsoft.Extensions.Logging.Abstractions": "1.0.0-*",
"Microsoft.Extensions.OptionsModel": "1.0.0-*"
},
"frameworks": {
"net451": {},
"dotnet5.4": {

View File

@ -0,0 +1,285 @@
// 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 Xunit;
namespace Microsoft.AspNet.Routing.DecisionTree
{
public class DecisionTreeBuilderTest
{
[Fact]
public void BuildTree_Empty()
{
// Arrange
var items = new List<Item>();
// Act
var tree = DecisionTreeBuilder<Item>.GenerateTree(items, new ItemClassifier());
// Assert
Assert.Empty(tree.Criteria);
Assert.Empty(tree.Matches);
}
[Fact]
public void BuildTree_TrivialMatch()
{
// Arrange
var items = new List<Item>();
var item = new Item();
items.Add(item);
// Act
var tree = DecisionTreeBuilder<Item>.GenerateTree(items, new ItemClassifier());
// Assert
Assert.Empty(tree.Criteria);
Assert.Same(item, Assert.Single(tree.Matches));
}
[Fact]
public void BuildTree_WithMultipleCriteria()
{
// Arrange
var items = new List<Item>();
var item = new Item();
item.Criteria.Add("area", new DecisionCriterionValue(value: "Admin", isCatchAll: false));
item.Criteria.Add("controller", new DecisionCriterionValue(value: "Users", isCatchAll: false));
item.Criteria.Add("action", new DecisionCriterionValue(value: "AddUser", isCatchAll: false));
items.Add(item);
// Act
var tree = DecisionTreeBuilder<Item>.GenerateTree(items, new ItemClassifier());
// Assert
Assert.Empty(tree.Matches);
var area = Assert.Single(tree.Criteria);
Assert.Equal("area", area.Key);
Assert.Null(area.Fallback);
var admin = Assert.Single(area.Branches);
Assert.Equal("Admin", admin.Key);
Assert.Empty(admin.Value.Matches);
var controller = Assert.Single(admin.Value.Criteria);
Assert.Equal("controller", controller.Key);
Assert.Null(controller.Fallback);
var users = Assert.Single(controller.Branches);
Assert.Equal("Users", users.Key);
Assert.Empty(users.Value.Matches);
var action = Assert.Single(users.Value.Criteria);
Assert.Equal("action", action.Key);
Assert.Null(action.Fallback);
var addUser = Assert.Single(action.Branches);
Assert.Equal("AddUser", addUser.Key);
Assert.Empty(addUser.Value.Criteria);
Assert.Same(item, Assert.Single(addUser.Value.Matches));
}
[Fact]
public void BuildTree_WithMultipleItems()
{
// Arrange
var items = new List<Item>();
var item1 = new Item();
item1.Criteria.Add("controller", new DecisionCriterionValue(value: "Store", isCatchAll: false));
item1.Criteria.Add("action", new DecisionCriterionValue(value: "Buy", isCatchAll: false));
items.Add(item1);
var item2 = new Item();
item2.Criteria.Add("controller", new DecisionCriterionValue(value: "Store", isCatchAll: false));
item2.Criteria.Add("action", new DecisionCriterionValue(value: "Checkout", isCatchAll: false));
items.Add(item2);
// Act
var tree = DecisionTreeBuilder<Item>.GenerateTree(items, new ItemClassifier());
// Assert
Assert.Empty(tree.Matches);
var action = Assert.Single(tree.Criteria);
Assert.Equal("action", action.Key);
Assert.Null(action.Fallback);
var buy = action.Branches["Buy"];
Assert.Empty(buy.Matches);
var controller = Assert.Single(buy.Criteria);
Assert.Equal("controller", controller.Key);
Assert.Null(controller.Fallback);
var store = Assert.Single(controller.Branches);
Assert.Equal("Store", store.Key);
Assert.Empty(store.Value.Criteria);
Assert.Same(item1, Assert.Single(store.Value.Matches));
var checkout = action.Branches["Checkout"];
Assert.Empty(checkout.Matches);
controller = Assert.Single(checkout.Criteria);
Assert.Equal("controller", controller.Key);
Assert.Null(controller.Fallback);
store = Assert.Single(controller.Branches);
Assert.Equal("Store", store.Key);
Assert.Empty(store.Value.Criteria);
Assert.Same(item2, Assert.Single(store.Value.Matches));
}
[Fact]
public void BuildTree_WithInteriorMatch()
{
// Arrange
var items = new List<Item>();
var item1 = new Item();
item1.Criteria.Add("controller", new DecisionCriterionValue(value: "Store", isCatchAll: false));
item1.Criteria.Add("action", new DecisionCriterionValue(value: "Buy", isCatchAll: false));
items.Add(item1);
var item2 = new Item();
item2.Criteria.Add("controller", new DecisionCriterionValue(value: "Store", isCatchAll: false));
item2.Criteria.Add("action", new DecisionCriterionValue(value: "Checkout", isCatchAll: false));
items.Add(item2);
var item3 = new Item();
item3.Criteria.Add("action", new DecisionCriterionValue(value: "Buy", isCatchAll: false));
items.Add(item3);
// Act
var tree = DecisionTreeBuilder<Item>.GenerateTree(items, new ItemClassifier());
// Assert
Assert.Empty(tree.Matches);
var action = Assert.Single(tree.Criteria);
Assert.Equal("action", action.Key);
Assert.Null(action.Fallback);
var buy = action.Branches["Buy"];
Assert.Same(item3, Assert.Single(buy.Matches));
}
[Fact]
public void BuildTree_WithCatchAll()
{
// Arrange
var items = new List<Item>();
var item1 = new Item();
item1.Criteria.Add("country", new DecisionCriterionValue(value: "CA", isCatchAll: false));
item1.Criteria.Add("controller", new DecisionCriterionValue(value: "Store", isCatchAll: false));
item1.Criteria.Add("action", new DecisionCriterionValue(value: "Checkout", isCatchAll: false));
items.Add(item1);
var item2 = new Item();
item2.Criteria.Add("country", new DecisionCriterionValue(value: "US", isCatchAll: false));
item2.Criteria.Add("controller", new DecisionCriterionValue(value: "Store", isCatchAll: false));
item2.Criteria.Add("action", new DecisionCriterionValue(value: "Checkout", isCatchAll: false));
items.Add(item2);
var item3 = new Item();
item3.Criteria.Add("country", new DecisionCriterionValue(value: null, isCatchAll: true));
item3.Criteria.Add("controller", new DecisionCriterionValue(value: "Store", isCatchAll: false));
item3.Criteria.Add("action", new DecisionCriterionValue(value: "Checkout", isCatchAll: false));
items.Add(item3);
// Act
var tree = DecisionTreeBuilder<Item>.GenerateTree(items, new ItemClassifier());
// Assert
Assert.Empty(tree.Matches);
var country = Assert.Single(tree.Criteria);
Assert.Equal("country", country.Key);
var fallback = country.Fallback;
Assert.NotNull(fallback);
var controller = Assert.Single(fallback.Criteria);
Assert.Equal("controller", controller.Key);
Assert.Null(controller.Fallback);
var store = Assert.Single(controller.Branches);
Assert.Equal("Store", store.Key);
Assert.Empty(store.Value.Matches);
var action = Assert.Single(store.Value.Criteria);
Assert.Equal("action", action.Key);
Assert.Null(action.Fallback);
var checkout = Assert.Single(action.Branches);
Assert.Equal("Checkout", checkout.Key);
Assert.Empty(checkout.Value.Criteria);
Assert.Same(item3, Assert.Single(checkout.Value.Matches));
}
[Fact]
public void BuildTree_WithDivergentCriteria()
{
// Arrange
var items = new List<Item>();
var item1 = new Item();
item1.Criteria.Add("controller", new DecisionCriterionValue(value: "Store", isCatchAll: false));
item1.Criteria.Add("action", new DecisionCriterionValue(value: "Buy", isCatchAll: false));
items.Add(item1);
var item2 = new Item();
item2.Criteria.Add("controller", new DecisionCriterionValue(value: "Store", isCatchAll: false));
item2.Criteria.Add("action", new DecisionCriterionValue(value: "Checkout", isCatchAll: false));
items.Add(item2);
var item3 = new Item();
item3.Criteria.Add("stub", new DecisionCriterionValue(value: "Bleh", isCatchAll: false));
items.Add(item3);
// Act
var tree = DecisionTreeBuilder<Item>.GenerateTree(items, new ItemClassifier());
// Assert
Assert.Empty(tree.Matches);
var action = tree.Criteria[0];
Assert.Equal("action", action.Key);
var stub = tree.Criteria[1];
Assert.Equal("stub", stub.Key);
}
private class Item
{
public Item()
{
Criteria = new Dictionary<string, DecisionCriterionValue>(StringComparer.OrdinalIgnoreCase);
}
public Dictionary<string, DecisionCriterionValue> Criteria { get; private set; }
}
private class ItemClassifier : IClassifier<Item>
{
public IEqualityComparer<object> ValueComparer
{
get
{
return new RouteValueEqualityComparer();
}
}
public IDictionary<string, DecisionCriterionValue> GetCriteria(Item item)
{
return item.Criteria;
}
}
}
}

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.Props" Condition="'$(VSToolsPath)' != ''" />
<PropertyGroup Label="Globals">
<ProjectGuid>09c2933c-23ac-41b7-994d-e8a5184a629c</ProjectGuid>
<RootNamespace>Microsoft.AspNet.Routing.DecisionTree.Tests</RootNamespace>
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">..\..\artifacts\obj\$(MSBuildProjectName)</BaseIntermediateOutputPath>
<OutputPath Condition="'$(OutputPath)'=='' ">..\..\artifacts\bin\$(MSBuildProjectName)\</OutputPath>
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
</PropertyGroup>
<ItemGroup>
<Service Include="{82a7f48d-3b50-4b1e-b82e-3ada8210c358}" />
</ItemGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>

View File

@ -0,0 +1,21 @@
{
"compilationOptions": {
"warningsAsErrors": true
},
"dependencies": {
"Microsoft.AspNet.Routing": "1.0.0-*",
"Microsoft.AspNet.Routing.DecisionTree.Sources": {
"type": "build",
"version": "1.0.0-*"
},
"Microsoft.AspNet.Testing": "1.0.0-*",
"xunit.runner.aspnet": "2.0.0-aspnet-*"
},
"frameworks": {
"dnxcore50": { },
"dnx451": { }
},
"commands": {
"test": "xunit.runner.aspnet"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,338 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNet.Http.Internal;
using Microsoft.AspNet.Routing.Template;
using Microsoft.AspNet.Routing.Tree;
using Xunit;
namespace Microsoft.AspNet.Routing.Internal.Routing
{
public class LinkGenerationDecisionTreeTest
{
[Fact]
public void SelectSingleEntry_NoCriteria()
{
// Arrange
var entries = new List<TreeRouteLinkGenerationEntry>();
var entry = CreateEntry(new { });
entries.Add(entry);
var tree = new LinkGenerationDecisionTree(entries);
var context = CreateContext(new { });
// Act
var matches = tree.GetMatches(context);
// Assert
Assert.Same(entry, Assert.Single(matches).Entry);
}
[Fact]
public void SelectSingleEntry_MultipleCriteria()
{
// Arrange
var entries = new List<TreeRouteLinkGenerationEntry>();
var entry = CreateEntry(new { controller = "Store", action = "Buy" });
entries.Add(entry);
var tree = new LinkGenerationDecisionTree(entries);
var context = CreateContext(new { controller = "Store", action = "Buy" });
// Act
var matches = tree.GetMatches(context);
// Assert
Assert.Same(entry, Assert.Single(matches).Entry);
}
[Fact]
public void SelectSingleEntry_MultipleCriteria_AmbientValues()
{
// Arrange
var entries = new List<TreeRouteLinkGenerationEntry>();
var entry = CreateEntry(new { controller = "Store", action = "Buy" });
entries.Add(entry);
var tree = new LinkGenerationDecisionTree(entries);
var context = CreateContext(values: null, ambientValues: new { controller = "Store", action = "Buy" });
// Act
var matches = tree.GetMatches(context);
// Assert
var match = Assert.Single(matches);
Assert.Same(entry, match.Entry);
Assert.False(match.IsFallbackMatch);
}
[Fact]
public void SelectSingleEntry_MultipleCriteria_Replaced()
{
// Arrange
var entries = new List<TreeRouteLinkGenerationEntry>();
var entry = CreateEntry(new { controller = "Store", action = "Buy" });
entries.Add(entry);
var tree = new LinkGenerationDecisionTree(entries);
var context = CreateContext(
values: new { action = "Buy" },
ambientValues: new { controller = "Store", action = "Cart" });
// Act
var matches = tree.GetMatches(context);
// Assert
var match = Assert.Single(matches);
Assert.Same(entry, match.Entry);
Assert.False(match.IsFallbackMatch);
}
[Fact]
public void SelectSingleEntry_MultipleCriteria_AmbientValue_Ignored()
{
// Arrange
var entries = new List<TreeRouteLinkGenerationEntry>();
var entry = CreateEntry(new { controller = "Store", action = (string)null });
entries.Add(entry);
var tree = new LinkGenerationDecisionTree(entries);
var context = CreateContext(
values: new { controller = "Store" },
ambientValues: new { controller = "Store", action = "Buy" });
// Act
var matches = tree.GetMatches(context);
// Assert
var match = Assert.Single(matches);
Assert.Same(entry, match.Entry);
Assert.True(match.IsFallbackMatch);
}
[Fact]
public void SelectSingleEntry_MultipleCriteria_NoMatch()
{
// Arrange
var entries = new List<TreeRouteLinkGenerationEntry>();
var entry = CreateEntry(new { controller = "Store", action = "Buy" });
entries.Add(entry);
var tree = new LinkGenerationDecisionTree(entries);
var context = CreateContext(new { controller = "Store", action = "AddToCart" });
// Act
var matches = tree.GetMatches(context);
// Assert
Assert.Empty(matches);
}
[Fact]
public void SelectSingleEntry_MultipleCriteria_AmbientValue_NoMatch()
{
// Arrange
var entries = new List<TreeRouteLinkGenerationEntry>();
var entry = CreateEntry(new { controller = "Store", action = "Buy" });
entries.Add(entry);
var tree = new LinkGenerationDecisionTree(entries);
var context = CreateContext(
values: new { controller = "Store" },
ambientValues: new { controller = "Store", action = "Cart" });
// Act
var matches = tree.GetMatches(context);
// Assert
Assert.Empty(matches);
}
[Fact]
public void SelectMultipleEntries_OneDoesntMatch()
{
// Arrange
var entries = new List<TreeRouteLinkGenerationEntry>();
var entry1 = CreateEntry(new { controller = "Store", action = "Buy" });
entries.Add(entry1);
var entry2 = CreateEntry(new { controller = "Store", action = "Cart" });
entries.Add(entry2);
var tree = new LinkGenerationDecisionTree(entries);
var context = CreateContext(
values: new { controller = "Store" },
ambientValues: new { controller = "Store", action = "Buy" });
// Act
var matches = tree.GetMatches(context);
// Assert
Assert.Same(entry1, Assert.Single(matches).Entry);
}
[Fact]
public void SelectMultipleEntries_BothMatch_CriteriaSubset()
{
// Arrange
var entries = new List<TreeRouteLinkGenerationEntry>();
var entry1 = CreateEntry(new { controller = "Store", action = "Buy" });
entries.Add(entry1);
var entry2 = CreateEntry(new { controller = "Store" });
entry2.Order = 1;
entries.Add(entry2);
var tree = new LinkGenerationDecisionTree(entries);
var context = CreateContext(
values: new { controller = "Store" },
ambientValues: new { controller = "Store", action = "Buy" });
// Act
var matches = tree.GetMatches(context).Select(m => m.Entry).ToList();
// Assert
Assert.Equal(entries, matches);
}
[Fact]
public void SelectMultipleEntries_BothMatch_NonOverlappingCriteria()
{
// Arrange
var entries = new List<TreeRouteLinkGenerationEntry>();
var entry1 = CreateEntry(new { controller = "Store", action = "Buy" });
entries.Add(entry1);
var entry2 = CreateEntry(new { slug = "1234" });
entry2.Order = 1;
entries.Add(entry2);
var tree = new LinkGenerationDecisionTree(entries);
var context = CreateContext(new { controller = "Store", action = "Buy", slug = "1234" });
// Act
var matches = tree.GetMatches(context).Select(m => m.Entry).ToList();
// Assert
Assert.Equal(entries, matches);
}
// Precedence is ignored for sorting because they have different order
[Fact]
public void SelectMultipleEntries_BothMatch_OrderedByOrder()
{
// Arrange
var entries = new List<TreeRouteLinkGenerationEntry>();
var entry1 = CreateEntry(new { controller = "Store", action = "Buy" });
entry1.GenerationPrecedence = 0;
entries.Add(entry1);
var entry2 = CreateEntry(new { controller = "Store", action = "Buy" });
entry2.Order = 1;
entry2.GenerationPrecedence = 1;
entries.Add(entry2);
var tree = new LinkGenerationDecisionTree(entries);
var context = CreateContext(new { controller = "Store", action = "Buy" });
// Act
var matches = tree.GetMatches(context).Select(m => m.Entry).ToList();
// Assert
Assert.Equal(entries, matches);
}
// Precedence is used for sorting because they have the same order
[Fact]
public void SelectMultipleEntries_BothMatch_OrderedByPrecedence()
{
// Arrange
var entries = new List<TreeRouteLinkGenerationEntry>();
var entry1 = CreateEntry(new { controller = "Store", action = "Buy" });
entry1.GenerationPrecedence = 1;
entries.Add(entry1);
var entry2 = CreateEntry(new { controller = "Store", action = "Buy" });
entry2.GenerationPrecedence = 0;
entries.Add(entry2);
var tree = new LinkGenerationDecisionTree(entries);
var context = CreateContext(new { controller = "Store", action = "Buy" });
// Act
var matches = tree.GetMatches(context).Select(m => m.Entry).ToList();
// Assert
Assert.Equal(entries, matches);
}
// Template is used for sorting because they have the same order
[Fact]
public void SelectMultipleEntries_BothMatch_OrderedByTemplate()
{
// Arrange
var entries = new List<TreeRouteLinkGenerationEntry>();
var entry1 = CreateEntry(new { controller = "Store", action = "Buy" });
entry1.Template = TemplateParser.Parse("a");
entries.Add(entry1);
var entry2 = CreateEntry(new { controller = "Store", action = "Buy" });
entry2.Template = TemplateParser.Parse("b");
entries.Add(entry2);
var tree = new LinkGenerationDecisionTree(entries);
var context = CreateContext(new { controller = "Store", action = "Buy" });
// Act
var matches = tree.GetMatches(context).Select(m => m.Entry).ToList();
// Assert
Assert.Equal(entries, matches);
}
private TreeRouteLinkGenerationEntry CreateEntry(object requiredValues)
{
var entry = new TreeRouteLinkGenerationEntry();
entry.RequiredLinkValues = new RouteValueDictionary(requiredValues);
return entry;
}
private VirtualPathContext CreateContext(object values, object ambientValues = null)
{
var context = new VirtualPathContext(
new DefaultHttpContext(),
new RouteValueDictionary(ambientValues),
new RouteValueDictionary(values));
return context;
}
}
}

View File

@ -0,0 +1,124 @@
// 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.
#if DNX451
using System;
using Microsoft.AspNet.Routing;
using Microsoft.Extensions.OptionsModel;
using Moq;
using Xunit;
namespace Microsoft.AspNet.Routing.Template
{
public class RoutePrecedenceTests
{
[Theory]
[InlineData("Employees/{id}", "Employees/{employeeId}")]
[InlineData("abc", "def")]
[InlineData("{x:alpha}", "{x:int}")]
public void ComputeMatched_IsEqual(string xTemplate, string yTemplate)
{
// Arrange & Act
var xPrededence = ComputeMatched(xTemplate);
var yPrededence = ComputeMatched(yTemplate);
// Assert
Assert.Equal(xPrededence, yPrededence);
}
[Theory]
[InlineData("Employees/{id}", "Employees/{employeeId}")]
[InlineData("abc", "def")]
[InlineData("{x:alpha}", "{x:int}")]
public void ComputeGenerated_IsEqual(string xTemplate, string yTemplate)
{
// Arrange & Act
var xPrededence = ComputeGenerated(xTemplate);
var yPrededence = ComputeGenerated(yTemplate);
// Assert
Assert.Equal(xPrededence, yPrededence);
}
[Theory]
[InlineData("abc", "a{x}")]
[InlineData("abc", "{x}c")]
[InlineData("abc", "{x:int}")]
[InlineData("abc", "{x}")]
[InlineData("abc", "{*x}")]
[InlineData("{x:int}", "{x}")]
[InlineData("{x:int}", "{*x}")]
[InlineData("a{x}", "{x}")]
[InlineData("{x}c", "{x}")]
[InlineData("a{x}", "{*x}")]
[InlineData("{x}c", "{*x}")]
[InlineData("{x}", "{*x}")]
[InlineData("{*x:maxlength(10)}", "{*x}")]
[InlineData("abc/def", "abc/{x:int}")]
[InlineData("abc/def", "abc/{x}")]
[InlineData("abc/def", "abc/{*x}")]
[InlineData("abc/{x:int}", "abc/{x}")]
[InlineData("abc/{x:int}", "abc/{*x}")]
[InlineData("abc/{x}", "abc/{*x}")]
[InlineData("{x}/{y:int}", "{x}/{y}")]
public void ComputeMatched_IsLessThan(string xTemplate, string yTemplate)
{
// Arrange & Act
var xPrededence = ComputeMatched(xTemplate);
var yPrededence = ComputeMatched(yTemplate);
// Assert
Assert.True(xPrededence < yPrededence);
}
[Theory]
[InlineData("abc", "a{x}")]
[InlineData("abc", "{x}c")]
[InlineData("abc", "{x:int}")]
[InlineData("abc", "{x}")]
[InlineData("abc", "{*x}")]
[InlineData("{x:int}", "{x}")]
[InlineData("{x:int}", "{*x}")]
[InlineData("a{x}", "{x}")]
[InlineData("{x}c", "{x}")]
[InlineData("a{x}", "{*x}")]
[InlineData("{x}c", "{*x}")]
[InlineData("{x}", "{*x}")]
[InlineData("{*x:maxlength(10)}", "{*x}")]
[InlineData("abc/def", "abc/{x:int}")]
[InlineData("abc/def", "abc/{x}")]
[InlineData("abc/def", "abc/{*x}")]
[InlineData("abc/{x:int}", "abc/{x}")]
[InlineData("abc/{x:int}", "abc/{*x}")]
[InlineData("abc/{x}", "abc/{*x}")]
[InlineData("{x}/{y:int}", "{x}/{y}")]
public void ComputeGenerated_IsGreaterThan(string xTemplate, string yTemplate)
{
// Arrange & Act
var xPrecedence = ComputeGenerated(xTemplate);
var yPrecedence = ComputeGenerated(yTemplate);
// Assert
Assert.True(xPrecedence > yPrecedence);
}
private static decimal ComputeMatched(string template)
{
return Compute(template, RoutePrecedence.ComputeMatched);
}
private static decimal ComputeGenerated(string template)
{
return Compute(template, RoutePrecedence.ComputeGenerated);
}
private static decimal Compute(string template, Func<RouteTemplate, decimal> func)
{
var options = new Mock<IOptions<RouteOptions>>();
options.SetupGet(o => o.Value).Returns(new RouteOptions());
var parsed = TemplateParser.Parse(template);
return func(parsed);
}
}
}
#endif

View File

@ -830,7 +830,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
}
else
{
if (!string.Equals(x.Template, y.Template, StringComparison.Ordinal))
if (!string.Equals(x.TemplateText, y.TemplateText, StringComparison.Ordinal))
{
return false;
}