From 200d48879cced34f5c587bd052ef6cfb5e65a92a Mon Sep 17 00:00:00 2001 From: "ASP.NET CI" Date: Fri, 4 May 2018 07:49:01 -0700 Subject: [PATCH 01/20] Update dependencies.props [auto-updated: dependencies] --- build/dependencies.props | 46 ++++++++++++++++++++-------------------- korebuild-lock.txt | 4 ++-- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index adf9e37814..703213658f 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -4,32 +4,32 @@ 0.10.13 - 2.1.0-rc1-15774 - 2.1.0-rc1-30613 - 2.1.0-rc1-30613 - 2.1.0-rc1-30613 - 2.1.0-rc1-30613 - 2.1.0-rc1-30613 - 2.1.0-rc1-30613 - 2.1.0-rc1-30613 - 2.1.0-rc1-30613 - 2.1.0-rc1-30613 - 2.1.0-rc1-30613 - 2.1.0-rc1-30613 - 2.1.0-rc1-30613 - 2.1.0-rc1-30613 - 2.1.0-rc1-30613 - 2.1.0-rc1-30613 - 2.1.0-rc1-30613 - 2.1.0-rc1-30613 - 2.1.0-rc1-30613 - 2.1.0-rc1-30613 - 2.1.0-rc1-30613 + 2.1.0-rtm-15783 + 2.1.0-rtm-30721 + 2.1.0-rtm-30721 + 2.1.0-rtm-30721 + 2.1.0-rtm-30721 + 2.1.0-rtm-30721 + 2.1.0-rtm-30721 + 2.1.0-rtm-30721 + 2.1.0-rtm-30721 + 2.1.0-rtm-30721 + 2.1.0-rtm-30721 + 2.1.0-rtm-30721 + 2.1.0-rtm-30721 + 2.1.0-rtm-30721 + 2.1.0-rtm-30721 + 2.1.0-rtm-30721 + 2.1.0-rtm-30721 + 2.1.0-rtm-30721 + 2.1.0-rtm-30721 + 2.1.0-rtm-30721 + 2.1.0-rtm-30721 2.0.0 - 2.1.0-rc1-26419-02 + 2.1.0-rtm-26502-02 15.6.1 4.7.49 - 2.0.1 + 2.0.3 0.8.0 2.3.1 2.4.0-beta.1.build3945 diff --git a/korebuild-lock.txt b/korebuild-lock.txt index 9d4ef8c888..3673744db9 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:2.1.0-rc1-15774 -commithash:ed5ca9de3c652347dbb0158a9a65eff3471d2114 +version:2.1.0-rtm-15783 +commithash:5fc2b2f607f542a2ffde11c19825e786fc1a3774 From 7aeda0427c8f7c3ed43428ea3ec2836ebca19b26 Mon Sep 17 00:00:00 2001 From: "ASP.NET CI" Date: Tue, 29 May 2018 09:51:14 -0700 Subject: [PATCH 02/20] Update dependencies.props [auto-updated: dependencies] --- build/dependencies.props | 44 ++++++++++++++++++++-------------------- korebuild-lock.txt | 4 ++-- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index 703213658f..0e8869b7bb 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -4,29 +4,29 @@ 0.10.13 - 2.1.0-rtm-15783 - 2.1.0-rtm-30721 - 2.1.0-rtm-30721 - 2.1.0-rtm-30721 - 2.1.0-rtm-30721 - 2.1.0-rtm-30721 - 2.1.0-rtm-30721 - 2.1.0-rtm-30721 - 2.1.0-rtm-30721 - 2.1.0-rtm-30721 - 2.1.0-rtm-30721 - 2.1.0-rtm-30721 - 2.1.0-rtm-30721 - 2.1.0-rtm-30721 - 2.1.0-rtm-30721 - 2.1.0-rtm-30721 - 2.1.0-rtm-30721 - 2.1.0-rtm-30721 - 2.1.0-rtm-30721 - 2.1.0-rtm-30721 - 2.1.0-rtm-30721 + 2.1.1-rtm-15790 + 2.1.0 + 2.1.0 + 2.1.0 + 2.1.0 + 2.1.0 + 2.1.0 + 2.1.0 + 2.1.0 + 2.1.0 + 2.1.0 + 2.1.0 + 2.1.0 + 2.1.0 + 2.1.0 + 2.1.0 + 2.1.0 + 2.1.0 + 2.1.0 + 2.1.0 + 2.1.0 2.0.0 - 2.1.0-rtm-26502-02 + 2.1.0 15.6.1 4.7.49 2.0.3 diff --git a/korebuild-lock.txt b/korebuild-lock.txt index 3673744db9..cd5b409a1e 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:2.1.0-rtm-15783 -commithash:5fc2b2f607f542a2ffde11c19825e786fc1a3774 +version:2.1.1-rtm-15790 +commithash:274c65868e735f29f4078c1884c61c4371ee1fc0 From 37c499e1e151f3922e8b98515daa220c9f707e57 Mon Sep 17 00:00:00 2001 From: Nate McMaster Date: Tue, 5 Jun 2018 09:11:43 -0700 Subject: [PATCH 03/20] Bumping version from 2.1.0 to 2.1.1 --- version.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.props b/version.props index b9552451d8..669c874829 100644 --- a/version.props +++ b/version.props @@ -1,6 +1,6 @@ - + - 2.1.0 + 2.1.1 rtm $(VersionPrefix) $(VersionPrefix)-$(VersionSuffix)-final From 12912ae6eb34c780e85b0a44a9b6e40e0520ad67 Mon Sep 17 00:00:00 2001 From: "ASP.NET CI" Date: Tue, 12 Jun 2018 19:32:39 +0000 Subject: [PATCH 04/20] Update dependencies.props [auto-updated: dependencies] --- build/dependencies.props | 42 ++++++++++++++++++++-------------------- korebuild-lock.txt | 4 ++-- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index 0e8869b7bb..f721a17eef 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -4,29 +4,29 @@ 0.10.13 - 2.1.1-rtm-15790 - 2.1.0 - 2.1.0 - 2.1.0 - 2.1.0 - 2.1.0 - 2.1.0 - 2.1.0 - 2.1.0 + 2.1.1-rtm-15793 + 2.1.1 + 2.1.1 + 2.1.1 + 2.1.1 + 2.1.1 + 2.1.1 + 2.1.1 + 2.1.1 2.1.0 - 2.1.0 - 2.1.0 - 2.1.0 - 2.1.0 - 2.1.0 - 2.1.0 - 2.1.0 - 2.1.0 - 2.1.0 - 2.1.0 - 2.1.0 + 2.1.1 + 2.1.1 + 2.1.1 + 2.1.1 + 2.1.1 + 2.1.1 + 2.1.1 + 2.1.1 + 2.1.1 + 2.1.1 + 2.1.1 2.0.0 - 2.1.0 + 2.1.1 15.6.1 4.7.49 2.0.3 diff --git a/korebuild-lock.txt b/korebuild-lock.txt index cd5b409a1e..bc84e0cd53 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:2.1.1-rtm-15790 -commithash:274c65868e735f29f4078c1884c61c4371ee1fc0 +version:2.1.1-rtm-15793 +commithash:988313f4b064d6c69fc6f7b845b6384a6af3447a From 4cb04470a5d93f2c7caa613af1adc60898a6c9d2 Mon Sep 17 00:00:00 2001 From: Ryan Brandenburg Date: Thu, 14 Jun 2018 10:30:10 -0700 Subject: [PATCH 05/20] Set 2.1 baselines --- .../baseline.netcore.json | 68 +++--- .../baseline.netcore.json | 202 ++++++++++-------- 2 files changed, 145 insertions(+), 125 deletions(-) diff --git a/src/Microsoft.AspNetCore.Routing.Abstractions/baseline.netcore.json b/src/Microsoft.AspNetCore.Routing.Abstractions/baseline.netcore.json index 146393eba9..8f8a0cc67d 100644 --- a/src/Microsoft.AspNetCore.Routing.Abstractions/baseline.netcore.json +++ b/src/Microsoft.AspNetCore.Routing.Abstractions/baseline.netcore.json @@ -1,5 +1,5 @@ { - "AssemblyIdentity": "Microsoft.AspNetCore.Routing.Abstractions, Version=2.0.2.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "AssemblyIdentity": "Microsoft.AspNetCore.Routing.Abstractions, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", "Types": [ { "Name": "Microsoft.AspNetCore.Routing.IRouteConstraint", @@ -305,6 +305,28 @@ "System.Collections.Generic.IReadOnlyDictionary" ], "Members": [ + { + "Kind": "Method", + "Name": "get_Count", + "Parameters": [], + "ReturnType": "System.Int32", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.Collections.Generic.ICollection>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Clear", + "Parameters": [], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.Collections.Generic.ICollection>", + "Visibility": "Public", + "GenericParameter": [] + }, { "Kind": "Method", "Name": "get_Item", @@ -349,17 +371,6 @@ "Visibility": "Public", "GenericParameter": [] }, - { - "Kind": "Method", - "Name": "get_Count", - "Parameters": [], - "ReturnType": "System.Int32", - "Sealed": true, - "Virtual": true, - "ImplementedInterface": "System.Collections.Generic.ICollection>", - "Visibility": "Public", - "GenericParameter": [] - }, { "Kind": "Method", "Name": "get_Keys", @@ -402,17 +413,6 @@ "Visibility": "Public", "GenericParameter": [] }, - { - "Kind": "Method", - "Name": "Clear", - "Parameters": [], - "ReturnType": "System.Void", - "Sealed": true, - "Virtual": true, - "ImplementedInterface": "System.Collections.Generic.ICollection>", - "Visibility": "Public", - "GenericParameter": [] - }, { "Kind": "Method", "Name": "ContainsKey", @@ -786,17 +786,6 @@ "System.Collections.Generic.IEnumerator>" ], "Members": [ - { - "Kind": "Method", - "Name": "get_Current", - "Parameters": [], - "ReturnType": "System.Collections.Generic.KeyValuePair", - "Sealed": true, - "Virtual": true, - "ImplementedInterface": "System.Collections.Generic.IEnumerator>", - "Visibility": "Public", - "GenericParameter": [] - }, { "Kind": "Method", "Name": "Dispose", @@ -830,6 +819,17 @@ "Visibility": "Public", "GenericParameter": [] }, + { + "Kind": "Method", + "Name": "get_Current", + "Parameters": [], + "ReturnType": "System.Collections.Generic.KeyValuePair", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.Collections.Generic.IEnumerator>", + "Visibility": "Public", + "GenericParameter": [] + }, { "Kind": "Constructor", "Name": ".ctor", diff --git a/src/Microsoft.AspNetCore.Routing/baseline.netcore.json b/src/Microsoft.AspNetCore.Routing/baseline.netcore.json index 9a486ec55b..866f3e89cb 100644 --- a/src/Microsoft.AspNetCore.Routing/baseline.netcore.json +++ b/src/Microsoft.AspNetCore.Routing/baseline.netcore.json @@ -1,5 +1,5 @@ { - "AssemblyIdentity": "Microsoft.AspNetCore.Routing, Version=2.0.2.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "AssemblyIdentity": "Microsoft.AspNetCore.Routing, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", "Types": [ { "Name": "Microsoft.AspNetCore.Builder.MapRouteRouteBuilderExtensions", @@ -967,6 +967,66 @@ "Microsoft.AspNetCore.Routing.INamedRouter" ], "Members": [ + { + "Kind": "Method", + "Name": "OnRouteMatched", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Routing.RouteContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Abstract": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "OnVirtualPathGenerated", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Routing.VirtualPathContext" + } + ], + "ReturnType": "Microsoft.AspNetCore.Routing.VirtualPathData", + "Virtual": true, + "Abstract": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "RouteAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Routing.RouteContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouter", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetVirtualPath", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Routing.VirtualPathContext" + } + ], + "ReturnType": "Microsoft.AspNetCore.Routing.VirtualPathData", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouter", + "Visibility": "Public", + "GenericParameter": [] + }, { "Kind": "Method", "Name": "get_Constraints", @@ -1106,66 +1166,6 @@ "Visibility": "Protected", "GenericParameter": [] }, - { - "Kind": "Method", - "Name": "OnRouteMatched", - "Parameters": [ - { - "Name": "context", - "Type": "Microsoft.AspNetCore.Routing.RouteContext" - } - ], - "ReturnType": "System.Threading.Tasks.Task", - "Virtual": true, - "Abstract": true, - "Visibility": "Protected", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "OnVirtualPathGenerated", - "Parameters": [ - { - "Name": "context", - "Type": "Microsoft.AspNetCore.Routing.VirtualPathContext" - } - ], - "ReturnType": "Microsoft.AspNetCore.Routing.VirtualPathData", - "Virtual": true, - "Abstract": true, - "Visibility": "Protected", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "RouteAsync", - "Parameters": [ - { - "Name": "context", - "Type": "Microsoft.AspNetCore.Routing.RouteContext" - } - ], - "ReturnType": "System.Threading.Tasks.Task", - "Virtual": true, - "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouter", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "GetVirtualPath", - "Parameters": [ - { - "Name": "context", - "Type": "Microsoft.AspNetCore.Routing.VirtualPathContext" - } - ], - "ReturnType": "Microsoft.AspNetCore.Routing.VirtualPathData", - "Virtual": true, - "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouter", - "Visibility": "Public", - "GenericParameter": [] - }, { "Kind": "Method", "Name": "GetConstraints", @@ -1369,6 +1369,36 @@ "Microsoft.AspNetCore.Routing.IRouteCollection" ], "Members": [ + { + "Kind": "Method", + "Name": "RouteAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Routing.RouteContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouter", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetVirtualPath", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Routing.VirtualPathContext" + } + ], + "ReturnType": "Microsoft.AspNetCore.Routing.VirtualPathData", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouter", + "Visibility": "Public", + "GenericParameter": [] + }, { "Kind": "Method", "Name": "get_Item", @@ -1406,36 +1436,6 @@ "Visibility": "Public", "GenericParameter": [] }, - { - "Kind": "Method", - "Name": "RouteAsync", - "Parameters": [ - { - "Name": "context", - "Type": "Microsoft.AspNetCore.Routing.RouteContext" - } - ], - "ReturnType": "System.Threading.Tasks.Task", - "Virtual": true, - "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouter", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "GetVirtualPath", - "Parameters": [ - { - "Name": "context", - "Type": "Microsoft.AspNetCore.Routing.VirtualPathContext" - } - ], - "ReturnType": "Microsoft.AspNetCore.Routing.VirtualPathData", - "Virtual": true, - "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouter", - "Visibility": "Public", - "GenericParameter": [] - }, { "Kind": "Constructor", "Name": ".ctor", @@ -2455,6 +2455,26 @@ ], "Visibility": "Public", "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "loggerFactory", + "Type": "Microsoft.Extensions.Logging.ILoggerFactory" + }, + { + "Name": "objectPool", + "Type": "Microsoft.Extensions.ObjectPool.ObjectPool" + }, + { + "Name": "constraintResolver", + "Type": "Microsoft.AspNetCore.Routing.IInlineConstraintResolver" + } + ], + "Visibility": "Public", + "GenericParameter": [] } ], "GenericParameters": [] From ebab62c7666b26fe0f98546cbb210352a7fa1d1d Mon Sep 17 00:00:00 2001 From: Nate McMaster Date: Wed, 27 Jun 2018 13:39:51 -0700 Subject: [PATCH 06/20] Bumping version from 2.1.1 to 2.1.2 --- version.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.props b/version.props index 669c874829..478dfd16ed 100644 --- a/version.props +++ b/version.props @@ -1,6 +1,6 @@  - 2.1.1 + 2.1.2 rtm $(VersionPrefix) $(VersionPrefix)-$(VersionSuffix)-final From 9c23ffb2159a3f3ce30511e9e7cbca6603c835bc Mon Sep 17 00:00:00 2001 From: Kiran Challa Date: Mon, 9 Jul 2018 12:05:04 -0700 Subject: [PATCH 07/20] Added tests for verifying the effects of change made to empty string hashcode from netcoreapp2.0 to 2.1 --- .../Tree/TreeRouterTest.cs | 89 ++++++++++++++++++- 1 file changed, 88 insertions(+), 1 deletion(-) diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Tree/TreeRouterTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/Tree/TreeRouterTest.cs index 8538c10e06..66d15c5314 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/Tree/TreeRouterTest.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/Tree/TreeRouterTest.cs @@ -1399,7 +1399,6 @@ namespace Microsoft.AspNetCore.Routing.Tree Assert.Empty(pathData.DataTokens); } - [Fact] public void TreeRouter_GenerateLink_Match_WithParameters() { @@ -1965,6 +1964,94 @@ namespace Microsoft.AspNetCore.Routing.Tree Assert.DoesNotContain(nestedValues, kvp => kvp.Key == "category1"); } + [Fact] + public void TreeRouter_GenerateLink_MatchesNullRequiredValue_WithNullRequestValueString() + { + // Arrange + var builder = CreateBuilder(); + var entry = MapOutboundEntry( + builder, + "Help/Store", + requiredValues: new { area = (string)null, action = "Edit", controller = "Store" }); + var route = builder.Build(); + var context = CreateVirtualPathContext(new { area = (string)null, action = "Edit", controller = "Store" }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.NotNull(pathData); + Assert.Equal("/Help/Store", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } + + [Fact] + public void TreeRouter_GenerateLink_MatchesNullRequiredValue_WithEmptyRequestValueString() + { + // Arrange + var builder = CreateBuilder(); + var entry = MapOutboundEntry( + builder, + "Help/Store", + requiredValues: new { area = (string)null, action = "Edit", controller = "Store" }); + var route = builder.Build(); + var context = CreateVirtualPathContext(new { area = "", action = "Edit", controller = "Store" }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.NotNull(pathData); + Assert.Equal("/Help/Store", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } + + [Fact] + public void TreeRouter_GenerateLink_MatchesEmptyStringRequiredValue_WithNullRequestValueString() + { + // Arrange + var builder = CreateBuilder(); + var entry = MapOutboundEntry( + builder, + "Help/Store", + requiredValues: new { foo = "", action = "Edit", controller = "Store" }); + var route = builder.Build(); + var context = CreateVirtualPathContext(new { foo = (string)null, action = "Edit", controller = "Store" }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.NotNull(pathData); + Assert.Equal("/Help/Store", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } + + [Fact] + public void TreeRouter_GenerateLink_MatchesEmptyStringRequiredValue_WithEmptyRequestValueString() + { + // Arrange + var builder = CreateBuilder(); + var entry = MapOutboundEntry( + builder, + "Help/Store", + requiredValues: new { foo = "", action = "Edit", controller = "Store" }); + var route = builder.Build(); + var context = CreateVirtualPathContext(new { foo = "", action = "Edit", controller = "Store" }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.NotNull(pathData); + Assert.Equal("/Help/Store", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } + private static RouteContext CreateRouteContext(string requestPath) { var request = new Mock(MockBehavior.Strict); From fe10d7c626d8c099c2faeb267481032798399b1d Mon Sep 17 00:00:00 2001 From: Nate McMaster Date: Wed, 11 Jul 2018 15:06:41 -0700 Subject: [PATCH 08/20] Reverting version from 2.1.2 back to 2.1.1 As a result of changing the way we apply servicing updates to aspnet core, this repo did not need the version bump because there are no planned product changes in this repo. --- version.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.props b/version.props index 478dfd16ed..669c874829 100644 --- a/version.props +++ b/version.props @@ -1,6 +1,6 @@  - 2.1.2 + 2.1.1 rtm $(VersionPrefix) $(VersionPrefix)-$(VersionSuffix)-final From b9cf6886d58cffd0bcc401b46a485a86e31bbdce Mon Sep 17 00:00:00 2001 From: Nate McMaster Date: Wed, 11 Jul 2018 18:49:43 -0700 Subject: [PATCH 09/20] Updating dependencies to 2.1.2 and adding a section for pinned variable versions --- build/dependencies.props | 15 +++++++++++---- korebuild-lock.txt | 4 ++-- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index f721a17eef..a48b1bd036 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -2,16 +2,18 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - + + + 0.10.13 - 2.1.1-rtm-15793 + 2.1.3-rtm-15802 2.1.1 2.1.1 2.1.1 2.1.1 2.1.1 2.1.1 - 2.1.1 + 2.1.2 2.1.1 2.1.0 2.1.1 @@ -26,7 +28,7 @@ 2.1.1 2.1.1 2.0.0 - 2.1.1 + 2.1.2 15.6.1 4.7.49 2.0.3 @@ -34,5 +36,10 @@ 2.3.1 2.4.0-beta.1.build3945 + + + + + diff --git a/korebuild-lock.txt b/korebuild-lock.txt index bc84e0cd53..251c227c83 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:2.1.1-rtm-15793 -commithash:988313f4b064d6c69fc6f7b845b6384a6af3447a +version:2.1.3-rtm-15802 +commithash:a7c08b45b440a7d2058a0aa1eaa3eb6ba811976a From 58b66f7cbb3858aa1b7b8b2848b8d3cec4df0e40 Mon Sep 17 00:00:00 2001 From: Nate McMaster Date: Thu, 12 Jul 2018 11:58:07 -0700 Subject: [PATCH 10/20] Pin version variables to the ASP.NET Core 2.1.2 baseline This reverts our previous policy of cascading versions on all servicing updates. This moves variables into the 'pinned' section, and points them to the latest stable release (versions that were used at the time of the 2.1.2 release). --- build/dependencies.props | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index a48b1bd036..6aafe4467f 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -4,9 +4,24 @@ - + 0.10.13 2.1.3-rtm-15802 + 2.0.0 + 2.1.2 + 15.6.1 + 4.7.49 + 2.0.3 + 0.8.0 + 2.3.1 + 2.4.0-beta.1.build3945 + + + + + + + 2.1.1 2.1.1 2.1.1 @@ -27,19 +42,5 @@ 2.1.1 2.1.1 2.1.1 - 2.0.0 - 2.1.2 - 15.6.1 - 4.7.49 - 2.0.3 - 0.8.0 - 2.3.1 - 2.4.0-beta.1.build3945 - - - - - - - + \ No newline at end of file From d255d1510f021dee37fc78a76195130e544f6b7c Mon Sep 17 00:00:00 2001 From: Eilon Lipton Date: Tue, 24 Jul 2018 10:55:29 -0700 Subject: [PATCH 11/20] Update CONTRIBUTING.md --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 64ff041d5c..eac4268e4c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,4 @@ Contributing ====== -Information on contributing to this repo is in the [Contributing Guide](https://github.com/aspnet/Home/blob/dev/CONTRIBUTING.md) in the Home repo. +Information on contributing to this repo is in the [Contributing Guide](https://github.com/aspnet/Home/blob/master/CONTRIBUTING.md) in the Home repo. From 34499dbe24034e402a483a9dd70722edab3a615f Mon Sep 17 00:00:00 2001 From: Kiran Challa Date: Tue, 24 Jul 2018 12:19:16 -0700 Subject: [PATCH 12/20] Added support for suppressing link generation for endpoints --- .../ISuppressLinkGenerationMetadata.cs | 9 +++++++++ .../RouteValuesBasedEndpointFinder.cs | 7 +++++++ .../RouteValueBasedEndpointFinderTest.cs | 17 +++++++++++++++++ 3 files changed, 33 insertions(+) create mode 100644 src/Microsoft.AspNetCore.Routing.Abstractions/ISuppressLinkGenerationMetadata.cs diff --git a/src/Microsoft.AspNetCore.Routing.Abstractions/ISuppressLinkGenerationMetadata.cs b/src/Microsoft.AspNetCore.Routing.Abstractions/ISuppressLinkGenerationMetadata.cs new file mode 100644 index 0000000000..2e6a7da5d8 --- /dev/null +++ b/src/Microsoft.AspNetCore.Routing.Abstractions/ISuppressLinkGenerationMetadata.cs @@ -0,0 +1,9 @@ +// 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.AspNetCore.Routing +{ + public interface ISuppressLinkGenerationMetadata + { + } +} diff --git a/src/Microsoft.AspNetCore.Routing/RouteValuesBasedEndpointFinder.cs b/src/Microsoft.AspNetCore.Routing/RouteValuesBasedEndpointFinder.cs index 2c3afaf55d..e4fc5c16a7 100644 --- a/src/Microsoft.AspNetCore.Routing/RouteValuesBasedEndpointFinder.cs +++ b/src/Microsoft.AspNetCore.Routing/RouteValuesBasedEndpointFinder.cs @@ -107,6 +107,13 @@ namespace Microsoft.AspNetCore.Routing var endpoints = _endpointDataSource.Endpoints.OfType(); foreach (var endpoint in endpoints) { + // Do not consider an endpoint for link generation if the following marker metadata is on it + var suppressLinkGeneration = endpoint.Metadata.GetMetadata(); + if (suppressLinkGeneration != null) + { + continue; + } + var entry = CreateOutboundRouteEntry(endpoint); var outboundMatch = new OutboundMatch() { Entry = entry }; diff --git a/test/Microsoft.AspNetCore.Routing.Tests/RouteValueBasedEndpointFinderTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/RouteValueBasedEndpointFinderTest.cs index 6296604d2a..fab9c41600 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/RouteValueBasedEndpointFinderTest.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/RouteValueBasedEndpointFinderTest.cs @@ -224,6 +224,21 @@ namespace Microsoft.AspNetCore.Routing Assert.Same(expected, actual); } + [Fact] + public void GetOutboundMatches_DoesNotInclude_EndpointsWithSuppressLinkGenerationMetadata() + { + // Arrange + var endpoint = CreateEndpoint( + "/a", + metadataCollection: new EndpointMetadataCollection(new[] { new SuppressLinkGenerationMetadata() })); + + // Act + var finder = CreateEndpointFinder(endpoint); + + // Assert + Assert.Empty(finder.AllMatches); + } + private CustomRouteValuesBasedEndpointFinder CreateEndpointFinder(params Endpoint[] endpoints) { return CreateEndpointFinder(new DefaultEndpointDataSource(endpoints)); @@ -304,5 +319,7 @@ namespace Microsoft.AspNetCore.Routing return matches; } } + + private class SuppressLinkGenerationMetadata : ISuppressLinkGenerationMetadata { } } } From 71cb933a08a56fc89de1f09200dc1eac5101ad79 Mon Sep 17 00:00:00 2001 From: Kiran Challa Date: Fri, 20 Jul 2018 10:09:31 -0700 Subject: [PATCH 13/20] Show a flattened tree in LinkGenerationDecisionTree's DebuggerDisplayString [Fixes #636] Flatten the LinkGenerationDecisionTree to show as debugger display string --- .../Internal/LinkGenerationDecisionTree.cs | 50 +++++++++++++++++++ .../LinkGenerationDecisionTreeTest.cs | 38 +++++++++++++- 2 files changed, 87 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.AspNetCore.Routing/Internal/LinkGenerationDecisionTree.cs b/src/Microsoft.AspNetCore.Routing/Internal/LinkGenerationDecisionTree.cs index 951d6b645c..8e81202fdc 100644 --- a/src/Microsoft.AspNetCore.Routing/Internal/LinkGenerationDecisionTree.cs +++ b/src/Microsoft.AspNetCore.Routing/Internal/LinkGenerationDecisionTree.cs @@ -3,12 +3,16 @@ using System; using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; using Microsoft.AspNetCore.Routing.DecisionTree; using Microsoft.AspNetCore.Routing.Tree; namespace Microsoft.AspNetCore.Routing.Internal { // A decision tree that matches link generation entries based on route data. + [DebuggerDisplay("{DebuggerDisplayString,nq}")] public class LinkGenerationDecisionTree { private readonly DecisionTreeNode _root; @@ -160,5 +164,51 @@ namespace Microsoft.AspNetCore.Routing.Internal y.Match.Entry.RouteTemplate.TemplateText); } } + + // Example output: + // + // => action: Buy => controller: Store => version: V1(Matches: Store/Buy/V1) + // => action: Buy => controller: Store => version: V2(Matches: Store/Buy/V2) + // => action: Buy => controller: Store => area: Admin(Matches: Admin/Store/Buy) + // => action: Buy => controller: Products(Matches: Products/Buy) + // => action: Cart => controller: Store(Matches: Store/Cart) + internal string DebuggerDisplayString + { + get + { + var sb = new StringBuilder(); + var branchStack = new Stack(); + branchStack.Push(string.Empty); + FlattenTree(_root, branchStack, sb); + return sb.ToString(); + } + } + + private void FlattenTree(DecisionTreeNode node, Stack branchStack, StringBuilder sb) + { + // leaf node + if (node.Criteria.Count == 0) + { + var temp = new StringBuilder(); + foreach (var branch in branchStack) + { + temp.Insert(0, branch); + } + sb.Append(temp.ToString()); + sb.Append(" (Matches: "); + sb.Append(string.Join(", ", node.Matches.Select(m => m.Entry.RouteTemplate.TemplateText))); + sb.AppendLine(")"); + } + + foreach (var criterion in node.Criteria) + { + foreach (var branch in criterion.Branches) + { + branchStack.Push($" => {criterion.Key}: {branch.Key}"); + FlattenTree(branch.Value, branchStack, sb); + branchStack.Pop(); + } + } + } } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Internal/LinkGenerationDecisionTreeTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/Internal/LinkGenerationDecisionTreeTest.cs index c8c34b09d4..f07296faba 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/Internal/LinkGenerationDecisionTreeTest.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/Internal/LinkGenerationDecisionTreeTest.cs @@ -1,9 +1,11 @@ // 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 Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.AspNetCore.Routing.Template; using Microsoft.AspNetCore.Routing.Tree; using Xunit; @@ -318,11 +320,45 @@ namespace Microsoft.AspNetCore.Routing.Internal.Routing Assert.Equal(entries, matches); } - private OutboundMatch CreateMatch(object requiredValues) + [Fact] + public void ToDebuggerDisplayString_GivesAFlattenedTree() + { + // Arrange + var entries = new List(); + entries.Add(CreateMatch(new { action = "Buy", controller = "Store", version = "V1" }, "Store/Buy/V1")); + entries.Add(CreateMatch(new { action = "Buy", controller = "Store", area = "Admin" }, "Admin/Store/Buy")); + entries.Add(CreateMatch(new { action = "Buy", controller = "Products" }, "Products/Buy")); + entries.Add(CreateMatch(new { action = "Buy", controller = "Store", version = "V2" }, "Store/Buy/V2")); + entries.Add(CreateMatch(new { action = "Cart", controller = "Store" }, "Store/Cart")); + entries.Add(CreateMatch(new { action = "Index", controller = "Home" }, "Home/Index/{id?}")); + var tree = new LinkGenerationDecisionTree(entries); + var newLine = Environment.NewLine; + var expected = + " => action: Buy => controller: Store => version: V1 (Matches: Store/Buy/V1)" + newLine + + " => action: Buy => controller: Store => version: V2 (Matches: Store/Buy/V2)" + newLine + + " => action: Buy => controller: Store => area: Admin (Matches: Admin/Store/Buy)" + newLine + + " => action: Buy => controller: Products (Matches: Products/Buy)" + newLine + + " => action: Cart => controller: Store (Matches: Store/Cart)" + newLine + + " => action: Index => controller: Home (Matches: Home/Index/{id?})" + newLine; + + // Act + var flattenedTree = tree.DebuggerDisplayString; + + // Assert + Assert.Equal(expected, flattenedTree); + } + + private OutboundMatch CreateMatch(object requiredValues, string routeTemplate = null) { var match = new OutboundMatch(); match.Entry = new OutboundRouteEntry(); match.Entry.RequiredLinkValues = new RouteValueDictionary(requiredValues); + + if (!string.IsNullOrEmpty(routeTemplate)) + { + match.Entry.RouteTemplate = new RouteTemplate(RoutePatternFactory.Parse(routeTemplate)); + } + return match; } From 6f4c10a664bbb155e5085e822f126feef1d33eb4 Mon Sep 17 00:00:00 2001 From: Kiran Challa Date: Tue, 24 Jul 2018 17:17:08 -0700 Subject: [PATCH 14/20] PR feedback --- .../Internal/LinkGenerationDecisionTree.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Microsoft.AspNetCore.Routing/Internal/LinkGenerationDecisionTree.cs b/src/Microsoft.AspNetCore.Routing/Internal/LinkGenerationDecisionTree.cs index 8e81202fdc..eb055f7696 100644 --- a/src/Microsoft.AspNetCore.Routing/Internal/LinkGenerationDecisionTree.cs +++ b/src/Microsoft.AspNetCore.Routing/Internal/LinkGenerationDecisionTree.cs @@ -179,22 +179,22 @@ namespace Microsoft.AspNetCore.Routing.Internal var sb = new StringBuilder(); var branchStack = new Stack(); branchStack.Push(string.Empty); - FlattenTree(_root, branchStack, sb); + FlattenTree(branchStack, sb, _root); return sb.ToString(); } } - private void FlattenTree(DecisionTreeNode node, Stack branchStack, StringBuilder sb) + private void FlattenTree(Stack branchStack, StringBuilder sb, DecisionTreeNode node) { // leaf node if (node.Criteria.Count == 0) { - var temp = new StringBuilder(); + var matchesSb = new StringBuilder(); foreach (var branch in branchStack) { - temp.Insert(0, branch); + matchesSb.Insert(0, branch); } - sb.Append(temp.ToString()); + sb.Append(matchesSb.ToString()); sb.Append(" (Matches: "); sb.Append(string.Join(", ", node.Matches.Select(m => m.Entry.RouteTemplate.TemplateText))); sb.AppendLine(")"); @@ -205,7 +205,7 @@ namespace Microsoft.AspNetCore.Routing.Internal foreach (var branch in criterion.Branches) { branchStack.Push($" => {criterion.Key}: {branch.Key}"); - FlattenTree(branch.Value, branchStack, sb); + FlattenTree(branchStack, sb, branch.Value); branchStack.Pop(); } } From f37ca0d2e9252e412367783722eaf777bb6da9b1 Mon Sep 17 00:00:00 2001 From: Kiran Challa Date: Sat, 21 Jul 2018 04:14:15 -0700 Subject: [PATCH 15/20] Show list of endpoints in CompositeEndpointDataSource's DebuggerDisplayString [Fixes #633] Show list of registered endpoints as debugger display string --- .../CompositeEndpointDataSource.cs | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/src/Microsoft.AspNetCore.Routing/CompositeEndpointDataSource.cs b/src/Microsoft.AspNetCore.Routing/CompositeEndpointDataSource.cs index bb4836da32..5c6a95500a 100644 --- a/src/Microsoft.AspNetCore.Routing/CompositeEndpointDataSource.cs +++ b/src/Microsoft.AspNetCore.Routing/CompositeEndpointDataSource.cs @@ -3,12 +3,17 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; +using System.Text; using System.Threading; +using Microsoft.AspNetCore.Routing.EndpointConstraints; +using Microsoft.AspNetCore.Routing.Matchers; using Microsoft.Extensions.Primitives; namespace Microsoft.AspNetCore.Routing { + [DebuggerDisplay("{DebuggerDisplayString,nq}")] public class CompositeEndpointDataSource : EndpointDataSource { private readonly EndpointDataSource[] _dataSources; @@ -106,5 +111,51 @@ namespace Microsoft.AspNetCore.Routing _cts = new CancellationTokenSource(); _consumerChangeToken = new CancellationChangeToken(_cts.Token); } + + private string DebuggerDisplayString + { + get + { + // Try using private variable '_endpoints' to avoid initialization + if (_endpoints == null) + { + return "No endpoints"; + } + + var sb = new StringBuilder(); + foreach (var endpoint in _endpoints) + { + if (endpoint is MatcherEndpoint matcherEndpoint) + { + var template = matcherEndpoint.RoutePattern.RawText; + template = string.IsNullOrEmpty(template) ? "\"\"" : template; + sb.Append(template); + var requiredValues = matcherEndpoint.RequiredValues.Select(kvp => $"{kvp.Key} = \"{kvp.Value ?? "null"}\""); + sb.Append(", Required Values: new { "); + sb.Append(string.Join(", ", requiredValues)); + sb.Append(" }"); + sb.Append(", Order:"); + sb.Append(matcherEndpoint.Order); + + var httpEndpointConstraints = matcherEndpoint.Metadata.GetOrderedMetadata() + .OfType(); + foreach (var constraint in httpEndpointConstraints) + { + sb.Append(", Http Methods: "); + sb.Append(string.Join(", ", constraint.HttpMethods)); + sb.Append(", Constraint Order:"); + sb.Append(constraint.Order); + } + sb.AppendLine(); + } + else + { + sb.Append("Non-MatcherEndpoint. DisplayName:"); + sb.AppendLine(endpoint.DisplayName); + } + } + return sb.ToString(); + } + } } } From 147c9527f3a7062ee692f573188828666152ced4 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Tue, 24 Jul 2018 17:31:51 -0700 Subject: [PATCH 16/20] Implement EndpointSelector and MatcherPolicy (#646) Implement EndpointSelector and MatcherPolicy This change makes the EndpointSelector API more concrete, and is the beggining of removing EndpointConstraint by making it obsolete. To that end, I'm introducing MatcherPolicy, which is a feature-collection API for registering policies that interact with the DfaMatcher. The two policies that we'll need to start are: - ability to order endpoints - ability to append 'policy' nodes to the graph These two concepts together replace EndpointConstraint. Extending our graph representation is a really efficient way to processing most common scenarios. --- In general this helps with common cases where 4 or so endpoints match the URL, but with different HTTP methods supported on each. Today we have to process route values and call into some 'policy' to make a decision about which one is the winner. This change pushes this knowledge down into the graph so that it's roughly as cheap as a dictionary lookup, and can be done allocation-free. The big savings here is ability to remove more candidates *before* collecting route data. --- Along with this change, I also built 'rejection' into the DFA node model, you can see an example with the HTTP Method handling that I implemented. I implemented a policy that can treat failure to resolve an HTTP method as a 405 response by returning a failure endpoint. This is at the heart of much of the feedback we've gotten in this area around versioning and http method handling. We also have a version of this feature in MVC for [Consumes]. --- ...heFindCandidateSetSingleEntryBenchmark.cs} | 10 +- .../Matchers/MatcherBenchmarkBase.cs | 1 + ... MatcherFindCandidateSetAzureBenchmark.cs} | 10 +- ...ndCandidateSetAzureBenchmark.generated.cs} | 2 +- ...MatcherFindCandidateSetGithubBenchmark.cs} | 10 +- ...dCandidateSetGithubBenchmark.generated.cs} | 2 +- ...ndCandidateSetSmallEntryCountBenchmark.cs} | 10 +- .../Matchers/TrivialMatcher.cs | 12 +- .../RoutingServiceCollectionExtensions.cs | 4 +- ... => EndpointConstraintEndpointSelector.cs} | 104 ++-- .../HttpMethodEndpointConstraint.cs | 5 +- .../IEndpointConstraint.cs | 17 +- .../Matchers/Candidate.cs | 16 + .../Matchers/CandidateSet.cs | 169 ++++-- .../Matchers/CandidateState.cs | 31 ++ .../Matchers/DefaultEndpointSelector.cs | 86 +++ .../Matchers/DfaMatcher.cs | 171 ++---- .../Matchers/DfaMatcherBuilder.cs | 254 ++++++--- .../Matchers/DfaNode.cs | 38 +- .../Matchers/DfaState.cs | 15 +- .../Matchers/EndpointMetadataComparer.cs | 59 ++ .../Matchers/EndpointSelector.cs | 16 + .../Matchers/HttpMethodMatcherPolicy.cs | 182 +++++++ .../Matchers/IEndpointComparerPolicy.cs | 12 + .../Matchers/INodeBuilderPolicy.cs | 16 + .../Matchers/MatcherBuilderEntry.cs | 49 -- .../Matchers/MatcherEndpointComparer.cs | 103 ++++ .../Matchers/MatcherPolicy.cs | 10 + .../Matchers/PolicyJumpTable.cs | 17 + .../Matchers/PolicyJumpTableBuilder.cs | 32 ++ .../Matchers/PolicyJumpTableEdge.cs | 18 + .../Matchers/PolicyNodeEdge.cs | 20 + .../Metadata/HttpMethodMetadata.cs | 24 + .../Metadata/IHttpMethodMetadata.cs | 12 + .../Patterns/RoutePattern.cs | 8 + .../EndpointConstraintEndpointSelectorTest.cs | 510 ++++++++++++++++++ .../EndpointSelectorTests.cs | 505 ----------------- .../HttpMethodEndpointConstraintTest.cs | 2 + .../Matchers/BarebonesMatcher.cs | 12 +- .../Matchers/CandidateSetTest.cs | 103 ++++ .../Matchers/DefaultEndpointSelectorTest.cs | 203 +++++++ .../Matchers/DfaMatcherBuilderTest.cs | 407 ++++++++++++-- .../Matchers/DfaMatcherTest.cs | 20 +- .../Matchers/EndpointMetadataComparerTest.cs | 91 ++++ .../HttpMethodMatcherPolicyIntegrationTest.cs | 232 ++++++++ .../Matchers/HttpMethodMatcherPolicyTest.cs | 171 ++++++ .../Matchers/MatcherEndpointComparerTest.cs | 254 +++++++++ .../Matchers/RouteMatcherBuilder.cs | 42 +- .../Matchers/TreeRouterMatcherBuilder.cs | 40 +- 49 files changed, 3192 insertions(+), 945 deletions(-) rename benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/{MatcherSelectCandidatesSingleEntryBenchmark.cs => MatcheFindCandidateSetSingleEntryBenchmark.cs} (83%) rename benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/{MatcherSelectCandidatesAzureBenchmark.cs => MatcherFindCandidateSetAzureBenchmark.cs} (84%) rename benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/{MatcherSelectCandidatesAzureBenchmark.generated.cs => MatcherFindCandidateSetAzureBenchmark.generated.cs} (99%) rename benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/{MatcherSelectCandidatesGithubBenchmark.cs => MatcherFindCandidateSetGithubBenchmark.cs} (83%) rename benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/{MatcherSelectCandidatesGithubBenchmark.generated.cs => MatcherFindCandidateSetGithubBenchmark.generated.cs} (99%) rename benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/{MatcherSelectCandidatesSmallEntryCountBenchmark.cs => MatcherFindCandidateSetSmallEntryCountBenchmark.cs} (90%) rename src/Microsoft.AspNetCore.Routing/EndpointConstraints/{EndpointSelector.cs => EndpointConstraintEndpointSelector.cs} (69%) create mode 100644 src/Microsoft.AspNetCore.Routing/Matchers/CandidateState.cs create mode 100644 src/Microsoft.AspNetCore.Routing/Matchers/DefaultEndpointSelector.cs create mode 100644 src/Microsoft.AspNetCore.Routing/Matchers/EndpointMetadataComparer.cs create mode 100644 src/Microsoft.AspNetCore.Routing/Matchers/EndpointSelector.cs create mode 100644 src/Microsoft.AspNetCore.Routing/Matchers/HttpMethodMatcherPolicy.cs create mode 100644 src/Microsoft.AspNetCore.Routing/Matchers/IEndpointComparerPolicy.cs create mode 100644 src/Microsoft.AspNetCore.Routing/Matchers/INodeBuilderPolicy.cs delete mode 100644 src/Microsoft.AspNetCore.Routing/Matchers/MatcherBuilderEntry.cs create mode 100644 src/Microsoft.AspNetCore.Routing/Matchers/MatcherEndpointComparer.cs create mode 100644 src/Microsoft.AspNetCore.Routing/Matchers/MatcherPolicy.cs create mode 100644 src/Microsoft.AspNetCore.Routing/Matchers/PolicyJumpTable.cs create mode 100644 src/Microsoft.AspNetCore.Routing/Matchers/PolicyJumpTableBuilder.cs create mode 100644 src/Microsoft.AspNetCore.Routing/Matchers/PolicyJumpTableEdge.cs create mode 100644 src/Microsoft.AspNetCore.Routing/Matchers/PolicyNodeEdge.cs create mode 100644 src/Microsoft.AspNetCore.Routing/Metadata/HttpMethodMetadata.cs create mode 100644 src/Microsoft.AspNetCore.Routing/Metadata/IHttpMethodMetadata.cs create mode 100644 test/Microsoft.AspNetCore.Routing.Tests/EndpointConstraints/EndpointConstraintEndpointSelectorTest.cs delete mode 100644 test/Microsoft.AspNetCore.Routing.Tests/EndpointConstraints/EndpointSelectorTests.cs create mode 100644 test/Microsoft.AspNetCore.Routing.Tests/Matchers/CandidateSetTest.cs create mode 100644 test/Microsoft.AspNetCore.Routing.Tests/Matchers/DefaultEndpointSelectorTest.cs create mode 100644 test/Microsoft.AspNetCore.Routing.Tests/Matchers/EndpointMetadataComparerTest.cs create mode 100644 test/Microsoft.AspNetCore.Routing.Tests/Matchers/HttpMethodMatcherPolicyIntegrationTest.cs create mode 100644 test/Microsoft.AspNetCore.Routing.Tests/Matchers/HttpMethodMatcherPolicyTest.cs create mode 100644 test/Microsoft.AspNetCore.Routing.Tests/Matchers/MatcherEndpointComparerTest.cs diff --git a/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/MatcherSelectCandidatesSingleEntryBenchmark.cs b/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/MatcheFindCandidateSetSingleEntryBenchmark.cs similarity index 83% rename from benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/MatcherSelectCandidatesSingleEntryBenchmark.cs rename to benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/MatcheFindCandidateSetSingleEntryBenchmark.cs index 1769ae37b9..18818df532 100644 --- a/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/MatcherSelectCandidatesSingleEntryBenchmark.cs +++ b/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/MatcheFindCandidateSetSingleEntryBenchmark.cs @@ -7,7 +7,7 @@ using Microsoft.AspNetCore.Http; namespace Microsoft.AspNetCore.Routing.Matchers { - public class MatcherSelectCandidatesSingleEntryBenchmark : MatcherBenchmarkBase + public class MatcheFindCandidateSetSingleEntryBenchmark : MatcherBenchmarkBase { private TrivialMatcher _baseline; private DfaMatcher _dfa; @@ -40,9 +40,9 @@ namespace Microsoft.AspNetCore.Routing.Matchers var path = httpContext.Request.Path.Value; var segments = new ReadOnlySpan(Array.Empty()); - var candidates = _baseline.SelectCandidates(path, segments); + var candidates = _baseline.FindCandidateSet(path, segments); - var endpoint = candidates.Candidates[0].Endpoint; + var endpoint = candidates[0].Endpoint; Validate(Requests[0], Endpoints[0], endpoint); } @@ -54,9 +54,9 @@ namespace Microsoft.AspNetCore.Routing.Matchers Span segments = stackalloc PathSegment[FastPathTokenizer.DefaultSegmentCount]; var count = FastPathTokenizer.Tokenize(path, segments); - var candidates = _dfa.SelectCandidates(path, segments.Slice(0, count)); + var candidates = _dfa.FindCandidateSet(httpContext, path, segments.Slice(0, count)); - var endpoint = candidates.Candidates[0].Endpoint; + var endpoint = candidates[0].Endpoint; Validate(Requests[0], Endpoints[0], endpoint); } } diff --git a/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/MatcherBenchmarkBase.cs b/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/MatcherBenchmarkBase.cs index 3cb9649887..bf6a18297b 100644 --- a/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/MatcherBenchmarkBase.cs +++ b/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/MatcherBenchmarkBase.cs @@ -8,6 +8,7 @@ using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing.EndpointConstraints; +using Microsoft.AspNetCore.Routing.Metadata; using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.Extensions.DependencyInjection; diff --git a/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/MatcherSelectCandidatesAzureBenchmark.cs b/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/MatcherFindCandidateSetAzureBenchmark.cs similarity index 84% rename from benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/MatcherSelectCandidatesAzureBenchmark.cs rename to benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/MatcherFindCandidateSetAzureBenchmark.cs index edf73d6a48..7c346eca4a 100644 --- a/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/MatcherSelectCandidatesAzureBenchmark.cs +++ b/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/MatcherFindCandidateSetAzureBenchmark.cs @@ -7,7 +7,7 @@ using BenchmarkDotNet.Attributes; namespace Microsoft.AspNetCore.Routing.Matchers { // Generated from https://github.com/Azure/azure-rest-api-specs - public partial class MatcherSelectCandidatesAzureBenchmark : MatcherBenchmarkBase + public partial class MatcherFindCandidateSetAzureBenchmark : MatcherBenchmarkBase { private const int SampleCount = 100; @@ -42,9 +42,9 @@ namespace Microsoft.AspNetCore.Routing.Matchers var path = httpContext.Request.Path.Value; var segments = new ReadOnlySpan(Array.Empty()); - var candidates = _baseline.Matchers[sample].SelectCandidates(path, segments); + var candidates = _baseline.Matchers[sample].FindCandidateSet(path, segments); - var endpoint = candidates.Candidates[0].Endpoint; + var endpoint = candidates[0].Endpoint; Validate(httpContext, Endpoints[sample], endpoint); } } @@ -61,9 +61,9 @@ namespace Microsoft.AspNetCore.Routing.Matchers Span segments = stackalloc PathSegment[FastPathTokenizer.DefaultSegmentCount]; var count = FastPathTokenizer.Tokenize(path, segments); - var candidates = _dfa.SelectCandidates(path, segments.Slice(0, count)); + var candidates = _dfa.FindCandidateSet(httpContext, path, segments.Slice(0, count)); - var endpoint = candidates.Candidates[0].Endpoint; + var endpoint = candidates[0].Endpoint; Validate(httpContext, Endpoints[sample], endpoint); } } diff --git a/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/MatcherSelectCandidatesAzureBenchmark.generated.cs b/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/MatcherFindCandidateSetAzureBenchmark.generated.cs similarity index 99% rename from benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/MatcherSelectCandidatesAzureBenchmark.generated.cs rename to benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/MatcherFindCandidateSetAzureBenchmark.generated.cs index 65bea532fd..c68f822fc2 100644 --- a/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/MatcherSelectCandidatesAzureBenchmark.generated.cs +++ b/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/MatcherFindCandidateSetAzureBenchmark.generated.cs @@ -7,7 +7,7 @@ using Microsoft.AspNetCore.Http; namespace Microsoft.AspNetCore.Routing.Matchers { // This code was generated by the Swaggatherer - public partial class MatcherSelectCandidatesAzureBenchmark : MatcherBenchmarkBase + public partial class MatcherFindCandidateSetAzureBenchmark : MatcherBenchmarkBase { private const int EndpointCount = 3517; diff --git a/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/MatcherSelectCandidatesGithubBenchmark.cs b/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/MatcherFindCandidateSetGithubBenchmark.cs similarity index 83% rename from benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/MatcherSelectCandidatesGithubBenchmark.cs rename to benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/MatcherFindCandidateSetGithubBenchmark.cs index f40382dd5d..7b13d8fddf 100644 --- a/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/MatcherSelectCandidatesGithubBenchmark.cs +++ b/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/MatcherFindCandidateSetGithubBenchmark.cs @@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers { // Generated from https://github.com/APIs-guru/openapi-directory // Use https://editor2.swagger.io/ to convert from yaml to json- - public partial class MatcherSelectCandidatesGithubBenchmark : MatcherBenchmarkBase + public partial class MatcherFindCandidateSetGithubBenchmark : MatcherBenchmarkBase { private BarebonesMatcher _baseline; private DfaMatcher _dfa; @@ -35,9 +35,9 @@ namespace Microsoft.AspNetCore.Routing.Matchers var path = httpContext.Request.Path.Value; var segments = new ReadOnlySpan(Array.Empty()); - var candidates = _baseline.Matchers[i].SelectCandidates(path, segments); + var candidates = _baseline.Matchers[i].FindCandidateSet(path, segments); - var endpoint = candidates.Candidates[0].Endpoint; + var endpoint = candidates[0].Endpoint; Validate(httpContext, Endpoints[i], endpoint); } } @@ -53,9 +53,9 @@ namespace Microsoft.AspNetCore.Routing.Matchers Span segments = stackalloc PathSegment[FastPathTokenizer.DefaultSegmentCount]; var count = FastPathTokenizer.Tokenize(path, segments); - var candidates = _dfa.SelectCandidates(path, segments.Slice(0, count)); + var candidates = _dfa.FindCandidateSet(httpContext, path, segments.Slice(0, count)); - var endpoint = candidates.Candidates[0].Endpoint; + var endpoint = candidates[0].Endpoint; Validate(httpContext, Endpoints[i], endpoint); } } diff --git a/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/MatcherSelectCandidatesGithubBenchmark.generated.cs b/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/MatcherFindCandidateSetGithubBenchmark.generated.cs similarity index 99% rename from benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/MatcherSelectCandidatesGithubBenchmark.generated.cs rename to benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/MatcherFindCandidateSetGithubBenchmark.generated.cs index 2326b6bd8a..034547b9d2 100644 --- a/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/MatcherSelectCandidatesGithubBenchmark.generated.cs +++ b/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/MatcherFindCandidateSetGithubBenchmark.generated.cs @@ -7,7 +7,7 @@ using Microsoft.AspNetCore.Http; namespace Microsoft.AspNetCore.Routing.Matchers { // This code was generated by the Swaggatherer - public partial class MatcherSelectCandidatesGithubBenchmark : MatcherBenchmarkBase + public partial class MatcherFindCandidateSetGithubBenchmark : MatcherBenchmarkBase { private const int EndpointCount = 155; diff --git a/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/MatcherSelectCandidatesSmallEntryCountBenchmark.cs b/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/MatcherFindCandidateSetSmallEntryCountBenchmark.cs similarity index 90% rename from benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/MatcherSelectCandidatesSmallEntryCountBenchmark.cs rename to benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/MatcherFindCandidateSetSmallEntryCountBenchmark.cs index 790c8993f3..472ff9c2a8 100644 --- a/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/MatcherSelectCandidatesSmallEntryCountBenchmark.cs +++ b/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/MatcherFindCandidateSetSmallEntryCountBenchmark.cs @@ -7,7 +7,7 @@ using Microsoft.AspNetCore.Http; namespace Microsoft.AspNetCore.Routing.Matchers { - public class MatcherSelectCandidatesSmallEntryCountBenchmark : MatcherBenchmarkBase + public class MatcherFindCandidateSetSmallEntryCountBenchmark : MatcherBenchmarkBase { private TrivialMatcher _baseline; private DfaMatcher _dfa; @@ -75,9 +75,9 @@ namespace Microsoft.AspNetCore.Routing.Matchers var segments = new ReadOnlySpan(Array.Empty()); - var candidates = _baseline.SelectCandidates(path, segments); + var candidates = _baseline.FindCandidateSet(path, segments); - var endpoint = candidates.Candidates[0].Endpoint; + var endpoint = candidates[0].Endpoint; Validate(Requests[0], Endpoints[9], endpoint); } @@ -90,9 +90,9 @@ namespace Microsoft.AspNetCore.Routing.Matchers Span segments = stackalloc PathSegment[FastPathTokenizer.DefaultSegmentCount]; var count = FastPathTokenizer.Tokenize(path, segments); - var candidates = _dfa.SelectCandidates(path, segments.Slice(0, count)); + var candidates = _dfa.FindCandidateSet(httpContext, path, segments.Slice(0, count)); - var endpoint = candidates.Candidates[0].Endpoint; + var endpoint = candidates[0].Endpoint; Validate(Requests[0], Endpoints[9], endpoint); } } diff --git a/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/TrivialMatcher.cs b/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/TrivialMatcher.cs index 0483a0b5a9..afdb27d0d9 100644 --- a/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/TrivialMatcher.cs +++ b/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/TrivialMatcher.cs @@ -13,17 +13,13 @@ namespace Microsoft.AspNetCore.Routing.Matchers internal sealed class TrivialMatcher : Matcher { private readonly MatcherEndpoint _endpoint; - private readonly CandidateSet _candidates; + private readonly Candidate[] _candidates; public TrivialMatcher(MatcherEndpoint endpoint) { _endpoint = endpoint; - _candidates = new CandidateSet( - new Candidate[] { new Candidate(endpoint), }, - - // Single candidate group that contains one entry. - CandidateSet.MakeGroups(new[] { 1 })); + _candidates = new Candidate[] { new Candidate(endpoint), }; } public sealed override Task MatchAsync(HttpContext httpContext, IEndpointFeature feature) @@ -49,14 +45,14 @@ namespace Microsoft.AspNetCore.Routing.Matchers } // This is here so this can be tested alongside DFA matcher. - internal CandidateSet SelectCandidates(string path, ReadOnlySpan segments) + internal Candidate[] FindCandidateSet(string path, ReadOnlySpan segments) { if (string.Equals(_endpoint.RoutePattern.RawText, path, StringComparison.OrdinalIgnoreCase)) { return _candidates; } - return CandidateSet.Empty; + return Array.Empty(); } } } diff --git a/src/Microsoft.AspNetCore.Routing/DependencyInjection/RoutingServiceCollectionExtensions.cs b/src/Microsoft.AspNetCore.Routing/DependencyInjection/RoutingServiceCollectionExtensions.cs index 1474d6e76d..fac20603a5 100644 --- a/src/Microsoft.AspNetCore.Routing/DependencyInjection/RoutingServiceCollectionExtensions.cs +++ b/src/Microsoft.AspNetCore.Routing/DependencyInjection/RoutingServiceCollectionExtensions.cs @@ -73,11 +73,13 @@ namespace Microsoft.Extensions.DependencyInjection services.TryAddSingleton, NameBasedEndpointFinder>(); services.TryAddSingleton, RouteValuesBasedEndpointFinder>(); services.TryAddSingleton(); + // // Endpoint Selection // - services.TryAddSingleton(); + services.TryAddSingleton(); services.TryAddSingleton(); + services.TryAddEnumerable(ServiceDescriptor.Singleton()); // Will be cached by the EndpointSelector services.TryAddEnumerable( diff --git a/src/Microsoft.AspNetCore.Routing/EndpointConstraints/EndpointSelector.cs b/src/Microsoft.AspNetCore.Routing/EndpointConstraints/EndpointConstraintEndpointSelector.cs similarity index 69% rename from src/Microsoft.AspNetCore.Routing/EndpointConstraints/EndpointSelector.cs rename to src/Microsoft.AspNetCore.Routing/EndpointConstraints/EndpointConstraintEndpointSelector.cs index 4ade56e671..f063079f87 100644 --- a/src/Microsoft.AspNetCore.Routing/EndpointConstraints/EndpointSelector.cs +++ b/src/Microsoft.AspNetCore.Routing/EndpointConstraints/EndpointConstraintEndpointSelector.cs @@ -1,20 +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. -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Routing.Matchers; -using Microsoft.Extensions.Internal; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; -using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing.Matchers; +using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Routing.EndpointConstraints { - internal class EndpointSelector + internal class EndpointConstraintEndpointSelector : EndpointSelector { private static readonly IReadOnlyList EmptyEndpoints = Array.Empty(); @@ -22,7 +19,7 @@ namespace Microsoft.AspNetCore.Routing.EndpointConstraints private readonly EndpointConstraintCache _endpointConstraintCache; private readonly ILogger _logger; - public EndpointSelector( + public EndpointConstraintEndpointSelector( CompositeEndpointDataSource dataSource, EndpointConstraintCache endpointConstraintCache, ILoggerFactory loggerFactory) @@ -32,11 +29,19 @@ namespace Microsoft.AspNetCore.Routing.EndpointConstraints _endpointConstraintCache = endpointConstraintCache; } - public Endpoint SelectBestCandidate(HttpContext context, IReadOnlyList candidates) + public override Task SelectAsync( + HttpContext httpContext, + IEndpointFeature feature, + CandidateSet candidates) { - if (context == null) + if (httpContext == null) { - throw new ArgumentNullException(nameof(context)); + throw new ArgumentNullException(nameof(httpContext)); + } + + if (feature == null) + { + throw new ArgumentNullException(nameof(feature)); } if (candidates == null) @@ -44,25 +49,30 @@ namespace Microsoft.AspNetCore.Routing.EndpointConstraints throw new ArgumentNullException(nameof(candidates)); } - var finalMatches = EvaluateEndpointConstraints(context, candidates); + var finalMatches = EvaluateEndpointConstraints(httpContext, candidates); if (finalMatches == null || finalMatches.Count == 0) { - return null; + return Task.CompletedTask; } else if (finalMatches.Count == 1) { - var selectedEndpoint = finalMatches[0]; + var endpoint = finalMatches[0].Endpoint; + var values = finalMatches[0].Values; - return selectedEndpoint; + feature.Endpoint = endpoint; + feature.Invoker = (endpoint as MatcherEndpoint)?.Invoker; + feature.Values = values; + + return Task.CompletedTask; } else { var endpointNames = string.Join( Environment.NewLine, - finalMatches.Select(a => a.DisplayName)); + finalMatches.Select(a => a.Endpoint.DisplayName)); - Log.MatchAmbiguous(_logger, context, finalMatches); + Log.MatchAmbiguous(_logger, httpContext, finalMatches); var message = Resources.FormatAmbiguousEndpoints( Environment.NewLine, @@ -72,31 +82,61 @@ namespace Microsoft.AspNetCore.Routing.EndpointConstraints } } - private IReadOnlyList EvaluateEndpointConstraints( + private IReadOnlyList EvaluateEndpointConstraints( HttpContext context, - IReadOnlyList endpoints) + CandidateSet candidateSet) { var candidates = new List(); // Perf: Avoid allocations - for (var i = 0; i < endpoints.Count; i++) + for (var i = 0; i < candidateSet.Count; i++) { - var endpoint = endpoints[i]; - var constraints = _endpointConstraintCache.GetEndpointConstraints(context, endpoint); - candidates.Add(new EndpointSelectorCandidate(endpoint, constraints)); + ref var candidate = ref candidateSet[i]; + if (candidate.IsValidCandidate) + { + var endpoint = candidate.Endpoint; + var constraints = _endpointConstraintCache.GetEndpointConstraints(context, endpoint); + candidates.Add(new EndpointSelectorCandidate( + endpoint, + candidate.Score, + candidate.Values, + constraints)); + } } var matches = EvaluateEndpointConstraintsCore(context, candidates, startingOrder: null); - List results = null; + List results = null; if (matches != null) { - results = new List(matches.Count); - // Perf: Avoid allocations - for (var i = 0; i < matches.Count; i++) + results = new List(matches.Count); + + // We need to disambiguate based on 'score' - take the first value of 'score' + // and then we only copy matches while they have the same score. This accounts + // for a difference in behavior between new routing and old. + switch (matches.Count) { - var candidate = matches[i]; - results.Add(candidate.Endpoint); + case 0: + break; + + case 1: + results.Add(matches[0]); + break; + + default: + var score = matches[0].Score; + for (var i = 0; i < matches.Count; i++) + { + if (matches[i].Score != score) + { + break; + } + + results.Add(matches[i]); + } + + break; + } } @@ -213,11 +253,11 @@ namespace Microsoft.AspNetCore.Routing.EndpointConstraints new EventId(1, "MatchAmbiguous"), "Request matched multiple endpoints for request path '{Path}'. Matching endpoints: {AmbiguousEndpoints}"); - public static void MatchAmbiguous(ILogger logger, HttpContext httpContext, IEnumerable endpoints) + public static void MatchAmbiguous(ILogger logger, HttpContext httpContext, IEnumerable endpoints) { if (logger.IsEnabled(LogLevel.Error)) { - _matchAmbiguous(logger, httpContext.Request.Path, endpoints.Select(e => e.DisplayName), null); + _matchAmbiguous(logger, httpContext.Request.Path, endpoints.Select(e => e.Endpoint.DisplayName), null); } } } diff --git a/src/Microsoft.AspNetCore.Routing/EndpointConstraints/HttpMethodEndpointConstraint.cs b/src/Microsoft.AspNetCore.Routing/EndpointConstraints/HttpMethodEndpointConstraint.cs index 9c890732bc..7631e10b22 100644 --- a/src/Microsoft.AspNetCore.Routing/EndpointConstraints/HttpMethodEndpointConstraint.cs +++ b/src/Microsoft.AspNetCore.Routing/EndpointConstraints/HttpMethodEndpointConstraint.cs @@ -4,10 +4,11 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using Microsoft.AspNetCore.Routing.Metadata; namespace Microsoft.AspNetCore.Routing.EndpointConstraints { - public class HttpMethodEndpointConstraint : IEndpointConstraint + public class HttpMethodEndpointConstraint : IEndpointConstraint, IHttpMethodMetadata { public static readonly int HttpMethodConstraintOrder = 100; @@ -40,6 +41,8 @@ namespace Microsoft.AspNetCore.Routing.EndpointConstraints public int Order => HttpMethodConstraintOrder; + IReadOnlyList IHttpMethodMetadata.HttpMethods => _httpMethods; + public virtual bool Accept(EndpointConstraintContext context) { if (context == null) diff --git a/src/Microsoft.AspNetCore.Routing/EndpointConstraints/IEndpointConstraint.cs b/src/Microsoft.AspNetCore.Routing/EndpointConstraints/IEndpointConstraint.cs index ace843104d..92a1f17388 100644 --- a/src/Microsoft.AspNetCore.Routing/EndpointConstraints/IEndpointConstraint.cs +++ b/src/Microsoft.AspNetCore.Routing/EndpointConstraints/IEndpointConstraint.cs @@ -1,10 +1,9 @@ // 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.Http; using System; using System.Collections.Generic; -using System.Text; +using Microsoft.AspNetCore.Http; namespace Microsoft.AspNetCore.Routing.EndpointConstraints { @@ -28,9 +27,13 @@ namespace Microsoft.AspNetCore.Routing.EndpointConstraints { } - public struct EndpointSelectorCandidate + public readonly struct EndpointSelectorCandidate { - public EndpointSelectorCandidate(Endpoint endpoint, IReadOnlyList constraints) + public EndpointSelectorCandidate( + Endpoint endpoint, + int score, + RouteValueDictionary values, + IReadOnlyList constraints) { if (endpoint == null) { @@ -38,11 +41,17 @@ namespace Microsoft.AspNetCore.Routing.EndpointConstraints } Endpoint = endpoint; + Score = score; + Values = values; Constraints = constraints; } public Endpoint Endpoint { get; } + public int Score { get; } + + public RouteValueDictionary Values { get; } + public IReadOnlyList Constraints { get; } } diff --git a/src/Microsoft.AspNetCore.Routing/Matchers/Candidate.cs b/src/Microsoft.AspNetCore.Routing/Matchers/Candidate.cs index 857baa24cc..5b6459b386 100644 --- a/src/Microsoft.AspNetCore.Routing/Matchers/Candidate.cs +++ b/src/Microsoft.AspNetCore.Routing/Matchers/Candidate.cs @@ -34,6 +34,19 @@ namespace Microsoft.AspNetCore.Routing.Matchers public readonly MatchProcessor[] MatchProcessors; + // Score is a sequential integer value that in determines the priority of an Endpoint. + // Scores are computed within the context of candidate set, and are meaningless when + // applied to endpoints not in the set. + // + // The score concept boils down the system of comparisons done when ordering Endpoints + // to a single value that can be compared easily. This can be defeated by having + // int32.MaxValue + 1 endpoints in a single set, but you would have other problems by + // that point. + // + // Score is not part of the Endpoint itself, because it's contextual based on where + // the endpoint appears. An Endpoint is often be a member of multiple candiate sets. + public readonly int Score; + // Used in tests. public Candidate(MatcherEndpoint endpoint) { @@ -44,12 +57,14 @@ namespace Microsoft.AspNetCore.Routing.Matchers CatchAll = default; ComplexSegments = Array.Empty<(RoutePatternPathSegment pathSegment, int segmentIndex)>(); MatchProcessors = Array.Empty(); + Score = 0; Flags = CandidateFlags.None; } public Candidate( MatcherEndpoint endpoint, + int score, KeyValuePair[] slots, (string parameterName, int segmentIndex, int slotIndex)[] captures, (string parameterName, int segmentIndex, int slotIndex) catchAll, @@ -57,6 +72,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers MatchProcessor[] matchProcessors) { Endpoint = endpoint; + Score = score; Slots = slots; Captures = captures; CatchAll = catchAll; diff --git a/src/Microsoft.AspNetCore.Routing/Matchers/CandidateSet.cs b/src/Microsoft.AspNetCore.Routing/Matchers/CandidateSet.cs index ad85378f5c..9f89b765a2 100644 --- a/src/Microsoft.AspNetCore.Routing/Matchers/CandidateSet.cs +++ b/src/Microsoft.AspNetCore.Routing/Matchers/CandidateSet.cs @@ -2,56 +2,155 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Runtime.CompilerServices; namespace Microsoft.AspNetCore.Routing.Matchers { - internal class CandidateSet + public sealed class CandidateSet { - public static readonly CandidateSet Empty = new CandidateSet(Array.Empty(), Array.Empty()); + // We inline storage for 4 candidates here to avoid allocations in common + // cases. There's no real reason why 4 is important, it just seemed like + // a plausible number. + private CandidateState _state0; + private CandidateState _state1; + private CandidateState _state2; + private CandidateState _state3; - // The array of candidates. - public readonly Candidate[] Candidates; + private CandidateState[] _additionalCandidates; - // The number of groups. - public readonly int GroupCount; - - // The array of groups. Groups define a contiguous sets of indices into - // the candidates array. - // - // The groups array always contains N+1 entries where N is the number of groups. - // The extra array entry is there to make indexing easier, so we can lookup the 'end' - // of the last group without branching. - // - // Example: - // Group0: Candidates[0], Candidates[1] - // Group1: Candidates[2], Candidates[3], Candidates[4] - // - // The groups array would look like: { 0, 2, 5, } - public readonly int[] Groups; - - public CandidateSet(Candidate[] candidates, int[] groups) + // Provided to make testing possible/easy for someone implementing + // an EndpointSelector. + public CandidateSet(MatcherEndpoint[] endpoints, int[] scores) { - Candidates = candidates; - Groups = groups; + Count = endpoints.Length; - GroupCount = groups.Length == 0 ? 0 : groups.Length - 1; + switch (endpoints.Length) + { + case 0: + return; + + case 1: + _state0 = new CandidateState(endpoints[0], score: scores[0]); + break; + + case 2: + _state0 = new CandidateState(endpoints[0], score: scores[0]); + _state1 = new CandidateState(endpoints[1], score: scores[1]); + break; + + case 3: + _state0 = new CandidateState(endpoints[0], score: scores[0]); + _state1 = new CandidateState(endpoints[1], score: scores[1]); + _state2 = new CandidateState(endpoints[2], score: scores[2]); + break; + + case 4: + _state0 = new CandidateState(endpoints[0], score: scores[0]); + _state1 = new CandidateState(endpoints[1], score: scores[1]); + _state2 = new CandidateState(endpoints[2], score: scores[2]); + _state3 = new CandidateState(endpoints[3], score: scores[3]); + break; + + default: + _state0 = new CandidateState(endpoints[0], score: scores[0]); + _state1 = new CandidateState(endpoints[1], score: scores[1]); + _state2 = new CandidateState(endpoints[2], score: scores[2]); + _state3 = new CandidateState(endpoints[3], score: scores[3]); + + _additionalCandidates = new CandidateState[endpoints.Length - 4]; + for (var i = 4; i < endpoints.Length; i++) + { + _additionalCandidates[i - 4] = new CandidateState(endpoints[i], score: scores[i]); + } + break; + } } - // See description on Groups. - public static int[] MakeGroups(int[] lengths) + internal CandidateSet(Candidate[] candidates) { - var groups = new int[lengths.Length + 1]; + Count = candidates.Length; - var sum = 0; - for (var i = 0; i < lengths.Length; i++) + switch (candidates.Length) { - groups[i] = sum; - sum += lengths[i]; + case 0: + return; + + case 1: + _state0 = new CandidateState(candidates[0].Endpoint, candidates[0].Score); + break; + + case 2: + _state0 = new CandidateState(candidates[0].Endpoint, candidates[0].Score); + _state1 = new CandidateState(candidates[1].Endpoint, candidates[1].Score); + break; + + case 3: + _state0 = new CandidateState(candidates[0].Endpoint, candidates[0].Score); + _state1 = new CandidateState(candidates[1].Endpoint, candidates[1].Score); + _state2 = new CandidateState(candidates[2].Endpoint, candidates[2].Score); + break; + + case 4: + _state0 = new CandidateState(candidates[0].Endpoint, candidates[0].Score); + _state1 = new CandidateState(candidates[1].Endpoint, candidates[1].Score); + _state2 = new CandidateState(candidates[2].Endpoint, candidates[2].Score); + _state3 = new CandidateState(candidates[3].Endpoint, candidates[3].Score); + break; + + default: + _state0 = new CandidateState(candidates[0].Endpoint, candidates[0].Score); + _state1 = new CandidateState(candidates[1].Endpoint, candidates[1].Score); + _state2 = new CandidateState(candidates[2].Endpoint, candidates[2].Score); + _state3 = new CandidateState(candidates[3].Endpoint, candidates[3].Score); + + _additionalCandidates = new CandidateState[candidates.Length - 4]; + for (var i = 4; i < candidates.Length; i++) + { + _additionalCandidates[i - 4] = new CandidateState(candidates[i].Endpoint, candidates[i].Score); + } + break; } + } - groups[lengths.Length] = sum; + public int Count { get; } - return groups; + // Note that this is a ref-return because of both mutability and performance. + // We don't want to copy these fat structs if it can be avoided. + public ref CandidateState this[int index] + { + // PERF: Force inlining + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + // Friendliness for inlining + if ((uint)index >= Count) + { + ThrowIndexArgumentOutOfRangeException(); + } + + switch (index) + { + case 0: + return ref _state0; + + case 1: + return ref _state1; + + case 2: + return ref _state2; + + case 3: + return ref _state3; + + default: + return ref _additionalCandidates[index - 4]; + } + } + } + + private static void ThrowIndexArgumentOutOfRangeException() + { + throw new ArgumentOutOfRangeException("index"); } } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNetCore.Routing/Matchers/CandidateState.cs b/src/Microsoft.AspNetCore.Routing/Matchers/CandidateState.cs new file mode 100644 index 0000000000..b302b23bf2 --- /dev/null +++ b/src/Microsoft.AspNetCore.Routing/Matchers/CandidateState.cs @@ -0,0 +1,31 @@ +// 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.AspNetCore.Routing.Matchers +{ + public struct CandidateState + { + // Provided for testability + public CandidateState(MatcherEndpoint endpoint) + : this(endpoint, score: 0) + { + } + + public CandidateState(MatcherEndpoint endpoint, int score) + { + Endpoint = endpoint; + Score = score; + + IsValidCandidate = true; + Values = null; + } + + public MatcherEndpoint Endpoint { get; } + + public int Score { get; } + + public bool IsValidCandidate { get; set; } + + public RouteValueDictionary Values { get; set; } + } +} diff --git a/src/Microsoft.AspNetCore.Routing/Matchers/DefaultEndpointSelector.cs b/src/Microsoft.AspNetCore.Routing/Matchers/DefaultEndpointSelector.cs new file mode 100644 index 0000000000..b76010851f --- /dev/null +++ b/src/Microsoft.AspNetCore.Routing/Matchers/DefaultEndpointSelector.cs @@ -0,0 +1,86 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Routing.Matchers +{ + internal class DefaultEndpointSelector : EndpointSelector + { + public override Task SelectAsync( + HttpContext httpContext, + IEndpointFeature feature, + CandidateSet candidates) + { + MatcherEndpoint endpoint = null; + RouteValueDictionary values = null; + int? foundScore = null; + for (var i = 0; i < candidates.Count; i++) + { + ref var state = ref candidates[i]; + + var isValid = state.IsValidCandidate; + if (isValid && foundScore == null) + { + // This is the first match we've seen - speculatively assign it. + endpoint = state.Endpoint; + values = state.Values; + foundScore = state.Score; + } + else if (isValid && foundScore < state.Score) + { + // This candidate is lower priority than the one we've seen + // so far, we can stop. + // + // Don't worry about the 'null < state.Score' case, it returns false. + break; + } + else if (isValid && foundScore == state.Score) + { + // This is the second match we've found of the same score, so there + // must be an ambiguity. + // + // Don't worry about the 'null == state.Score' case, it returns false. + + ReportAmbiguity(candidates); + + // Unreachable, ReportAmbiguity always throws. + throw new NotSupportedException(); + } + } + + if (endpoint != null) + { + feature.Endpoint = endpoint; + feature.Invoker = endpoint.Invoker; + feature.Values = values; + } + + return Task.CompletedTask; + } + + private static void ReportAmbiguity(CandidateSet candidates) + { + // If we get here it's the result of an ambiguity - we're OK with this + // being a littler slower and more allocatey. + var matches = new List(); + for (var i = 0; i < candidates.Count; i++) + { + ref var state = ref candidates[i]; + if (state.IsValidCandidate) + { + matches.Add(state.Endpoint); + } + } + + var message = Resources.FormatAmbiguousEndpoints( + Environment.NewLine, + string.Join(Environment.NewLine, matches.Select(e => e.DisplayName))); + throw new AmbiguousMatchException(message); + } + } +} diff --git a/src/Microsoft.AspNetCore.Routing/Matchers/DfaMatcher.cs b/src/Microsoft.AspNetCore.Routing/Matchers/DfaMatcher.cs index 75a700a02b..a7908e1662 100644 --- a/src/Microsoft.AspNetCore.Routing/Matchers/DfaMatcher.cs +++ b/src/Microsoft.AspNetCore.Routing/Matchers/DfaMatcher.cs @@ -2,25 +2,21 @@ // 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.Diagnostics; -using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Routing.EndpointConstraints; using Microsoft.AspNetCore.Routing.Patterns; namespace Microsoft.AspNetCore.Routing.Matchers { internal sealed class DfaMatcher : Matcher { - private readonly EndpointSelector _endpointSelector; + private readonly EndpointSelector _selector; private readonly DfaState[] _states; - public DfaMatcher(EndpointSelector endpointSelector, DfaState[] states) + public DfaMatcher(EndpointSelector selector, DfaState[] states) { - _endpointSelector = endpointSelector; + _selector = selector; _states = states; } @@ -46,117 +42,40 @@ namespace Microsoft.AspNetCore.Routing.Matchers var count = FastPathTokenizer.Tokenize(path, buffer); var segments = buffer.Slice(0, count); - // SelectCandidates will process the DFA and return a candidate set. This does + // FindCandidateSet will process the DFA and return a candidate set. This does // some preliminary matching of the URL (mostly the literal segments). - var candidates = SelectCandidates(path, segments); - if (candidates.GroupCount == 0) + var candidates = FindCandidateSet(httpContext, path, segments); + if (candidates.Length == 0) { return Task.CompletedTask; } - // At this point we have a candidate set, defined as a list of groups - // of candidates. Each member of a given group has the same priority - // (priority is defined by order, precedence and other factors like http method - // or version). + // At this point we have a candidate set, defined as a list of endpoints in + // priority order. // // We don't yet know that any candidate can be considered a match, because // we haven't processed things like route constraints and complex segments. // - // Now we'll go group by group to capture route values, process constraints, + // Now we'll iterate each endpoint to capture route values, process constraints, // and process complex segments. + + // `candidates` has all of our internal state that we use to process the + // set of endpoints before we call the EndpointSelector. // - // Perf: using groups.Length - 1 here to elide the bounds check. We're relying - // on assumptions of how Groups works. - var candidatesArray = candidates.Candidates; - var groups = candidates.Groups; + // `candidateSet` is the mutable state that we pass to the EndpointSelector. + var candidateSet = new CandidateSet(candidates); - for (var i = 0; i < groups.Length - 1; i++) + for (var i = 0; i < candidates.Length; i++) { - var start = groups[i]; - var length = groups[i + 1] - groups[i]; - var group = candidatesArray.AsSpan(start, length); - - // Yes, these allocate. We should revise how this interaction works exactly - // once the extensibility is more locked down. + // PERF: using ref here to avoid copying around big structs. // - // Would could produce a fast path for a small number of members in - // a group. - var members = new BitArray(group.Length); - var groupValues = new RouteValueDictionary[group.Length]; + // Reminder! + // candidate: readonly data about the endpoint and how to match + // state: mutable storarge for our processing + ref var candidate = ref candidates[i]; + ref var state = ref candidateSet[i]; - if (FilterGroup( - httpContext, - path, - segments, - group, - members, - groupValues)) - { - // We must have some matches because FilterGroup returned true. - - // So: this code is SUPER SUPER temporary. We don't intent to keep - // EndpointSelector around for very long. - var candidatesForEndpointSelector = new List(); - for (var j = 0; j < group.Length; j++) - { - if (members.Get(j)) - { - candidatesForEndpointSelector.Add(group[j].Endpoint); - } - } - - var result = _endpointSelector.SelectBestCandidate(httpContext, candidatesForEndpointSelector); - if (result != null) - { - // Find the route values, based on which endpoint was selected. We have - // to do this because the endpoint selector returns an endpoint - // instead of mutating the feature. - for (var j = 0; j < group.Length; j++) - { - if (ReferenceEquals(result, group[j].Endpoint)) - { - feature.Endpoint = result; - feature.Invoker = ((MatcherEndpoint)result).Invoker; - feature.Values = groupValues[j]; - return Task.CompletedTask; - } - } - } - - // End super temporary code - } - } - - return Task.CompletedTask; - } - - internal CandidateSet SelectCandidates(string path, ReadOnlySpan segments) - { - var states = _states; - - var destination = 0; - for (var i = 0; i < segments.Length; i++) - { - destination = states[destination].Transitions.GetDestination(path, segments[i]); - } - - return states[destination].Candidates; - } - - private bool FilterGroup( - HttpContext httpContext, - string path, - ReadOnlySpan segments, - ReadOnlySpan group, - BitArray members, - RouteValueDictionary[] groupValues) - { - var hasMatch = false; - for (var i = 0; i < group.Length; i++) - { - // PERF: specifically not copying group[i] into a local. It's a relatively - // fat struct and we don't want to eagerly copy it. - var flags = group[i].Flags; + var flags = candidate.Flags; // First process all of the parameters and defaults. RouteValueDictionary values; @@ -170,7 +89,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers // // We want to create a new array for the route values based on Slots // as a prototype. - var prototype = group[i].Slots; + var prototype = candidate.Slots; var slots = new KeyValuePair[prototype.Length]; if ((flags & Candidate.CandidateFlags.HasDefaults) != 0) @@ -180,38 +99,62 @@ namespace Microsoft.AspNetCore.Routing.Matchers if ((flags & Candidate.CandidateFlags.HasCaptures) != 0) { - ProcessCaptures(slots, group[i].Captures, path, segments); + ProcessCaptures(slots, candidate.Captures, path, segments); } if ((flags & Candidate.CandidateFlags.HasCatchAll) != 0) - { - ProcessCatchAll(slots, group[i].CatchAll, path, segments); + { + ProcessCatchAll(slots, candidate.CatchAll, path, segments); } values = RouteValueDictionary.FromArray(slots); } - groupValues[i] = values; - + state.Values = values; + // Now that we have the route values, we need to process complex segments. // Complex segments go through an old API that requires a fully-materialized // route value dictionary. var isMatch = true; if ((flags & Candidate.CandidateFlags.HasComplexSegments) != 0) { - isMatch &= ProcessComplexSegments(group[i].ComplexSegments, path, segments, values); + isMatch &= ProcessComplexSegments(candidate.ComplexSegments, path, segments, values); } if ((flags & Candidate.CandidateFlags.HasMatchProcessors) != 0) { - isMatch &= ProcessMatchProcessors(group[i].MatchProcessors, httpContext, values); + isMatch &= ProcessMatchProcessors(candidate.MatchProcessors, httpContext, values); } - members.Set(i, isMatch); - hasMatch |= isMatch; + state.IsValidCandidate = isMatch; } - return hasMatch; + return _selector.SelectAsync(httpContext, feature, candidateSet); + } + + internal Candidate[] FindCandidateSet( + HttpContext httpContext, + string path, + ReadOnlySpan segments) + { + var states = _states; + + // Process each path segment + var destination = 0; + for (var i = 0; i < segments.Length; i++) + { + destination = states[destination].PathTransitions.GetDestination(path, segments[i]); + } + + // Process an arbitrary number of policy-based decisions + var policyTransitions = states[destination].PolicyTransitions; + while (policyTransitions != null) + { + destination = policyTransitions.GetDestination(httpContext); + policyTransitions = states[destination].PolicyTransitions; + } + + return states[destination].Candidates; } private void ProcessCaptures( diff --git a/src/Microsoft.AspNetCore.Routing/Matchers/DfaMatcherBuilder.cs b/src/Microsoft.AspNetCore.Routing/Matchers/DfaMatcherBuilder.cs index ab9936ed06..c5f75cee4e 100644 --- a/src/Microsoft.AspNetCore.Routing/Matchers/DfaMatcherBuilder.cs +++ b/src/Microsoft.AspNetCore.Routing/Matchers/DfaMatcherBuilder.cs @@ -4,32 +4,37 @@ using System; using System.Collections.Generic; using System.Linq; -using Microsoft.AspNetCore.Routing.EndpointConstraints; using Microsoft.AspNetCore.Routing.Patterns; -using Microsoft.AspNetCore.Routing.Template; -using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Routing.Matchers { internal class DfaMatcherBuilder : MatcherBuilder { - private readonly List _entries = new List(); - private readonly IInlineConstraintResolver _constraintResolver = new DefaultInlineConstraintResolver(Options.Create(new RouteOptions())); + private readonly List _endpoints = new List(); private readonly MatchProcessorFactory _matchProcessorFactory; - private readonly EndpointSelector _endpointSelector; + private readonly EndpointSelector _selector; + private readonly MatcherPolicy[] _policies; + private readonly INodeBuilderPolicy[] _nodeBuilders; + private readonly MatcherEndpointComparer _comparer; public DfaMatcherBuilder( MatchProcessorFactory matchProcessorFactory, - EndpointSelector endpointSelector) + EndpointSelector selector, + IEnumerable policies) { - _matchProcessorFactory = matchProcessorFactory ?? throw new ArgumentNullException(nameof(matchProcessorFactory)); - _endpointSelector = endpointSelector ?? throw new ArgumentNullException(nameof(endpointSelector)); + _matchProcessorFactory = matchProcessorFactory; + _selector = selector; + _policies = policies.OrderBy(p => p.Order).ToArray(); + + // Taking care to use _policies, which has been sorted. + _nodeBuilders = _policies.OfType().ToArray(); + _comparer = new MatcherEndpointComparer(_policies.OfType().ToArray()); } public override void AddEndpoint(MatcherEndpoint endpoint) { - _entries.Add(new MatcherBuilderEntry(endpoint)); + _endpoints.Add(endpoint); } public DfaNode BuildDfaTree() @@ -38,48 +43,48 @@ namespace Microsoft.AspNetCore.Routing.Matchers // because a 'parameter' node can also traverse the same paths that literal nodes // traverse. This means that we need to order the entries first, or else we will // miss possible edges in the DFA. - _entries.Sort(); + _endpoints.Sort(_comparer); // Since we're doing a BFS we will process each 'level' of the tree in stages // this list will hold the set of items we need to process at the current // stage. - var work = new List<(MatcherBuilderEntry entry, List parents)>(); + var work = new List<(MatcherEndpoint endpoint, List parents)>(); var root = new DfaNode() { Depth = 0, Label = "/" }; // To prepare for this we need to compute the max depth, as well as // a seed list of items to process (entry, root). var maxDepth = 0; - for (var i = 0; i < _entries.Count; i++) + for (var i = 0; i < _endpoints.Count; i++) { - var entry = _entries[i]; - maxDepth = Math.Max(maxDepth, entry.RoutePattern.PathSegments.Count); + var endpoint = _endpoints[i]; + maxDepth = Math.Max(maxDepth, endpoint.RoutePattern.PathSegments.Count); - work.Add((entry, new List() { root, })); + work.Add((endpoint, new List() { root, })); } // Now we process the entries a level at a time. for (var depth = 0; depth <= maxDepth; depth++) { // As we process items, collect the next set of items. - var nextWork = new List<(MatcherBuilderEntry entry, List parents)>(); + var nextWork = new List<(MatcherEndpoint endpoint, List parents)>(); for (var i = 0; i < work.Count; i++) { - var (entry, parents) = work[i]; + var (endpoint, parents) = work[i]; - if (!HasAdditionalRequiredSegments(entry, depth)) + if (!HasAdditionalRequiredSegments(endpoint, depth)) { for (var j = 0; j < parents.Count; j++) { var parent = parents[j]; - parent.Matches.Add(entry); + parent.Matches.Add(endpoint); } } // Find the parents of this edge at the current depth var nextParents = new List(); - var segment = GetCurrentSegment(entry, depth); + var segment = GetCurrentSegment(endpoint, depth); if (segment == null) { continue; @@ -133,7 +138,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers parent.CatchAll.CatchAll = parent.CatchAll; } - parent.CatchAll.Matches.Add(entry); + parent.CatchAll.Matches.Add(endpoint); } else if (segment.IsSimple && part.IsParameter) { @@ -172,7 +177,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers if (nextParents.Count > 0) { - nextWork.Add((entry, nextParents)); + nextWork.Add((endpoint, nextParents)); } } @@ -180,22 +185,26 @@ namespace Microsoft.AspNetCore.Routing.Matchers work = nextWork; } + // Build the trees of policy nodes (like HTTP methods). Post-order traversal + // means that we won't have infinite recursion. + root.Visit(ApplyPolicies); + return root; } - private RoutePatternPathSegment GetCurrentSegment(MatcherBuilderEntry entry, int depth) + private RoutePatternPathSegment GetCurrentSegment(MatcherEndpoint endpoint, int depth) { - if (depth < entry.RoutePattern.PathSegments.Count) + if (depth < endpoint.RoutePattern.PathSegments.Count) { - return entry.RoutePattern.PathSegments[depth]; + return endpoint.RoutePattern.PathSegments[depth]; } - if (entry.RoutePattern.PathSegments.Count == 0) + if (endpoint.RoutePattern.PathSegments.Count == 0) { return null; } - var lastSegment = entry.RoutePattern.PathSegments[entry.RoutePattern.PathSegments.Count - 1]; + var lastSegment = endpoint.RoutePattern.PathSegments[endpoint.RoutePattern.PathSegments.Count - 1]; if (lastSegment.IsSimple && lastSegment.Parts[0] is RoutePatternParameterPart parameterPart && parameterPart.IsCatchAll) { return lastSegment; @@ -209,46 +218,56 @@ namespace Microsoft.AspNetCore.Routing.Matchers var root = BuildDfaTree(); var states = new List(); - var tables = new List(); - AddNode(root, states, tables); + var tableBuilders = new List<(JumpTableBuilder pathBuilder, PolicyJumpTableBuilder policyBuilder)>(); + AddNode(root, states, tableBuilders); var exit = states.Count; - states.Add(new DfaState(CandidateSet.Empty, null)); - tables.Add(new JumpTableBuilder() { DefaultDestination = exit, ExitDestination = exit, }); + states.Add(new DfaState(Array.Empty(), null, null)); + tableBuilders.Add((new JumpTableBuilder() { DefaultDestination = exit, ExitDestination = exit, }, null)); - for (var i = 0; i < tables.Count; i++) + for (var i = 0; i < tableBuilders.Count; i++) { - if (tables[i].DefaultDestination == JumpTableBuilder.InvalidDestination) + if (tableBuilders[i].pathBuilder?.DefaultDestination == JumpTableBuilder.InvalidDestination) { - tables[i].DefaultDestination = exit; + tableBuilders[i].pathBuilder.DefaultDestination = exit; } - if (tables[i].ExitDestination == JumpTableBuilder.InvalidDestination) + if (tableBuilders[i].pathBuilder?.ExitDestination == JumpTableBuilder.InvalidDestination) { - tables[i].ExitDestination = exit; + tableBuilders[i].pathBuilder.ExitDestination = exit; + } + + if (tableBuilders[i].policyBuilder?.ExitDestination == JumpTableBuilder.InvalidDestination) + { + tableBuilders[i].policyBuilder.ExitDestination = exit; } } for (var i = 0; i < states.Count; i++) { - states[i] = new DfaState(states[i].Candidates, tables[i].Build()); + states[i] = new DfaState( + states[i].Candidates, + tableBuilders[i].pathBuilder?.Build(), + tableBuilders[i].policyBuilder?.Build()); } - return new DfaMatcher(_endpointSelector, states.ToArray()); + return new DfaMatcher(_selector, states.ToArray()); } - private int AddNode(DfaNode node, List states, List tables) + private int AddNode( + DfaNode node, + List states, + List<(JumpTableBuilder pathBuilder, PolicyJumpTableBuilder policyBuilder)> tableBuilders) { - node.Matches.Sort(); + node.Matches.Sort(_comparer); var stateIndex = states.Count; - var candidates = new CandidateSet( - node.Matches.Select(CreateCandidate).ToArray(), - CandidateSet.MakeGroups(GetGroupLengths(node))); - states.Add(new DfaState(candidates, null)); - var table = new JumpTableBuilder(); - tables.Add(table); + var candidates = CreateCandidates(node.Matches); + states.Add(new DfaState(candidates, null, null)); + + var pathBuilder = new JumpTableBuilder(); + tableBuilders.Add((pathBuilder, null)); foreach (var kvp in node.Literals) { @@ -258,7 +277,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers } var transition = Transition(kvp.Value); - table.AddEntry(kvp.Key, transition); + pathBuilder.AddEntry(kvp.Key, transition); } if (node.Parameters != null && @@ -267,26 +286,37 @@ namespace Microsoft.AspNetCore.Routing.Matchers { // This node has a single transition to but it should accept zero-width segments // this can happen when a node only has catchall parameters. - table.DefaultDestination = Transition(node.Parameters); - table.ExitDestination = table.DefaultDestination; + pathBuilder.DefaultDestination = Transition(node.Parameters); + pathBuilder.ExitDestination = pathBuilder.DefaultDestination; } else if (node.Parameters != null && node.CatchAll != null) { // This node has a separate transition for zero-width segments // this can happen when a node has both parameters and catchall parameters. - table.DefaultDestination = Transition(node.Parameters); - table.ExitDestination = Transition(node.CatchAll); + pathBuilder.DefaultDestination = Transition(node.Parameters); + pathBuilder.ExitDestination = Transition(node.CatchAll); } else if (node.Parameters != null) { // This node has paramters but no catchall. - table.DefaultDestination = Transition(node.Parameters); + pathBuilder.DefaultDestination = Transition(node.Parameters); } else if (node.CatchAll != null) { // This node has a catchall but no parameters - table.DefaultDestination = Transition(node.CatchAll); - table.ExitDestination = table.DefaultDestination; + pathBuilder.DefaultDestination = Transition(node.CatchAll); + pathBuilder.ExitDestination = pathBuilder.DefaultDestination; + } + + if (node.PolicyEdges.Count > 0) + { + var policyBuilder = new PolicyJumpTableBuilder(node.NodeBuilder); + tableBuilders[stateIndex] = (pathBuilder, policyBuilder); + + foreach (var kvp in node.PolicyEdges) + { + policyBuilder.AddEntry(kvp.Key, Transition(kvp.Value)); + } } return stateIndex; @@ -294,27 +324,58 @@ namespace Microsoft.AspNetCore.Routing.Matchers int Transition(DfaNode next) { // Break cycles - return ReferenceEquals(node, next) ? stateIndex : AddNode(next, states, tables); + return ReferenceEquals(node, next) ? stateIndex : AddNode(next, states, tableBuilders); } } + // Builds an array of candidates for a node, assigns a 'score' for each + // endpoint. + internal Candidate[] CreateCandidates(IReadOnlyList endpoints) + { + if (endpoints.Count == 0) + { + return Array.Empty(); + } + + var candiates = new Candidate[endpoints.Count]; + + var score = 0; + var examplar = endpoints[0]; + candiates[0] = CreateCandidate(examplar, score); + + for (var i = 1; i < endpoints.Count; i++) + { + var endpoint = endpoints[i]; + if (!_comparer.Equals(examplar, endpoint)) + { + // This endpoint doesn't have the same priority. + examplar = endpoint; + score++; + } + + candiates[i] = CreateCandidate(endpoint, score); + } + + return candiates; + } + // internal for tests - internal Candidate CreateCandidate(MatcherBuilderEntry entry) + internal Candidate CreateCandidate(MatcherEndpoint endpoint, int score) { var assignments = new Dictionary(StringComparer.OrdinalIgnoreCase); var slots = new List>(); var captures = new List<(string parameterName, int segmentIndex, int slotIndex)>(); (string parameterName, int segmentIndex, int slotIndex) catchAll = default; - foreach (var kvp in entry.Endpoint.RoutePattern.Defaults) + foreach (var kvp in endpoint.RoutePattern.Defaults) { assignments.Add(kvp.Key, assignments.Count); slots.Add(kvp); } - for (var i = 0; i < entry.Endpoint.RoutePattern.PathSegments.Count; i++) + for (var i = 0; i < endpoint.RoutePattern.PathSegments.Count; i++) { - var segment = entry.Endpoint.RoutePattern.PathSegments[i]; + var segment = endpoint.RoutePattern.PathSegments[i]; if (!segment.IsSimple) { continue; @@ -346,9 +407,9 @@ namespace Microsoft.AspNetCore.Routing.Matchers } var complexSegments = new List<(RoutePatternPathSegment pathSegment, int segmentIndex)>(); - for (var i = 0; i < entry.RoutePattern.PathSegments.Count; i++) + for (var i = 0; i < endpoint.RoutePattern.PathSegments.Count; i++) { - var segment = entry.RoutePattern.PathSegments[i]; + var segment = endpoint.RoutePattern.PathSegments[i]; if (segment.IsSimple) { continue; @@ -358,9 +419,9 @@ namespace Microsoft.AspNetCore.Routing.Matchers } var matchProcessors = new List(); - foreach (var kvp in entry.Endpoint.RoutePattern.Constraints) + foreach (var kvp in endpoint.RoutePattern.Constraints) { - var parameter = entry.Endpoint.RoutePattern.GetParameter(kvp.Key); // may be null, that's ok + var parameter = endpoint.RoutePattern.GetParameter(kvp.Key); // may be null, that's ok var constraintReferences = kvp.Value; for (var i = 0; i < constraintReferences.Count; i++) { @@ -371,7 +432,8 @@ namespace Microsoft.AspNetCore.Routing.Matchers } return new Candidate( - entry.Endpoint, + endpoint, + score, slots.ToArray(), captures.ToArray(), catchAll, @@ -393,7 +455,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers for (var i = 1; i < node.Matches.Count; i++) { - if (!exemplar.PriorityEquals(node.Matches[i])) + if (!_comparer.Equals(exemplar, node.Matches[i])) { groups.Add(length); length = 0; @@ -409,11 +471,11 @@ namespace Microsoft.AspNetCore.Routing.Matchers return groups.ToArray(); } - private static bool HasAdditionalRequiredSegments(MatcherBuilderEntry entry, int depth) + private static bool HasAdditionalRequiredSegments(MatcherEndpoint endpoint, int depth) { - for (var i = depth; i < entry.RoutePattern.PathSegments.Count; i++) + for (var i = depth; i < endpoint.RoutePattern.PathSegments.Count; i++) { - var segment = entry.RoutePattern.PathSegments[i]; + var segment = endpoint.RoutePattern.PathSegments[i]; if (!segment.IsSimple) { // Complex segments always require more processing @@ -437,5 +499,59 @@ namespace Microsoft.AspNetCore.Routing.Matchers return false; } + + private void ApplyPolicies(DfaNode node) + { + if (node.Matches.Count == 0) + { + return; + } + + // Start with the current node as the root. + var work = new List() { node, }; + for (var i = 0; i < _nodeBuilders.Length; i++) + { + var nodeBuilder = _nodeBuilders[i]; + + // Build a list of each + var nextWork = new List(); + + for (var j = 0; j < work.Count; j++) + { + var parent = work[j]; + if (!nodeBuilder.AppliesToNode(parent.Matches)) + { + // This node-builder doesn't care about this node, so add it to the list + // to be processed by the next node-builder. + nextWork.Add(parent); + continue; + } + + // This node-builder does apply to this node, so we need to create new nodes for each edge, + // and then attach them to the parent. + var edges = nodeBuilder.GetEdges(parent.Matches); + for (var k = 0; k < edges.Count; k++) + { + var edge = edges[k]; + + var next = new DfaNode(); + + // TODO: https://github.com/aspnet/Routing/issues/648 + next.Matches.AddRange(edge.Endpoints.Cast().ToArray()); + nextWork.Add(next); + + parent.PolicyEdges.Add(edge.State, next); + } + + // Associate the node-builder so we can build a jump table later. + parent.NodeBuilder = nodeBuilder; + + // The parent no longer has matches, it's not considered a terminal node. + parent.Matches.Clear(); + } + + work = nextWork; + } + } } } diff --git a/src/Microsoft.AspNetCore.Routing/Matchers/DfaNode.cs b/src/Microsoft.AspNetCore.Routing/Matchers/DfaNode.cs index bf16975db8..5508f0f460 100644 --- a/src/Microsoft.AspNetCore.Routing/Matchers/DfaNode.cs +++ b/src/Microsoft.AspNetCore.Routing/Matchers/DfaNode.cs @@ -16,7 +16,8 @@ namespace Microsoft.AspNetCore.Routing.Matchers public DfaNode() { Literals = new Dictionary(StringComparer.OrdinalIgnoreCase); - Matches = new List(); + Matches = new List(); + PolicyEdges = new Dictionary(); } // The depth of the node. The depth indicates the number of segments @@ -25,8 +26,8 @@ namespace Microsoft.AspNetCore.Routing.Matchers // Just for diagnostics and debugging public string Label { get; set; } - - public List Matches { get; } + + public List Matches { get; } public Dictionary Literals { get; } @@ -34,6 +35,37 @@ namespace Microsoft.AspNetCore.Routing.Matchers public DfaNode CatchAll { get; set; } + public INodeBuilderPolicy NodeBuilder { get; set; } + + public Dictionary PolicyEdges { get; } + + public void Visit(Action visitor) + { + foreach (var kvp in Literals) + { + kvp.Value.Visit(visitor); + } + + // Break cycles + if (Parameters != null && !ReferenceEquals(this, Parameters)) + { + Parameters.Visit(visitor); + } + + // Break cycles + if (CatchAll != null && !ReferenceEquals(this, CatchAll)) + { + CatchAll.Visit(visitor); + } + + foreach (var kvp in PolicyEdges) + { + kvp.Value.Visit(visitor); + } + + visitor(this); + } + private string DebuggerToString() { var builder = new StringBuilder(); diff --git a/src/Microsoft.AspNetCore.Routing/Matchers/DfaState.cs b/src/Microsoft.AspNetCore.Routing/Matchers/DfaState.cs index cdab988567..a854fdbc6f 100644 --- a/src/Microsoft.AspNetCore.Routing/Matchers/DfaState.cs +++ b/src/Microsoft.AspNetCore.Routing/Matchers/DfaState.cs @@ -8,18 +8,23 @@ namespace Microsoft.AspNetCore.Routing.Matchers [DebuggerDisplay("{DebuggerToString(),nq}")] internal readonly struct DfaState { - public readonly CandidateSet Candidates; - public readonly JumpTable Transitions; + public readonly Candidate[] Candidates; + public readonly JumpTable PathTransitions; + public readonly PolicyJumpTable PolicyTransitions; - public DfaState(CandidateSet candidates, JumpTable transitions) + public DfaState(Candidate[] candidates, JumpTable pathTransitions, PolicyJumpTable policyTransitions) { Candidates = candidates; - Transitions = transitions; + PathTransitions = pathTransitions; + PolicyTransitions = policyTransitions; } public string DebuggerToString() { - return $"m: {Candidates.Candidates?.Length ?? 0}, j: ({Transitions?.DebuggerToString()})"; + return + $"matches: {Candidates?.Length ?? 0}, " + + $"path: ({PathTransitions?.DebuggerToString()}), " + + $"policy: ({PolicyTransitions?.DebuggerToString()})"; } } } diff --git a/src/Microsoft.AspNetCore.Routing/Matchers/EndpointMetadataComparer.cs b/src/Microsoft.AspNetCore.Routing/Matchers/EndpointMetadataComparer.cs new file mode 100644 index 0000000000..7a742fecba --- /dev/null +++ b/src/Microsoft.AspNetCore.Routing/Matchers/EndpointMetadataComparer.cs @@ -0,0 +1,59 @@ +// 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.AspNetCore.Routing.Matchers +{ + public abstract class EndpointMetadataComparer : IComparer where TMetadata : class + { + public static readonly EndpointMetadataComparer Default = new DefaultComparer(); + + public int Compare(Endpoint x, Endpoint y) + { + if (x == null) + { + throw new ArgumentNullException(nameof(x)); + } + + if (y == null) + { + throw new ArgumentNullException(nameof(y)); + } + + return CompareMetadata(GetMetadata(x), GetMetadata(y)); + } + + protected virtual TMetadata GetMetadata(Endpoint endpoint) + { + return endpoint.Metadata.GetMetadata(); + } + + protected virtual int CompareMetadata(TMetadata x, TMetadata y) + { + // The default policy is that if x endpoint defines TMetadata, and + // y endpoint does not, then x is *more specific* than y. We return + // -1 for this case so that x will come first in the sort order. + + if (x == null && y != null) + { + // y is more specific + return 1; + } + else if (x != null && y == null) + { + // x is more specific + return -1; + } + + // both endpoints have this metadata, or both do not have it, they have + // the same specificity. + return 0; + } + + private class DefaultComparer : EndpointMetadataComparer where T : class + { + } + } +} diff --git a/src/Microsoft.AspNetCore.Routing/Matchers/EndpointSelector.cs b/src/Microsoft.AspNetCore.Routing/Matchers/EndpointSelector.cs new file mode 100644 index 0000000000..6f8e420a59 --- /dev/null +++ b/src/Microsoft.AspNetCore.Routing/Matchers/EndpointSelector.cs @@ -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.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Routing.Matchers +{ + public abstract class EndpointSelector + { + public abstract Task SelectAsync( + HttpContext httpContext, + IEndpointFeature feature, + CandidateSet candidates); + } +} diff --git a/src/Microsoft.AspNetCore.Routing/Matchers/HttpMethodMatcherPolicy.cs b/src/Microsoft.AspNetCore.Routing/Matchers/HttpMethodMatcherPolicy.cs new file mode 100644 index 0000000000..17802e61c0 --- /dev/null +++ b/src/Microsoft.AspNetCore.Routing/Matchers/HttpMethodMatcherPolicy.cs @@ -0,0 +1,182 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing.Metadata; +using Microsoft.AspNetCore.Routing.Patterns; + +namespace Microsoft.AspNetCore.Routing.Matchers +{ + public sealed class HttpMethodEndpointSelectorPolicy : MatcherPolicy, IEndpointComparerPolicy, INodeBuilderPolicy + { + // Used in tests + internal const string Http405EndpointDisplayName = "405 HTTP Method Not Supported"; + + // Used in tests + internal const string AnyMethod = "*"; + + public IComparer Comparer => new HttpMethodMetadataEndpointComparer(); + + // The order value is chosen to be less than 0, so that it comes before naively + // written policies. + public override int Order => -1000; + + public bool AppliesToNode(IReadOnlyList endpoints) + { + if (endpoints == null) + { + throw new ArgumentNullException(nameof(endpoints)); + } + + for (var i = 0; i < endpoints.Count; i++) + { + if (endpoints[i].Metadata.GetMetadata()?.HttpMethods.Any() == true) + { + return true; + } + } + + return false; + } + + public IReadOnlyList GetEdges(IReadOnlyList endpoints) + { + // The algorithm here is designed to be preserve the order of the endpoints + // while also being relatively simple. Preserving order is important. + var allHttpMethods = endpoints + .SelectMany(e => GetHttpMethods(e)) + .Distinct() + .OrderBy(m => m); // Sort for testability + + var dictionary = new Dictionary>(); + foreach (var httpMethod in allHttpMethods) + { + dictionary.Add(httpMethod, new List()); + } + + dictionary.Add(AnyMethod, new List()); + + for (var i = 0; i < endpoints.Count; i++) + { + var endpoint = endpoints[i]; + + var httpMethods = GetHttpMethods(endpoint); + if (httpMethods.Count == 0) + { + // This endpoint suports all HTTP methods. + foreach (var kvp in dictionary) + { + kvp.Value.Add(endpoint); + } + + continue; + } + + for (var j = 0; j < httpMethods.Count; j++) + { + dictionary[httpMethods[j]].Add(endpoint); + } + } + + // Adds a very low priority endpoint that will reject the request with + // a 405 if nothing else can handle this verb. This is only done if + // no other actions exist that handle the 'all verbs'. + // + // The rationale for this is that we want to report a 405 if none of + // the supported methods match, but we don't want to report a 405 in a + // case where an application defines an endpoint that handles all verbs, but + // a constraint rejects the request, or a complex segment fails to parse. We + // consider a case like that a 'user input validation' failure rather than + // a semantic violation of HTTP. + // + // This will make 405 much more likely in API-focused applications, and somewhat + // unlikely in a traditional MVC application. That's good. + if (dictionary[AnyMethod].Count == 0) + { + dictionary[AnyMethod].Add(CreateRejectionEndpoint(allHttpMethods)); + } + + var edges = new List(); + foreach (var kvp in dictionary) + { + edges.Add(new PolicyNodeEdge(kvp.Key, kvp.Value)); + } + + return edges; + + IReadOnlyList GetHttpMethods(Endpoint e) + { + return e.Metadata.GetMetadata()?.HttpMethods ?? Array.Empty(); + } + } + + public PolicyJumpTable BuildJumpTable(int exitDestination, IReadOnlyList edges) + { + var dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); + for (var i = 0; i < edges.Count; i++) + { + // We create this data, so it's safe to cast it to a string. + dictionary.Add((string)edges[i].State, edges[i].Destination); + } + + if (dictionary.TryGetValue(AnyMethod, out var matchesAnyVerb)) + { + // If we have endpoints that match any HTTP method, use that as the exit. + exitDestination = matchesAnyVerb; + dictionary.Remove(AnyMethod); + } + + return new DictionaryPolicyJumpTable(exitDestination, dictionary); + } + + private Endpoint CreateRejectionEndpoint(IEnumerable httpMethods) + { + var allow = string.Join(", ", httpMethods); + return new MatcherEndpoint( + (next) => (context) => + { + context.Response.StatusCode = 405; + context.Response.Headers.Add("Allow", allow); + return Task.CompletedTask; + }, + RoutePatternFactory.Parse("/"), + new RouteValueDictionary(), + 0, + EndpointMetadataCollection.Empty, + Http405EndpointDisplayName); + } + + private class DictionaryPolicyJumpTable : PolicyJumpTable + { + private readonly int _exitDestination; + private readonly Dictionary _destinations; + + public DictionaryPolicyJumpTable(int exitDestination, Dictionary destinations) + { + _exitDestination = exitDestination; + _destinations = destinations; + } + + public override int GetDestination(HttpContext httpContext) + { + var httpMethod = httpContext.Request.Method; + return _destinations.TryGetValue(httpMethod, out var destination) ? destination : _exitDestination; + } + } + + private class HttpMethodMetadataEndpointComparer : EndpointMetadataComparer + { + protected override int CompareMetadata(IHttpMethodMetadata x, IHttpMethodMetadata y) + { + // Ignore the metadata if it has an empty list of HTTP methods. + return base.CompareMetadata( + x?.HttpMethods.Count > 0 ? x : null, + y?.HttpMethods.Count > 0 ? y : null); + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Routing/Matchers/IEndpointComparerPolicy.cs b/src/Microsoft.AspNetCore.Routing/Matchers/IEndpointComparerPolicy.cs new file mode 100644 index 0000000000..abd16e5e9a --- /dev/null +++ b/src/Microsoft.AspNetCore.Routing/Matchers/IEndpointComparerPolicy.cs @@ -0,0 +1,12 @@ +// 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.AspNetCore.Routing.Matchers +{ + public interface IEndpointComparerPolicy + { + IComparer Comparer { get; } + } +} diff --git a/src/Microsoft.AspNetCore.Routing/Matchers/INodeBuilderPolicy.cs b/src/Microsoft.AspNetCore.Routing/Matchers/INodeBuilderPolicy.cs new file mode 100644 index 0000000000..5dab03ceb8 --- /dev/null +++ b/src/Microsoft.AspNetCore.Routing/Matchers/INodeBuilderPolicy.cs @@ -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.AspNetCore.Routing.Matchers +{ + public interface INodeBuilderPolicy + { + bool AppliesToNode(IReadOnlyList endpoints); + + IReadOnlyList GetEdges(IReadOnlyList endpoints); + + PolicyJumpTable BuildJumpTable(int exitDestination, IReadOnlyList edges); + } +} diff --git a/src/Microsoft.AspNetCore.Routing/Matchers/MatcherBuilderEntry.cs b/src/Microsoft.AspNetCore.Routing/Matchers/MatcherBuilderEntry.cs deleted file mode 100644 index d1aed77693..0000000000 --- a/src/Microsoft.AspNetCore.Routing/Matchers/MatcherBuilderEntry.cs +++ /dev/null @@ -1,49 +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; -using Microsoft.AspNetCore.Routing.Patterns; -using Microsoft.AspNetCore.Routing.Template; - -namespace Microsoft.AspNetCore.Routing.Matchers -{ - internal class MatcherBuilderEntry : IComparable - { - public MatcherBuilderEntry(MatcherEndpoint endpoint) - { - Endpoint = endpoint; - - Precedence = RoutePrecedence.ComputeInbound(endpoint.RoutePattern); - } - - public MatcherEndpoint Endpoint { get; } - - public int Order => Endpoint.Order; - - public RoutePattern RoutePattern => Endpoint.RoutePattern; - - public decimal Precedence { get; } - - public int CompareTo(MatcherBuilderEntry other) - { - var comparison = Order.CompareTo(other.Order); - if (comparison != 0) - { - return comparison; - } - - comparison = Precedence.CompareTo(other.Precedence); - if (comparison != 0) - { - return comparison; - } - - return RoutePattern.RawText.CompareTo(other.RoutePattern.RawText); - } - - public bool PriorityEquals(MatcherBuilderEntry other) - { - return Order == other.Order && Precedence == other.Precedence; - } - } -} diff --git a/src/Microsoft.AspNetCore.Routing/Matchers/MatcherEndpointComparer.cs b/src/Microsoft.AspNetCore.Routing/Matchers/MatcherEndpointComparer.cs new file mode 100644 index 0000000000..fb5f087297 --- /dev/null +++ b/src/Microsoft.AspNetCore.Routing/Matchers/MatcherEndpointComparer.cs @@ -0,0 +1,103 @@ +// 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; + +namespace Microsoft.AspNetCore.Routing.Matchers +{ + // Use to sort and group MatcherEndpoints. + // + // NOTE: + // When ordering endpoints, we compare the route templates as an absolute last resort. + // This is used as a factor to ensure that we always have a predictable ordering + // for tests, errors, etc. + // + // When we group endpoints we don't consider the route template, because we're trying + // to group endpoints not separate them. + // + // TLDR: + // IComparer implementation considers the template string as a tiebreaker. + // IEqualityComparer implementation does not. + // This is cool and good. + internal class MatcherEndpointComparer : IComparer, IEqualityComparer + { + private readonly IComparer[] _comparers; + + public MatcherEndpointComparer(IEndpointComparerPolicy[] policies) + { + // Order, Precedence, (others)... + _comparers = new IComparer[2 + policies.Length]; + _comparers[0] = OrderComparer.Instance; + _comparers[1] = PrecedenceComparer.Instance; + for (var i = 0; i < policies.Length; i++) + { + _comparers[i + 2] = policies[i].Comparer; + } + } + + public int Compare(MatcherEndpoint x, MatcherEndpoint y) + { + // We don't expose this publicly, and we should never call it on + // a null endpoint. + Debug.Assert(x != null); + Debug.Assert(y != null); + + var compare = CompareCore(x, y); + + // Since we're sorting, use the route template as a last resort. + return compare == 0 ? x.RoutePattern.RawText.CompareTo(y.RoutePattern.RawText) : compare; + } + + public bool Equals(MatcherEndpoint x, MatcherEndpoint y) + { + // We don't expose this publicly, and we should never call it on + // a null endpoint. + Debug.Assert(x != null); + Debug.Assert(y != null); + + return CompareCore(x, y) == 0; + } + + public int GetHashCode(MatcherEndpoint obj) + { + // This should not be possible to call publicly. + Debug.Fail("We don't expect this to be called."); + throw new System.NotImplementedException(); + } + + private int CompareCore(MatcherEndpoint x, MatcherEndpoint y) + { + for (var i = 0; i < _comparers.Length; i++) + { + var compare = _comparers[i].Compare(x, y); + if (compare != 0) + { + return compare; + } + } + + return 0; + } + + private class OrderComparer : IComparer + { + public static readonly IComparer Instance = new OrderComparer(); + + public int Compare(MatcherEndpoint x, MatcherEndpoint y) + { + return x.Order.CompareTo(y.Order); + } + } + + private class PrecedenceComparer : IComparer + { + public static readonly IComparer Instance = new PrecedenceComparer(); + + public int Compare(MatcherEndpoint x, MatcherEndpoint y) + { + return x.RoutePattern.InboundPrecedence.CompareTo(y.RoutePattern.InboundPrecedence); + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Routing/Matchers/MatcherPolicy.cs b/src/Microsoft.AspNetCore.Routing/Matchers/MatcherPolicy.cs new file mode 100644 index 0000000000..92d715c936 --- /dev/null +++ b/src/Microsoft.AspNetCore.Routing/Matchers/MatcherPolicy.cs @@ -0,0 +1,10 @@ +// 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.AspNetCore.Routing +{ + public abstract class MatcherPolicy + { + public abstract int Order { get; } + } +} diff --git a/src/Microsoft.AspNetCore.Routing/Matchers/PolicyJumpTable.cs b/src/Microsoft.AspNetCore.Routing/Matchers/PolicyJumpTable.cs new file mode 100644 index 0000000000..0aecc7de6c --- /dev/null +++ b/src/Microsoft.AspNetCore.Routing/Matchers/PolicyJumpTable.cs @@ -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. + +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Routing.Matchers +{ + public abstract class PolicyJumpTable + { + public abstract int GetDestination(HttpContext httpContext); + + internal virtual string DebuggerToString() + { + return GetType().Name; + } + } +} diff --git a/src/Microsoft.AspNetCore.Routing/Matchers/PolicyJumpTableBuilder.cs b/src/Microsoft.AspNetCore.Routing/Matchers/PolicyJumpTableBuilder.cs new file mode 100644 index 0000000000..c4e07b1715 --- /dev/null +++ b/src/Microsoft.AspNetCore.Routing/Matchers/PolicyJumpTableBuilder.cs @@ -0,0 +1,32 @@ +// 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.AspNetCore.Routing.Matchers +{ + internal class PolicyJumpTableBuilder + { + private readonly INodeBuilderPolicy _nodeBuilder; + private readonly List _entries; + + public PolicyJumpTableBuilder(INodeBuilderPolicy nodeBuilder) + { + _nodeBuilder = nodeBuilder; + _entries = new List(); + } + + // The destination state for a non-match. + public int ExitDestination { get; set; } = JumpTableBuilder.InvalidDestination; + + public void AddEntry(object state, int destination) + { + _entries.Add(new PolicyJumpTableEdge(state, destination)); + } + + public PolicyJumpTable Build() + { + return _nodeBuilder.BuildJumpTable(ExitDestination, _entries); + } + } +} diff --git a/src/Microsoft.AspNetCore.Routing/Matchers/PolicyJumpTableEdge.cs b/src/Microsoft.AspNetCore.Routing/Matchers/PolicyJumpTableEdge.cs new file mode 100644 index 0000000000..62482bb22c --- /dev/null +++ b/src/Microsoft.AspNetCore.Routing/Matchers/PolicyJumpTableEdge.cs @@ -0,0 +1,18 @@ +// 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.AspNetCore.Routing.Matchers +{ + public readonly struct PolicyJumpTableEdge + { + public PolicyJumpTableEdge(object state, int destination) + { + State = state ?? throw new System.ArgumentNullException(nameof(state)); + Destination = destination; + } + + public object State { get; } + + public int Destination { get; } + } +} diff --git a/src/Microsoft.AspNetCore.Routing/Matchers/PolicyNodeEdge.cs b/src/Microsoft.AspNetCore.Routing/Matchers/PolicyNodeEdge.cs new file mode 100644 index 0000000000..bde491fa94 --- /dev/null +++ b/src/Microsoft.AspNetCore.Routing/Matchers/PolicyNodeEdge.cs @@ -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.AspNetCore.Routing.Matchers +{ + public readonly struct PolicyNodeEdge + { + public PolicyNodeEdge(object state, IReadOnlyList endpoints) + { + State = state ?? throw new System.ArgumentNullException(nameof(state)); + Endpoints = endpoints ?? throw new System.ArgumentNullException(nameof(endpoints)); + } + + public IReadOnlyList Endpoints { get; } + + public object State { get; } + } +} diff --git a/src/Microsoft.AspNetCore.Routing/Metadata/HttpMethodMetadata.cs b/src/Microsoft.AspNetCore.Routing/Metadata/HttpMethodMetadata.cs new file mode 100644 index 0000000000..90cfd057a3 --- /dev/null +++ b/src/Microsoft.AspNetCore.Routing/Metadata/HttpMethodMetadata.cs @@ -0,0 +1,24 @@ +// 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.AspNetCore.Routing.Metadata +{ + public sealed class HttpMethodMetadata : IHttpMethodMetadata + { + public HttpMethodMetadata(IEnumerable httpMethods) + { + if (httpMethods == null) + { + throw new ArgumentNullException(nameof(httpMethods)); + } + + HttpMethods = httpMethods.ToArray(); + } + + public IReadOnlyList HttpMethods { get; } + } +} diff --git a/src/Microsoft.AspNetCore.Routing/Metadata/IHttpMethodMetadata.cs b/src/Microsoft.AspNetCore.Routing/Metadata/IHttpMethodMetadata.cs new file mode 100644 index 0000000000..a77a617f13 --- /dev/null +++ b/src/Microsoft.AspNetCore.Routing/Metadata/IHttpMethodMetadata.cs @@ -0,0 +1,12 @@ +// 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.AspNetCore.Routing.Metadata +{ + public interface IHttpMethodMetadata + { + IReadOnlyList HttpMethods { get; } + } +} diff --git a/src/Microsoft.AspNetCore.Routing/Patterns/RoutePattern.cs b/src/Microsoft.AspNetCore.Routing/Patterns/RoutePattern.cs index 74757849c9..bc1551367b 100644 --- a/src/Microsoft.AspNetCore.Routing/Patterns/RoutePattern.cs +++ b/src/Microsoft.AspNetCore.Routing/Patterns/RoutePattern.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using Microsoft.AspNetCore.Routing.Template; namespace Microsoft.AspNetCore.Routing.Patterns { @@ -30,12 +31,19 @@ namespace Microsoft.AspNetCore.Routing.Patterns Constraints = constraints; Parameters = parameters; PathSegments = pathSegments; + + InboundPrecedence = RoutePrecedence.ComputeInbound(this); + OutboundPrecedence = RoutePrecedence.ComputeOutbound(this); } public IReadOnlyDictionary Defaults { get; } public IReadOnlyDictionary> Constraints { get; } + public decimal InboundPrecedence { get; } + + public decimal OutboundPrecedence { get; } + public string RawText { get; } public IReadOnlyList Parameters { get; } diff --git a/test/Microsoft.AspNetCore.Routing.Tests/EndpointConstraints/EndpointConstraintEndpointSelectorTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/EndpointConstraints/EndpointConstraintEndpointSelectorTest.cs new file mode 100644 index 0000000000..fe5d9bb3b2 --- /dev/null +++ b/test/Microsoft.AspNetCore.Routing.Tests/EndpointConstraints/EndpointConstraintEndpointSelectorTest.cs @@ -0,0 +1,510 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing.Matchers; +using Microsoft.AspNetCore.Routing.Patterns; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Logging.Testing; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Routing.EndpointConstraints +{ + public class EndpointConstraintEndpointSelectorTest + { + [Fact] + public async Task SelectBestCandidate_MultipleEndpoints_BestMatchSelected() + { + // Arrange + var defaultEndpoint = CreateEndpoint("No constraint endpoint"); + + var postEndpoint = CreateEndpoint( + "POST constraint endpoint", + new HttpMethodEndpointConstraint(new[] { "POST" })); + + var endpoints = new[] + { + defaultEndpoint, + postEndpoint + }; + + var selector = CreateSelector(endpoints); + + var httpContext = new DefaultHttpContext(); + httpContext.Request.Method = "POST"; + + var feature = new EndpointFeature(); + + // Act + await selector.SelectAsync(httpContext, feature, CreateCandidateSet(endpoints)); + + // Assert + Assert.Same(postEndpoint, feature.Endpoint); + } + + [Fact] + public async Task SelectBestCandidate_MultipleEndpoints_AmbiguousMatchExceptionThrown() + { + // Arrange + var expectedMessage = + "The request matched multiple endpoints. Matches: " + Environment.NewLine + + Environment.NewLine + + "Ambiguous1" + Environment.NewLine + + "Ambiguous2"; + + var defaultEndpoint1 = CreateEndpoint("Ambiguous1"); + var defaultEndpoint2 = CreateEndpoint("Ambiguous2"); + + var endpoints = new[] + { + defaultEndpoint1, + defaultEndpoint2 + }; + + var selector = CreateSelector(endpoints); + + var httpContext = new DefaultHttpContext(); + httpContext.Request.Method = "POST"; + + var feature = new EndpointFeature(); + + // Act + var ex = await Assert.ThrowsAnyAsync(() => + { + return selector.SelectAsync(httpContext, feature, CreateCandidateSet(endpoints)); + }); + + // Assert + Assert.Equal(expectedMessage, ex.Message); + } + + [Fact] + public async Task SelectBestCandidate_AmbiguousEndpoints_LogIsCorrect() + { + // Arrange + var sink = new TestSink(); + var loggerFactory = new TestLoggerFactory(sink, enabled: true); + + var endpoints = new[] + { + CreateEndpoint("A1"), + CreateEndpoint("A2"), + }; + + var selector = CreateSelector(endpoints, loggerFactory); + + var httpContext = CreateHttpContext("POST"); + var feature = new EndpointFeature(); + + var names = string.Join(", ", endpoints.Select(action => action.DisplayName)); + var expectedMessage = + $"Request matched multiple endpoints for request path '/test'. " + + $"Matching endpoints: {names}"; + + // Act + await Assert.ThrowsAsync(() => + { + return selector.SelectAsync(httpContext, feature, CreateCandidateSet(endpoints)); + }); + + // Assert + Assert.Empty(sink.Scopes); + var write = Assert.Single(sink.Writes); + Assert.Equal(expectedMessage, write.State?.ToString()); + } + + [Fact] + public async Task SelectBestCandidate_PrefersEndpointWithConstraints() + { + // Arrange + var endpointWithConstraint = CreateEndpoint( + "Has constraint", + new HttpMethodEndpointConstraint(new string[] { "POST" })); + + var endpointWithoutConstraints = CreateEndpoint("No constraint"); + + var endpoints = new[] { endpointWithConstraint, endpointWithoutConstraints }; + + var selector = CreateSelector(endpoints); + var httpContext = CreateHttpContext("POST"); + var feature = new EndpointFeature(); + + // Act + await selector.SelectAsync(httpContext, feature, CreateCandidateSet(endpoints)); + + // Assert + Assert.Same(endpointWithConstraint, endpointWithConstraint); + } + + [Fact] + public async Task SelectBestCandidate_ConstraintsRejectAll() + { + // Arrange + var endpoint1 = CreateEndpoint( + "action1", + new BooleanConstraint() { Pass = false, }); + + var endpoint2 = CreateEndpoint( + "action2", + new BooleanConstraint() { Pass = false, }); + + var endpoints = new[] { endpoint1, endpoint2 }; + + var selector = CreateSelector(endpoints); + var httpContext = CreateHttpContext("POST"); + var feature = new EndpointFeature(); + + // Act + await selector.SelectAsync(httpContext, feature, CreateCandidateSet(endpoints)); + + // Assert + Assert.Null(feature.Endpoint); + } + + [Fact] + public async Task SelectBestCandidate_ConstraintsRejectAll_DifferentStages() + { + // Arrange + var endpoint1 = CreateEndpoint( + "action1", + new BooleanConstraint() { Pass = false, Order = 0 }, + new BooleanConstraint() { Pass = true, Order = 1 }); + + var endpoint2 = CreateEndpoint( + "action2", + new BooleanConstraint() { Pass = true, Order = 0 }, + new BooleanConstraint() { Pass = false, Order = 1 }); + + var endpoints = new[] { endpoint1, endpoint2 }; + + var selector = CreateSelector(endpoints); + var httpContext = CreateHttpContext("POST"); + var feature = new EndpointFeature(); + + // Act + await selector.SelectAsync(httpContext, feature, CreateCandidateSet(endpoints)); + + // Assert + Assert.Null(feature.Endpoint); + } + + [Fact] + public async Task SelectBestCandidate_EndpointConstraintFactory() + { + // Arrange + var endpointWithConstraints = CreateEndpoint( + "actionWithConstraints", + new ConstraintFactory() + { + Constraint = new BooleanConstraint() { Pass = true }, + }); + + var actionWithoutConstraints = CreateEndpoint("actionWithoutConstraints"); + + var endpoints = new[] { endpointWithConstraints, actionWithoutConstraints }; + + var selector = CreateSelector(endpoints); + var httpContext = CreateHttpContext("POST"); + var feature = new EndpointFeature(); + + // Act + await selector.SelectAsync(httpContext, feature, CreateCandidateSet(endpoints)); + + // Assert + Assert.Same(endpointWithConstraints, feature.Endpoint); + } + + [Fact] + public async Task SelectBestCandidate_MultipleCallsNoConstraint_ReturnsEndpoint() + { + // Arrange + var noConstraint = CreateEndpoint("noConstraint"); + + var endpoints = new[] { noConstraint }; + + var selector = CreateSelector(endpoints); + var httpContext = CreateHttpContext("POST"); + var feature = new EndpointFeature(); + + // Act + await selector.SelectAsync(httpContext, feature, CreateCandidateSet(endpoints)); + var endpoint1 = feature.Endpoint; + + await selector.SelectAsync(httpContext, feature, CreateCandidateSet(endpoints)); + var endpoint2 = feature.Endpoint; + + // Assert + Assert.Same(endpoint1, noConstraint); + Assert.Same(endpoint2, noConstraint); + } + + [Fact] + public async Task SelectBestCandidate_MultipleCallsNonConstraintMetadata_ReturnsEndpoint() + { + // Arrange + var noConstraint = CreateEndpoint("noConstraint", new object()); + + var endpoints = new[] { noConstraint }; + + var selector = CreateSelector(endpoints); + var httpContext = CreateHttpContext("POST"); + var feature = new EndpointFeature(); + + // Act + await selector.SelectAsync(httpContext, feature, CreateCandidateSet(endpoints)); + var endpoint1 = feature.Endpoint; + + await selector.SelectAsync(httpContext, feature, CreateCandidateSet(endpoints)); + var endpoint2 = feature.Endpoint; + + // Assert + Assert.Same(endpoint1, noConstraint); + Assert.Same(endpoint2, noConstraint); + } + + [Fact] + public async Task SelectBestCandidate_EndpointConstraintFactory_ReturnsNull() + { + // Arrange + var nullConstraint = CreateEndpoint("nullConstraint", new ConstraintFactory()); + + var endpoints = new[] { nullConstraint }; + + var selector = CreateSelector(endpoints); + var httpContext = CreateHttpContext("POST"); + var feature = new EndpointFeature(); + + // Act + await selector.SelectAsync(httpContext, feature, CreateCandidateSet(endpoints)); + var endpoint1 = feature.Endpoint; + + await selector.SelectAsync(httpContext, feature, CreateCandidateSet(endpoints)); + var endpoint2 = feature.Endpoint; + + // Assert + Assert.Same(endpoint1, nullConstraint); + Assert.Same(endpoint2, nullConstraint); + } + + // There's a custom constraint provider registered that only understands BooleanConstraintMarker + [Fact] + public async Task SelectBestCandidate_CustomProvider() + { + // Arrange + var endpointWithConstraints = CreateEndpoint( + "actionWithConstraints", + new BooleanConstraintMarker() { Pass = true }); + + var endpointWithoutConstraints = CreateEndpoint("actionWithoutConstraints"); + + var endpoints = new[] { endpointWithConstraints, endpointWithoutConstraints, }; + + var selector = CreateSelector(endpoints); + var httpContext = CreateHttpContext("POST"); + var feature = new EndpointFeature(); + + // Act + await selector.SelectAsync(httpContext, feature, CreateCandidateSet(endpoints)); + + // Assert + Assert.Same(endpointWithConstraints, feature.Endpoint); + } + + // Due to ordering of stages, the first action will be better. + [Fact] + public async Task SelectBestCandidate_ConstraintsInOrder() + { + // Arrange + var best = CreateEndpoint("best", new BooleanConstraint() { Pass = true, Order = 0, }); + + var worst = CreateEndpoint("worst", new BooleanConstraint() { Pass = true, Order = 1, }); + + var endpoints = new[] { best, worst }; + + var selector = CreateSelector(endpoints); + var httpContext = CreateHttpContext("POST"); + var feature = new EndpointFeature(); + + // Act + await selector.SelectAsync(httpContext, feature, CreateCandidateSet(endpoints)); + + // Assert + Assert.Same(best, feature.Endpoint); + } + + // Due to ordering of stages, the first action will be better. + [Fact] + public async Task SelectBestCandidate_ConstraintsInOrder_MultipleStages() + { + // Arrange + var best = CreateEndpoint( + "best", + new BooleanConstraint() { Pass = true, Order = 0, }, + new BooleanConstraint() { Pass = true, Order = 1, }, + new BooleanConstraint() { Pass = true, Order = 2, }); + + var worst = CreateEndpoint( + "worst", + new BooleanConstraint() { Pass = true, Order = 0, }, + new BooleanConstraint() { Pass = true, Order = 1, }, + new BooleanConstraint() { Pass = true, Order = 3, }); + + var endpoints = new[] { best, worst }; + + var selector = CreateSelector(endpoints); + var httpContext = CreateHttpContext("POST"); + var feature = new EndpointFeature(); + + // Act + await selector.SelectAsync(httpContext, feature, CreateCandidateSet(endpoints)); + + // Assert + Assert.Same(best, feature.Endpoint); + } + + [Fact] + public async Task SelectBestCandidate_Fallback_ToEndpointWithoutConstraints() + { + // Arrange + var nomatch1 = CreateEndpoint( + "nomatch1", + new BooleanConstraint() { Pass = true, Order = 0, }, + new BooleanConstraint() { Pass = true, Order = 1, }, + new BooleanConstraint() { Pass = false, Order = 2, }); + + var nomatch2 = CreateEndpoint( + "nomatch2", + new BooleanConstraint() { Pass = true, Order = 0, }, + new BooleanConstraint() { Pass = true, Order = 1, }, + new BooleanConstraint() { Pass = false, Order = 3, }); + + var best = CreateEndpoint("best"); + + var endpoints = new[] { best, nomatch1, nomatch2 }; + + var selector = CreateSelector(endpoints); + var httpContext = CreateHttpContext("POST"); + var feature = new EndpointFeature(); + + // Act + await selector.SelectAsync(httpContext, feature, CreateCandidateSet(endpoints)); + + // Assert + Assert.Same(best, feature.Endpoint); + } + + private static MatcherEndpoint CreateEndpoint(string displayName, params object[] metadata) + { + return new MatcherEndpoint( + MatcherEndpoint.EmptyInvoker, + RoutePatternFactory.Parse("/"), + new RouteValueDictionary(), + 0, + new EndpointMetadataCollection(metadata), + displayName); + } + + private static CandidateSet CreateCandidateSet(MatcherEndpoint[] endpoints) + { + var scores = new int[endpoints.Length]; + return new CandidateSet(endpoints, scores); + } + + private static EndpointSelector CreateSelector(IReadOnlyList actions, ILoggerFactory loggerFactory = null) + { + loggerFactory = loggerFactory ?? NullLoggerFactory.Instance; + + var endpointDataSource = new CompositeEndpointDataSource(new[] { new DefaultEndpointDataSource(actions) }); + + var actionConstraintProviders = new IEndpointConstraintProvider[] { + new DefaultEndpointConstraintProvider(), + new BooleanConstraintProvider(), + }; + + return new EndpointConstraintEndpointSelector( + endpointDataSource, + GetEndpointConstraintCache(actionConstraintProviders), + loggerFactory); + } + + private static HttpContext CreateHttpContext(string httpMethod) + { + var serviceProvider = new ServiceCollection().BuildServiceProvider(); + + var httpContext = new Mock(MockBehavior.Strict); + + var request = new Mock(MockBehavior.Strict); + request.SetupGet(r => r.Method).Returns(httpMethod); + request.SetupGet(r => r.Path).Returns(new PathString("/test")); + request.SetupGet(r => r.Headers).Returns(new HeaderDictionary()); + httpContext.SetupGet(c => c.Request).Returns(request.Object); + httpContext.SetupGet(c => c.RequestServices).Returns(serviceProvider); + + return httpContext.Object; + } + + private static EndpointConstraintCache GetEndpointConstraintCache(IEndpointConstraintProvider[] actionConstraintProviders = null) + { + return new EndpointConstraintCache( + new CompositeEndpointDataSource(Array.Empty()), + actionConstraintProviders.AsEnumerable() ?? new List()); + } + + private class BooleanConstraint : IEndpointConstraint + { + public bool Pass { get; set; } + + public int Order { get; set; } + + public bool Accept(EndpointConstraintContext context) + { + return Pass; + } + } + + private class ConstraintFactory : IEndpointConstraintFactory + { + public IEndpointConstraint Constraint { get; set; } + + public bool IsReusable => true; + + public IEndpointConstraint CreateInstance(IServiceProvider services) + { + return Constraint; + } + } + + private class BooleanConstraintMarker : IEndpointConstraintMetadata + { + public bool Pass { get; set; } + } + + private class BooleanConstraintProvider : IEndpointConstraintProvider + { + public int Order { get; set; } + + public void OnProvidersExecuting(EndpointConstraintProviderContext context) + { + foreach (var item in context.Results) + { + if (item.Metadata is BooleanConstraintMarker marker) + { + Assert.Null(item.Constraint); + item.Constraint = new BooleanConstraint() { Pass = marker.Pass }; + } + } + } + + public void OnProvidersExecuted(EndpointConstraintProviderContext context) + { + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Routing.Tests/EndpointConstraints/EndpointSelectorTests.cs b/test/Microsoft.AspNetCore.Routing.Tests/EndpointConstraints/EndpointSelectorTests.cs deleted file mode 100644 index 008464113e..0000000000 --- a/test/Microsoft.AspNetCore.Routing.Tests/EndpointConstraints/EndpointSelectorTests.cs +++ /dev/null @@ -1,505 +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; -using System.Collections.Generic; -using System.Linq; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Routing.Matchers; -using Microsoft.AspNetCore.Routing.TestObjects; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Logging.Testing; -using Moq; -using Xunit; - -namespace Microsoft.AspNetCore.Routing.EndpointConstraints -{ - public class EndpointSelectorTests - { - [Fact] - public void SelectBestCandidate_MultipleEndpoints_BestMatchSelected() - { - // Arrange - var defaultEndpoint = new TestEndpoint( - EndpointMetadataCollection.Empty, - "No constraint endpoint"); - - var postEndpoint = new TestEndpoint( - new EndpointMetadataCollection(new object[] { new HttpMethodEndpointConstraint(new[] { "POST" }) }), - "POST constraint endpoint"); - - var endpoints = new Endpoint[] - { - defaultEndpoint, - postEndpoint - }; - - var endpointSelector = CreateSelector(endpoints); - - var httpContext = new DefaultHttpContext(); - httpContext.Request.Method = "POST"; - - // Act - var bestCandidateEndpoint = endpointSelector.SelectBestCandidate(httpContext, endpoints); - - // Assert - Assert.NotNull(postEndpoint); - } - - [Fact] - public void SelectBestCandidate_MultipleEndpoints_AmbiguousMatchExceptionThrown() - { - // Arrange - var expectedMessage = - "The request matched multiple endpoints. Matches: " + Environment.NewLine + - Environment.NewLine + - "Ambiguous1" + Environment.NewLine + - "Ambiguous2"; - - var defaultEndpoint1 = new TestEndpoint( - EndpointMetadataCollection.Empty, - "Ambiguous1"); - - var defaultEndpoint2 = new TestEndpoint( - EndpointMetadataCollection.Empty, - "Ambiguous2"); - - var endpoints = new Endpoint[] - { - defaultEndpoint1, - defaultEndpoint2 - }; - - var endpointSelector = CreateSelector(endpoints); - - var httpContext = new DefaultHttpContext(); - httpContext.Request.Method = "POST"; - - // Act - var ex = Assert.ThrowsAny(() => - { - endpointSelector.SelectBestCandidate(httpContext, endpoints); - }); - - // Assert - Assert.Equal(expectedMessage, ex.Message); - } - - [Fact] - public void SelectBestCandidate_AmbiguousEndpoints_LogIsCorrect() - { - // Arrange - var sink = new TestSink(); - var loggerFactory = new TestLoggerFactory(sink, enabled: true); - - var actions = new Endpoint[] - { - new TestEndpoint(EndpointMetadataCollection.Empty, "A1"), - new TestEndpoint(EndpointMetadataCollection.Empty, "A2"), - }; - var selector = CreateSelector(actions, loggerFactory); - - var httpContext = CreateHttpContext("POST"); - var actionNames = string.Join(", ", actions.Select(action => action.DisplayName)); - var expectedMessage = $"Request matched multiple endpoints for request path '/test'. Matching endpoints: {actionNames}"; - - // Act - Assert.Throws(() => { selector.SelectBestCandidate(httpContext, actions); }); - - // Assert - Assert.Empty(sink.Scopes); - var write = Assert.Single(sink.Writes); - Assert.Equal(expectedMessage, write.State?.ToString()); - } - - [Fact] - public void SelectBestCandidate_PrefersEndpointWithConstraints() - { - // Arrange - var actionWithConstraints = new TestEndpoint( - new EndpointMetadataCollection(new[] { new HttpMethodEndpointConstraint(new string[] { "POST" }) }), - "Has constraint"); - - var actionWithoutConstraints = new TestEndpoint( - EndpointMetadataCollection.Empty, - "No constraint"); - - var actions = new Endpoint[] { actionWithConstraints, actionWithoutConstraints }; - - var selector = CreateSelector(actions); - var context = CreateHttpContext("POST"); - - // Act - var action = selector.SelectBestCandidate(context, actions); - - // Assert - Assert.Same(action, actionWithConstraints); - } - - [Fact] - public void SelectBestCandidate_ConstraintsRejectAll() - { - // Arrange - var action1 = new TestEndpoint( - new EndpointMetadataCollection(new[] { new BooleanConstraint() { Pass = false, } }), - "action1"); - - var action2 = new TestEndpoint( - new EndpointMetadataCollection(new[] { new BooleanConstraint() { Pass = false, } }), - "action2"); - - var actions = new Endpoint[] { action1, action2 }; - - var selector = CreateSelector(actions); - var context = CreateHttpContext("POST"); - - // Act - var action = selector.SelectBestCandidate(context, actions); - - // Assert - Assert.Null(action); - } - - [Fact] - public void SelectBestCandidate_ConstraintsRejectAll_DifferentStages() - { - // Arrange - var action1 = new TestEndpoint(new EndpointMetadataCollection(new[] - { - new BooleanConstraint() { Pass = false, Order = 0 }, - new BooleanConstraint() { Pass = true, Order = 1 }, - }), - "action1"); - - var action2 = new TestEndpoint(new EndpointMetadataCollection(new[] - { - new BooleanConstraint() { Pass = true, Order = 0 }, - new BooleanConstraint() { Pass = false, Order = 1 }, - }), - "action2"); - - var actions = new Endpoint[] { action1, action2 }; - - var selector = CreateSelector(actions); - var context = CreateHttpContext("POST"); - - // Act - var action = selector.SelectBestCandidate(context, actions); - - // Assert - Assert.Null(action); - } - - [Fact] - public void SelectBestCandidate_EndpointConstraintFactory() - { - // Arrange - var actionWithConstraints = new TestEndpoint(new EndpointMetadataCollection(new[] - { - new ConstraintFactory() - { - Constraint = new BooleanConstraint() { Pass = true }, - }, - }), - "actionWithConstraints"); - - var actionWithoutConstraints = new TestEndpoint( - EndpointMetadataCollection.Empty, - "actionWithoutConstraints"); - - var actions = new Endpoint[] { actionWithConstraints, actionWithoutConstraints }; - - var selector = CreateSelector(actions); - var context = CreateHttpContext("POST"); - - // Act - var action = selector.SelectBestCandidate(context, actions); - - // Assert - Assert.Same(action, actionWithConstraints); - } - - [Fact] - public void SelectBestCandidate_MultipleCallsNoConstraint_ReturnsEndpoint() - { - // Arrange - var noConstraint = new TestEndpoint(EndpointMetadataCollection.Empty, "noConstraint"); - - var actions = new Endpoint[] { noConstraint }; - - var selector = CreateSelector(actions); - var context = CreateHttpContext("POST"); - - // Act - var action1 = selector.SelectBestCandidate(context, actions); - var action2 = selector.SelectBestCandidate(context, actions); - - // Assert - Assert.Same(action1, noConstraint); - Assert.Same(action2, noConstraint); - } - - [Fact] - public void SelectBestCandidate_MultipleCallsNonConstraintMetadata_ReturnsEndpoint() - { - // Arrange - var noConstraint = new TestEndpoint(new EndpointMetadataCollection(new[] - { - new object(), - }), - "noConstraint"); - - var actions = new Endpoint[] { noConstraint }; - - var selector = CreateSelector(actions); - var context = CreateHttpContext("POST"); - - // Act - var action1 = selector.SelectBestCandidate(context, actions); - var action2 = selector.SelectBestCandidate(context, actions); - - // Assert - Assert.Same(action1, noConstraint); - Assert.Same(action2, noConstraint); - } - - [Fact] - public void SelectBestCandidate_EndpointConstraintFactory_ReturnsNull() - { - // Arrange - var nullConstraint = new TestEndpoint(new EndpointMetadataCollection(new[] - { - new ConstraintFactory(), - }), - "nullConstraint"); - - var actions = new Endpoint[] { nullConstraint }; - - var selector = CreateSelector(actions); - var context = CreateHttpContext("POST"); - - // Act - var action1 = selector.SelectBestCandidate(context, actions); - var action2 = selector.SelectBestCandidate(context, actions); - - // Assert - Assert.Same(action1, nullConstraint); - Assert.Same(action2, nullConstraint); - } - - // There's a custom constraint provider registered that only understands BooleanConstraintMarker - [Fact] - public void SelectBestCandidate_CustomProvider() - { - // Arrange - var actionWithConstraints = new TestEndpoint(new EndpointMetadataCollection(new[] - { - new BooleanConstraintMarker() { Pass = true }, - }), - "actionWithConstraints"); - - var actionWithoutConstraints = new TestEndpoint( - EndpointMetadataCollection.Empty, - "actionWithoutConstraints"); - - var actions = new Endpoint[] { actionWithConstraints, actionWithoutConstraints, }; - - var selector = CreateSelector(actions); - var context = CreateHttpContext("POST"); - - // Act - var action = selector.SelectBestCandidate(context, actions); - - // Assert - Assert.Same(action, actionWithConstraints); - } - - // Due to ordering of stages, the first action will be better. - [Fact] - public void SelectBestCandidate_ConstraintsInOrder() - { - // Arrange - var best = new TestEndpoint(new EndpointMetadataCollection(new[] - { - new BooleanConstraint() { Pass = true, Order = 0, }, - }), - "best"); - - var worst = new TestEndpoint(new EndpointMetadataCollection(new[] - { - new BooleanConstraint() { Pass = true, Order = 1, }, - }), - "worst"); - - var actions = new Endpoint[] { best, worst }; - - var selector = CreateSelector(actions); - var context = CreateHttpContext("POST"); - - // Act - var action = selector.SelectBestCandidate(context, actions); - - // Assert - Assert.Same(action, best); - } - - // Due to ordering of stages, the first action will be better. - [Fact] - public void SelectBestCandidate_ConstraintsInOrder_MultipleStages() - { - // Arrange - var best = new TestEndpoint(new EndpointMetadataCollection(new[] - { - new BooleanConstraint() { Pass = true, Order = 0, }, - new BooleanConstraint() { Pass = true, Order = 1, }, - new BooleanConstraint() { Pass = true, Order = 2, }, - }), - "best"); - - var worst = new TestEndpoint(new EndpointMetadataCollection(new[] - { - new BooleanConstraint() { Pass = true, Order = 0, }, - new BooleanConstraint() { Pass = true, Order = 1, }, - new BooleanConstraint() { Pass = true, Order = 3, }, - }), - "worst"); - - var actions = new Endpoint[] { best, worst }; - - var selector = CreateSelector(actions); - var context = CreateHttpContext("POST"); - - // Act - var action = selector.SelectBestCandidate(context, actions); - - // Assert - Assert.Same(action, best); - } - - [Fact] - public void SelectBestCandidate_Fallback_ToEndpointWithoutConstraints() - { - // Arrange - var nomatch1 = new TestEndpoint(new EndpointMetadataCollection(new[] - { - new BooleanConstraint() { Pass = true, Order = 0, }, - new BooleanConstraint() { Pass = true, Order = 1, }, - new BooleanConstraint() { Pass = false, Order = 2, }, - }), - "nomatch1"); - - var nomatch2 = new TestEndpoint(new EndpointMetadataCollection(new[] - { - new BooleanConstraint() { Pass = true, Order = 0, }, - new BooleanConstraint() { Pass = true, Order = 1, }, - new BooleanConstraint() { Pass = false, Order = 3, }, - }), - "nomatch2"); - - var best = new TestEndpoint(EndpointMetadataCollection.Empty, "best"); - - var actions = new Endpoint[] { best, nomatch1, nomatch2 }; - - var selector = CreateSelector(actions); - var context = CreateHttpContext("POST"); - - // Act - var action = selector.SelectBestCandidate(context, actions); - - // Assert - Assert.Same(action, best); - } - - private static EndpointSelector CreateSelector(IReadOnlyList actions, ILoggerFactory loggerFactory = null) - { - loggerFactory = loggerFactory ?? NullLoggerFactory.Instance; - - var endpointDataSource = new CompositeEndpointDataSource(new[] { new DefaultEndpointDataSource(actions) }); - - var actionConstraintProviders = new IEndpointConstraintProvider[] { - new DefaultEndpointConstraintProvider(), - new BooleanConstraintProvider(), - }; - - return new EndpointSelector( - endpointDataSource, - GetEndpointConstraintCache(actionConstraintProviders), - loggerFactory); - } - - private static HttpContext CreateHttpContext(string httpMethod) - { - var serviceProvider = new ServiceCollection().BuildServiceProvider(); - - var httpContext = new Mock(MockBehavior.Strict); - - var request = new Mock(MockBehavior.Strict); - request.SetupGet(r => r.Method).Returns(httpMethod); - request.SetupGet(r => r.Path).Returns(new PathString("/test")); - request.SetupGet(r => r.Headers).Returns(new HeaderDictionary()); - httpContext.SetupGet(c => c.Request).Returns(request.Object); - httpContext.SetupGet(c => c.RequestServices).Returns(serviceProvider); - - return httpContext.Object; - } - - private static EndpointConstraintCache GetEndpointConstraintCache(IEndpointConstraintProvider[] actionConstraintProviders = null) - { - return new EndpointConstraintCache( - new CompositeEndpointDataSource(Array.Empty()), - actionConstraintProviders.AsEnumerable() ?? new List()); - } - - private class BooleanConstraint : IEndpointConstraint - { - public bool Pass { get; set; } - - public int Order { get; set; } - - public bool Accept(EndpointConstraintContext context) - { - return Pass; - } - } - - private class ConstraintFactory : IEndpointConstraintFactory - { - public IEndpointConstraint Constraint { get; set; } - - public bool IsReusable => true; - - public IEndpointConstraint CreateInstance(IServiceProvider services) - { - return Constraint; - } - } - - private class BooleanConstraintMarker : IEndpointConstraintMetadata - { - public bool Pass { get; set; } - } - - private class BooleanConstraintProvider : IEndpointConstraintProvider - { - public int Order { get; set; } - - public void OnProvidersExecuting(EndpointConstraintProviderContext context) - { - foreach (var item in context.Results) - { - if (item.Metadata is BooleanConstraintMarker marker) - { - Assert.Null(item.Constraint); - item.Constraint = new BooleanConstraint() { Pass = marker.Pass }; - } - } - } - - public void OnProvidersExecuted(EndpointConstraintProviderContext context) - { - } - } - } -} diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Internal/HttpMethodEndpointConstraintTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/Internal/HttpMethodEndpointConstraintTest.cs index 67af016137..53b38fea1a 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/Internal/HttpMethodEndpointConstraintTest.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/Internal/HttpMethodEndpointConstraintTest.cs @@ -63,6 +63,8 @@ namespace Microsoft.AspNetCore.Routing.Internal var endpointSelectorCandidate = new EndpointSelectorCandidate( new TestEndpoint(EndpointMetadataCollection.Empty, string.Empty), + 0, + new RouteValueDictionary(), new List { constraint }); context.Candidates = new List { endpointSelectorCandidate }; diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/BarebonesMatcher.cs b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/BarebonesMatcher.cs index d70cd6eece..1d94447c06 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/BarebonesMatcher.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/BarebonesMatcher.cs @@ -49,18 +49,14 @@ namespace Microsoft.AspNetCore.Routing.Matchers public readonly MatcherEndpoint Endpoint; private readonly string[] _segments; - private readonly CandidateSet _candidates; + private readonly Candidate[] _candidates; public InnerMatcher(string[] segments, MatcherEndpoint endpoint) { _segments = segments; Endpoint = endpoint; - _candidates = new CandidateSet( - new Candidate[] { new Candidate(endpoint), }, - - // Single candidate group that contains one entry. - CandidateSet.MakeGroups(new[] { 1 })); + _candidates = new Candidate[] { new Candidate(endpoint), }; } public bool TryMatch(string path) @@ -114,14 +110,14 @@ namespace Microsoft.AspNetCore.Routing.Matchers return segment == _segments.Length; } - internal CandidateSet SelectCandidates(string path, ReadOnlySpan segments) + internal Candidate[] FindCandidateSet(string path, ReadOnlySpan segments) { if (TryMatch(path)) { return _candidates; } - return CandidateSet.Empty; + return Array.Empty(); } public override Task MatchAsync(HttpContext httpContext, IEndpointFeature feature) diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/CandidateSetTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/CandidateSetTest.cs new file mode 100644 index 0000000000..7b75859884 --- /dev/null +++ b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/CandidateSetTest.cs @@ -0,0 +1,103 @@ +// 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.Linq; +using Microsoft.AspNetCore.Routing.Patterns; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Routing.Matchers +{ + public class CandidateSetTest + { + // We special case low numbers of candidates, so we want to verify that it works correctly for a variety + // of input sizes. + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + [InlineData(4)] + [InlineData(5)] // this is the break-point where we start to use a list. + [InlineData(6)] + public void Create_CreatesCandidateSet(int count) + { + // Arrange + var endpoints = new MatcherEndpoint[count]; + for (var i = 0; i < endpoints.Length; i++) + { + endpoints[i] = CreateEndpoint($"/{i}"); + } + + var builder = CreateDfaMatcherBuilder(); + var candidates = builder.CreateCandidates(endpoints); + + // Act + var candidateSet = new CandidateSet(candidates); + + // Assert + for (var i = 0; i < candidateSet.Count; i++) + { + ref var state = ref candidateSet[i]; + Assert.True(state.IsValidCandidate); + Assert.Same(endpoints[i], state.Endpoint); + Assert.Equal(candidates[i].Score, state.Score); + Assert.Null(state.Values); + } + } + + // We special case low numbers of candidates, so we want to verify that it works correctly for a variety + // of input sizes. + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + [InlineData(4)] + [InlineData(5)] // this is the break-point where we start to use a list. + [InlineData(6)] + public void Create_CreatesCandidateSet_TestConstructor(int count) + { + // Arrange + var endpoints = new MatcherEndpoint[count]; + for (var i = 0; i < endpoints.Length; i++) + { + endpoints[i] = CreateEndpoint($"/{i}"); + } + + // Act + var candidateSet = new CandidateSet(endpoints, Enumerable.Range(0, count).ToArray()); + + // Assert + for (var i = 0; i < candidateSet.Count; i++) + { + ref var state = ref candidateSet[i]; + Assert.True(state.IsValidCandidate); + Assert.Same(endpoints[i], state.Endpoint); + Assert.Equal(i, state.Score); + Assert.Null(state.Values); + } + } + + private MatcherEndpoint CreateEndpoint(string template) + { + return new MatcherEndpoint( + MatcherEndpoint.EmptyInvoker, + RoutePatternFactory.Parse(template), + new RouteValueDictionary(), + 0, + EndpointMetadataCollection.Empty, + "test"); + } + + private static DfaMatcherBuilder CreateDfaMatcherBuilder(params MatcherPolicy[] policies) + { + var dataSource = new CompositeEndpointDataSource(Array.Empty()); + return new DfaMatcherBuilder( + Mock.Of(), + Mock.Of(), + policies); + } + } +} diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/DefaultEndpointSelectorTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/DefaultEndpointSelectorTest.cs new file mode 100644 index 0000000000..13d6ff86e9 --- /dev/null +++ b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/DefaultEndpointSelectorTest.cs @@ -0,0 +1,203 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing.Patterns; +using Xunit; + +namespace Microsoft.AspNetCore.Routing.Matchers +{ + public class DefaultEndpointSelectorTest + { + [Fact] + public async Task SelectAsync_NoCandidates_DoesNothing() + { + // Arrange + var endpoints = new MatcherEndpoint[] { }; + var scores = new int[] { }; + var candidateSet = CreateCandidateSet(endpoints, scores); + + var (httpContext, feature) = CreateContext(); + var selector = CreateSelector(); + + // Act + await selector.SelectAsync(httpContext, feature, candidateSet); + + // Assert + Assert.Null(feature.Endpoint); + } + + [Fact] + public async Task SelectAsync_NoValidCandidates_DoesNothing() + { + // Arrange + var endpoints = new MatcherEndpoint[] { CreateEndpoint("/test"), }; + var scores = new int[] { 0, }; + var candidateSet = CreateCandidateSet(endpoints, scores); + + candidateSet[0].Values = new RouteValueDictionary(); + candidateSet[0].IsValidCandidate = false; + + var (httpContext, feature) = CreateContext(); + var selector = CreateSelector(); + + // Act + await selector.SelectAsync(httpContext, feature, candidateSet); + + // Assert + Assert.Null(feature.Endpoint); + Assert.Null(feature.Values); + } + + [Fact] + public async Task SelectAsync_SingleCandidate_ChoosesCandidate() + { + // Arrange + var endpoints = new MatcherEndpoint[] { CreateEndpoint("/test"), }; + var scores = new int[] { 0, }; + var candidateSet = CreateCandidateSet(endpoints, scores); + + candidateSet[0].Values = new RouteValueDictionary(); + candidateSet[0].IsValidCandidate = true; + + var (httpContext, feature) = CreateContext(); + var selector = CreateSelector(); + + // Act + await selector.SelectAsync(httpContext, feature, candidateSet); + + // Assert + Assert.Same(endpoints[0], feature.Endpoint); + Assert.Same(endpoints[0].Invoker, feature.Invoker); + Assert.NotNull(feature.Values); + } + + [Fact] + public async Task SelectAsync_SingleValidCandidate_ChoosesCandidate() + { + // Arrange + var endpoints = new MatcherEndpoint[] { CreateEndpoint("/test1"), CreateEndpoint("/test2"), }; + var scores = new int[] { 0, 0 }; + var candidateSet = CreateCandidateSet(endpoints, scores); + + candidateSet[0].IsValidCandidate = false; + candidateSet[1].IsValidCandidate = true; + + var (httpContext, feature) = CreateContext(); + var selector = CreateSelector(); + + // Act + await selector.SelectAsync(httpContext, feature, candidateSet); + + // Assert + Assert.Same(endpoints[1], feature.Endpoint); + } + + [Fact] + public async Task SelectAsync_SingleValidCandidateInGroup_ChoosesCandidate() + { + // Arrange + var endpoints = new MatcherEndpoint[] { CreateEndpoint("/test1"), CreateEndpoint("/test2"), CreateEndpoint("/test3"), }; + var scores = new int[] { 0, 0, 1 }; + var candidateSet = CreateCandidateSet(endpoints, scores); + + candidateSet[0].IsValidCandidate = false; + candidateSet[1].IsValidCandidate = true; + candidateSet[2].IsValidCandidate = true; + + var (httpContext, feature) = CreateContext(); + var selector = CreateSelector(); + + // Act + await selector.SelectAsync(httpContext, feature, candidateSet); + + // Assert + Assert.Same(endpoints[1], feature.Endpoint); + } + + [Fact] + public async Task SelectAsync_ManyGroupsLastCandidate_ChoosesCandidate() + { + // Arrange + var endpoints = new MatcherEndpoint[] + { + CreateEndpoint("/test1"), + CreateEndpoint("/test2"), + CreateEndpoint("/test3"), + CreateEndpoint("/test4"), + CreateEndpoint("/test5"), + }; + var scores = new int[] { 0, 1, 2, 3, 4 }; + var candidateSet = CreateCandidateSet(endpoints, scores); + + candidateSet[0].IsValidCandidate = false; + candidateSet[1].IsValidCandidate = false; + candidateSet[2].IsValidCandidate = false; + candidateSet[3].IsValidCandidate = false; + candidateSet[4].IsValidCandidate = true; + + var (httpContext, feature) = CreateContext(); + var selector = CreateSelector(); + + // Act + await selector.SelectAsync(httpContext, feature, candidateSet); + + // Assert + Assert.Same(endpoints[4], feature.Endpoint); + } + + [Fact] + public async Task SelectAsync_MultipleValidCandidatesInGroup_ReportsAmbiguity() + { + // Arrange + var endpoints = new MatcherEndpoint[] { CreateEndpoint("/test1"), CreateEndpoint("/test2"), CreateEndpoint("/test3"), }; + var scores = new int[] { 0, 1, 1 }; + var candidateSet = CreateCandidateSet(endpoints, scores); + + candidateSet[0].IsValidCandidate = false; + candidateSet[1].IsValidCandidate = true; + candidateSet[2].IsValidCandidate = true; + + var (httpContext, feature) = CreateContext(); + var selector = CreateSelector(); + + // Act + var ex = await Assert.ThrowsAsync(() => selector.SelectAsync(httpContext, feature, candidateSet)); + + // Assert + Assert.Equal( +@"The request matched multiple endpoints. Matches: + +test: /test2 +test: /test3", ex.Message); + Assert.Null(feature.Endpoint); + } + + private static (HttpContext httpContext, IEndpointFeature feature) CreateContext() + { + return (new DefaultHttpContext(), new EndpointFeature()); + } + + private static MatcherEndpoint CreateEndpoint(string template) + { + return new MatcherEndpoint( + MatcherEndpoint.EmptyInvoker, + RoutePatternFactory.Parse(template), + new RouteValueDictionary(), + 0, + EndpointMetadataCollection.Empty, + $"test: {template}"); + } + + private static CandidateSet CreateCandidateSet(MatcherEndpoint[] endpoints, int[] scores) + { + return new CandidateSet(endpoints, scores); + } + + private static DefaultEndpointSelector CreateSelector() + { + return new DefaultEndpointSelector(); + } + } +} diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/DfaMatcherBuilderTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/DfaMatcherBuilderTest.cs index b8a064b59a..71318fd943 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/DfaMatcherBuilderTest.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/DfaMatcherBuilderTest.cs @@ -5,9 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using Microsoft.AspNetCore.Routing.Constraints; -using Microsoft.AspNetCore.Routing.EndpointConstraints; using Microsoft.AspNetCore.Routing.Patterns; -using Microsoft.Extensions.Logging.Abstractions; using Moq; using Xunit; @@ -28,7 +26,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers var root = builder.BuildDfaTree(); // Assert - Assert.Same(endpoint, Assert.Single(root.Matches).Endpoint); + Assert.Same(endpoint, Assert.Single(root.Matches)); Assert.Null(root.Parameters); Assert.Empty(root.Literals); } @@ -67,7 +65,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers Assert.Equal("c", next.Key); var c = next.Value; - Assert.Same(endpoint, Assert.Single(c.Matches).Endpoint); + Assert.Same(endpoint, Assert.Single(c.Matches)); Assert.Null(c.Parameters); Assert.Empty(c.Literals); } @@ -97,7 +95,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers Assert.Empty(b.Literals); var c = b.Parameters; - Assert.Same(endpoint, Assert.Single(c.Matches).Endpoint); + Assert.Same(endpoint, Assert.Single(c.Matches)); Assert.Null(c.Parameters); Assert.Empty(c.Literals); } @@ -121,14 +119,14 @@ namespace Microsoft.AspNetCore.Routing.Matchers var a = root.Parameters; // The catch all can match a path like '/a' - Assert.Same(endpoint, Assert.Single(a.Matches).Endpoint); + Assert.Same(endpoint, Assert.Single(a.Matches)); Assert.Empty(a.Literals); Assert.Null(a.Parameters); // Catch-all nodes include an extra transition that loops to process // extra segments. var catchAll = a.CatchAll; - Assert.Same(endpoint, Assert.Single(catchAll.Matches).Endpoint); + Assert.Same(endpoint, Assert.Single(catchAll.Matches)); Assert.Empty(catchAll.Literals); Assert.Same(catchAll, catchAll.Parameters); Assert.Same(catchAll, catchAll.CatchAll); @@ -147,13 +145,13 @@ namespace Microsoft.AspNetCore.Routing.Matchers var root = builder.BuildDfaTree(); // Assert - Assert.Same(endpoint, Assert.Single(root.Matches).Endpoint); + Assert.Same(endpoint, Assert.Single(root.Matches)); Assert.Empty(root.Literals); // Catch-all nodes include an extra transition that loops to process // extra segments. var catchAll = root.CatchAll; - Assert.Same(endpoint, Assert.Single(catchAll.Matches).Endpoint); + Assert.Same(endpoint, Assert.Single(catchAll.Matches)); Assert.Empty(catchAll.Literals); Assert.Same(catchAll, catchAll.Parameters); } @@ -193,7 +191,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers Assert.Equal("c", next.Key); var c1 = next.Value; - Assert.Same(endpoint1, Assert.Single(c1.Matches).Endpoint); + Assert.Same(endpoint1, Assert.Single(c1.Matches)); Assert.Null(c1.Parameters); Assert.Empty(c1.Literals); @@ -205,7 +203,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers Assert.Equal("c", next.Key); var c2 = next.Value; - Assert.Same(endpoint2, Assert.Single(c2.Matches).Endpoint); + Assert.Same(endpoint2, Assert.Single(c2.Matches)); Assert.Null(c2.Parameters); Assert.Empty(c2.Literals); } @@ -248,8 +246,8 @@ namespace Microsoft.AspNetCore.Routing.Matchers var c1 = next.Value; Assert.Collection( c1.Matches, - e => Assert.Same(endpoint1, e.Endpoint), - e => Assert.Same(endpoint2, e.Endpoint)); + e => Assert.Same(endpoint1, e), + e => Assert.Same(endpoint2, e)); Assert.Null(c1.Parameters); Assert.Empty(c1.Literals); @@ -261,7 +259,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers Assert.Equal("c", next.Key); var c2 = next.Value; - Assert.Same(endpoint2, Assert.Single(c2.Matches).Endpoint); + Assert.Same(endpoint2, Assert.Single(c2.Matches)); Assert.Null(c2.Parameters); Assert.Empty(c2.Literals); } @@ -302,8 +300,8 @@ namespace Microsoft.AspNetCore.Routing.Matchers var c = next.Value; Assert.Collection( c.Matches, - e => Assert.Same(endpoint1, e.Endpoint), - e => Assert.Same(endpoint2, e.Endpoint)); + e => Assert.Same(endpoint1, e), + e => Assert.Same(endpoint2, e)); Assert.Null(c.Parameters); Assert.Empty(c.Literals); } @@ -331,13 +329,13 @@ namespace Microsoft.AspNetCore.Routing.Matchers Assert.Equal("a", next.Key); var a = next.Value; - Assert.Same(endpoint2, Assert.Single(a.Matches).Endpoint); + Assert.Same(endpoint2, Assert.Single(a.Matches)); next = Assert.Single(a.Literals); Assert.Equal("b", next.Key); var b1 = next.Value; - Assert.Same(endpoint2, Assert.Single(a.Matches).Endpoint); + Assert.Same(endpoint2, Assert.Single(a.Matches)); Assert.Null(b1.Parameters); next = Assert.Single(b1.Literals); @@ -346,13 +344,13 @@ namespace Microsoft.AspNetCore.Routing.Matchers var c1 = next.Value; Assert.Collection( c1.Matches, - e => Assert.Same(endpoint1, e.Endpoint), - e => Assert.Same(endpoint2, e.Endpoint)); + e => Assert.Same(endpoint1, e), + e => Assert.Same(endpoint2, e)); Assert.Null(c1.Parameters); Assert.Empty(c1.Literals); var catchAll = a.CatchAll; - Assert.Same(endpoint2, Assert.Single(catchAll.Matches).Endpoint); + Assert.Same(endpoint2, Assert.Single(catchAll.Matches)); Assert.Same(catchAll, catchAll.Parameters); Assert.Same(catchAll, catchAll.CatchAll); } @@ -380,11 +378,11 @@ namespace Microsoft.AspNetCore.Routing.Matchers Assert.Equal("a", next.Key); var a = next.Value; - Assert.Same(endpoint2, Assert.Single(a.Matches).Endpoint); + Assert.Same(endpoint2, Assert.Single(a.Matches)); Assert.Empty(a.Literals); var b1 = a.Parameters; - Assert.Same(endpoint2, Assert.Single(a.Matches).Endpoint); + Assert.Same(endpoint2, Assert.Single(a.Matches)); Assert.Null(b1.Parameters); next = Assert.Single(b1.Literals); @@ -393,17 +391,239 @@ namespace Microsoft.AspNetCore.Routing.Matchers var c1 = next.Value; Assert.Collection( c1.Matches, - e => Assert.Same(endpoint1, e.Endpoint), - e => Assert.Same(endpoint2, e.Endpoint)); + e => Assert.Same(endpoint1, e), + e => Assert.Same(endpoint2, e)); Assert.Null(c1.Parameters); Assert.Empty(c1.Literals); var catchAll = a.CatchAll; - Assert.Same(endpoint2, Assert.Single(catchAll.Matches).Endpoint); + Assert.Same(endpoint2, Assert.Single(catchAll.Matches)); Assert.Same(catchAll, catchAll.Parameters); Assert.Same(catchAll, catchAll.CatchAll); } + [Fact] + public void BuildDfaTree_WithPolicies() + { + // Arrange + var builder = CreateDfaMatcherBuilder(new TestMetadata1MatcherPolicy(), new TestMetadata2MatcherPolicy()); + + var endpoint1 = CreateEndpoint("/a", metadata: new object[] { new TestMetadata1(0), new TestMetadata2(true), }); + builder.AddEndpoint(endpoint1); + + // Act + var root = builder.BuildDfaTree(); + + // Assert + Assert.Empty(root.Matches); + Assert.Null(root.Parameters); + + var next = Assert.Single(root.Literals); + Assert.Equal("a", next.Key); + + var a = next.Value; + Assert.Empty(a.Matches); + Assert.IsType(a.NodeBuilder); + Assert.Collection( + a.PolicyEdges.OrderBy(e => e.Key), + e => Assert.Equal(0, e.Key)); + + var test1_0 = a.PolicyEdges[0]; + Assert.Empty(a.Matches); + Assert.IsType(test1_0.NodeBuilder); + Assert.Collection( + test1_0.PolicyEdges.OrderBy(e => e.Key), + e => Assert.Equal(true, e.Key)); + + var test2_true = test1_0.PolicyEdges[true]; + Assert.Same(endpoint1, Assert.Single(test2_true.Matches)); + Assert.Null(test2_true.NodeBuilder); + Assert.Empty(test2_true.PolicyEdges); + } + + [Fact] + public void BuildDfaTree_WithPolicies_AndBranches() + { + // Arrange + var builder = CreateDfaMatcherBuilder(new TestMetadata1MatcherPolicy(), new TestMetadata2MatcherPolicy()); + + var endpoint1 = CreateEndpoint("/a", metadata: new object[] { new TestMetadata1(0), new TestMetadata2(true), }); + builder.AddEndpoint(endpoint1); + + var endpoint2 = CreateEndpoint("/a", metadata: new object[] { new TestMetadata1(1), new TestMetadata2(true), }); + builder.AddEndpoint(endpoint2); + + var endpoint3 = CreateEndpoint("/a", metadata: new object[] { new TestMetadata1(1), new TestMetadata2(false), }); + builder.AddEndpoint(endpoint3); + + // Act + var root = builder.BuildDfaTree(); + + // Assert + Assert.Empty(root.Matches); + Assert.Null(root.Parameters); + + var next = Assert.Single(root.Literals); + Assert.Equal("a", next.Key); + + var a = next.Value; + Assert.Empty(a.Matches); + Assert.IsType(a.NodeBuilder); + Assert.Collection( + a.PolicyEdges.OrderBy(e => e.Key), + e => Assert.Equal(0, e.Key), + e => Assert.Equal(1, e.Key)); + + var test1_0 = a.PolicyEdges[0]; + Assert.Empty(test1_0.Matches); + Assert.IsType(test1_0.NodeBuilder); + Assert.Collection( + test1_0.PolicyEdges.OrderBy(e => e.Key), + e => Assert.Equal(true, e.Key)); + + var test2_true = test1_0.PolicyEdges[true]; + Assert.Same(endpoint1, Assert.Single(test2_true.Matches)); + Assert.Null(test2_true.NodeBuilder); + Assert.Empty(test2_true.PolicyEdges); + + var test1_1 = a.PolicyEdges[1]; + Assert.Empty(test1_1.Matches); + Assert.IsType(test1_1.NodeBuilder); + Assert.Collection( + test1_1.PolicyEdges.OrderBy(e => e.Key), + e => Assert.Equal(false, e.Key), + e => Assert.Equal(true, e.Key)); + + test2_true = test1_1.PolicyEdges[true]; + Assert.Same(endpoint2, Assert.Single(test2_true.Matches)); + Assert.Null(test2_true.NodeBuilder); + Assert.Empty(test2_true.PolicyEdges); + + var test2_false = test1_1.PolicyEdges[false]; + Assert.Same(endpoint3, Assert.Single(test2_false.Matches)); + Assert.Null(test2_false.NodeBuilder); + Assert.Empty(test2_false.PolicyEdges); + } + + [Fact] + public void BuildDfaTree_WithPolicies_AndBranches_FirstPolicySkipped() + { + // Arrange + var builder = CreateDfaMatcherBuilder(new TestMetadata1MatcherPolicy(), new TestMetadata2MatcherPolicy()); + + var endpoint1 = CreateEndpoint("/a", metadata: new object[] { new TestMetadata2(true), }); + builder.AddEndpoint(endpoint1); + + var endpoint2 = CreateEndpoint("/a", metadata: new object[] { new TestMetadata2(true), }); + builder.AddEndpoint(endpoint2); + + var endpoint3 = CreateEndpoint("/a", metadata: new object[] { new TestMetadata2(false), }); + builder.AddEndpoint(endpoint3); + + // Act + var root = builder.BuildDfaTree(); + + // Assert + Assert.Empty(root.Matches); + Assert.Null(root.Parameters); + + var next = Assert.Single(root.Literals); + Assert.Equal("a", next.Key); + + var a = next.Value; + Assert.Empty(a.Matches); + Assert.IsType(a.NodeBuilder); + Assert.Collection( + a.PolicyEdges.OrderBy(e => e.Key), + e => Assert.Equal(false, e.Key), + e => Assert.Equal(true, e.Key)); + + var test2_true = a.PolicyEdges[true]; + Assert.Equal(new[] { endpoint1, endpoint2, }, test2_true.Matches); + Assert.Null(test2_true.NodeBuilder); + Assert.Empty(test2_true.PolicyEdges); + + var test2_false = a.PolicyEdges[false]; + Assert.Equal(new[] { endpoint3, }, test2_false.Matches); + Assert.Null(test2_false.NodeBuilder); + Assert.Empty(test2_false.PolicyEdges); + } + + [Fact] + public void BuildDfaTree_WithPolicies_AndBranches_SecondSkipped() + { + // Arrange + var builder = CreateDfaMatcherBuilder(new TestMetadata1MatcherPolicy(), new TestMetadata2MatcherPolicy()); + + var endpoint1 = CreateEndpoint("/a", metadata: new object[] { new TestMetadata1(0), }); + builder.AddEndpoint(endpoint1); + + var endpoint2 = CreateEndpoint("/a", metadata: new object[] { new TestMetadata1(1), }); + builder.AddEndpoint(endpoint2); + + var endpoint3 = CreateEndpoint("/a", metadata: new object[] { new TestMetadata1(1), }); + builder.AddEndpoint(endpoint3); + + // Act + var root = builder.BuildDfaTree(); + + // Assert + Assert.Empty(root.Matches); + Assert.Null(root.Parameters); + + var next = Assert.Single(root.Literals); + Assert.Equal("a", next.Key); + + var a = next.Value; + Assert.Empty(a.Matches); + Assert.IsType(a.NodeBuilder); + Assert.Collection( + a.PolicyEdges.OrderBy(e => e.Key), + e => Assert.Equal(0, e.Key), + e => Assert.Equal(1, e.Key)); + + var test1_0 = a.PolicyEdges[0]; + Assert.Equal(new[] { endpoint1, }, test1_0.Matches); + Assert.Null(test1_0.NodeBuilder); + Assert.Empty(test1_0.PolicyEdges); + + var test1_1 = a.PolicyEdges[1]; + Assert.Equal(new[] { endpoint2, endpoint3, }, test1_1.Matches); + Assert.Null(test1_1.NodeBuilder); + Assert.Empty(test1_1.PolicyEdges); + } + + [Fact] + public void BuildDfaTree_WithPolicies_AndBranches_BothPoliciesSkipped() + { + // Arrange + var builder = CreateDfaMatcherBuilder(new TestMetadata1MatcherPolicy(), new TestMetadata2MatcherPolicy()); + + var endpoint1 = CreateEndpoint("/a", metadata: new object[] { }); + builder.AddEndpoint(endpoint1); + + var endpoint2 = CreateEndpoint("/a", metadata: new object[] { }); + builder.AddEndpoint(endpoint2); + + var endpoint3 = CreateEndpoint("/a", metadata: new object[] { }); + builder.AddEndpoint(endpoint3); + + // Act + var root = builder.BuildDfaTree(); + + // Assert + Assert.Empty(root.Matches); + Assert.Null(root.Parameters); + + var next = Assert.Single(root.Literals); + Assert.Equal("a", next.Key); + + var a = next.Value; + Assert.Equal(new[] { endpoint1, endpoint2, endpoint3, }, a.Matches); + Assert.Null(a.NodeBuilder); + Assert.Empty(a.PolicyEdges); + } + [Fact] public void CreateCandidate_JustLiterals() { @@ -413,7 +633,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers var builder = CreateDfaMatcherBuilder(); // Act - var candidate = builder.CreateCandidate(new MatcherBuilderEntry(endpoint)); + var candidate = builder.CreateCandidate(endpoint, score: 0); // Assert Assert.Equal(Candidate.CandidateFlags.None, candidate.Flags); @@ -433,7 +653,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers var builder = CreateDfaMatcherBuilder(); // Act - var candidate = builder.CreateCandidate(new MatcherBuilderEntry(endpoint)); + var candidate = builder.CreateCandidate(endpoint, score: 0); // Assert Assert.Equal(Candidate.CandidateFlags.HasCaptures, candidate.Flags); @@ -457,7 +677,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers var builder = CreateDfaMatcherBuilder(); // Act - var candidate = builder.CreateCandidate(new MatcherBuilderEntry(endpoint)); + var candidate = builder.CreateCandidate(endpoint, score: 0); // Assert Assert.Equal( @@ -487,7 +707,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers var builder = CreateDfaMatcherBuilder(); // Act - var candidate = builder.CreateCandidate(new MatcherBuilderEntry(endpoint)); + var candidate = builder.CreateCandidate(endpoint, score: 0); // Assert Assert.Equal( @@ -519,7 +739,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers var builder = CreateDfaMatcherBuilder(); // Act - var candidate = builder.CreateCandidate(new MatcherBuilderEntry(endpoint)); + var candidate = builder.CreateCandidate(endpoint, score: 0); // Assert Assert.Equal( @@ -550,7 +770,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers var builder = CreateDfaMatcherBuilder(); // Act - var candidate = builder.CreateCandidate(new MatcherBuilderEntry(endpoint)); + var candidate = builder.CreateCandidate(endpoint, score: 0); // Assert Assert.Equal( @@ -581,7 +801,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers var builder = CreateDfaMatcherBuilder(); // Act - var candidate = builder.CreateCandidate(new MatcherBuilderEntry(endpoint)); + var candidate = builder.CreateCandidate(endpoint, score: 0); // Assert Assert.Equal( Candidate.CandidateFlags.HasMatchProcessors, candidate.Flags); @@ -591,30 +811,137 @@ namespace Microsoft.AspNetCore.Routing.Matchers Assert.Empty(candidate.ComplexSegments); Assert.Single(candidate.MatchProcessors); } + + [Fact] + public void CreateCandidates_CreatesScoresCorrectly() + { + // Arrange + var endpoints = new[] + { + CreateEndpoint("/a/b/c", constraints: new { a = new IntRouteConstraint(), }, metadata: new object[] { new TestMetadata1(), new TestMetadata2(), }), + CreateEndpoint("/a/b/c", constraints: new { a = new AlphaRouteConstraint(), }, metadata: new object[] { new TestMetadata1(), new TestMetadata2(), }), + CreateEndpoint("/a/b/c", constraints: new { a = new IntRouteConstraint(), }, metadata: new object[] { new TestMetadata1(), }), + CreateEndpoint("/a/b/c", constraints: new { a = new IntRouteConstraint(), }, metadata: new object[] { new TestMetadata2(), }), + CreateEndpoint("/a/b/c", constraints: new { }, metadata: new object[] { }), + CreateEndpoint("/a/b/c", constraints: new { }, metadata: new object[] { }), + }; - private static DfaMatcherBuilder CreateDfaMatcherBuilder() + var builder = CreateDfaMatcherBuilder(new TestMetadata1MatcherPolicy(), new TestMetadata2MatcherPolicy()); + + // Act + var candidates = builder.CreateCandidates(endpoints); + + // Assert + Assert.Collection( + candidates, + c => Assert.Equal(0, c.Score), + c => Assert.Equal(0, c.Score), + c => Assert.Equal(1, c.Score), + c => Assert.Equal(2, c.Score), + c => Assert.Equal(3, c.Score), + c => Assert.Equal(3, c.Score)); + } + + private static DfaMatcherBuilder CreateDfaMatcherBuilder(params MatcherPolicy[] policies) { var dataSource = new CompositeEndpointDataSource(Array.Empty()); return new DfaMatcherBuilder( Mock.Of(), - new EndpointSelector( - dataSource, - new EndpointConstraintCache(dataSource, Array.Empty()), - NullLoggerFactory.Instance)); + Mock.Of(), + policies); } private MatcherEndpoint CreateEndpoint( string template, object defaults = null, - object constraints = null) + object constraints = null, + params object[] metadata) { return new MatcherEndpoint( MatcherEndpoint.EmptyInvoker, RoutePatternFactory.Parse(template, new RouteValueDictionary(defaults), new RouteValueDictionary(constraints)), new RouteValueDictionary(), 0, - new EndpointMetadataCollection(Array.Empty()), + new EndpointMetadataCollection(metadata), "test"); } + + private class TestMetadata1 + { + public TestMetadata1() + { + } + + public TestMetadata1(int state) + { + State = state; + } + + public int State { get; set; } + } + + private class TestMetadata1MatcherPolicy : MatcherPolicy, IEndpointComparerPolicy, INodeBuilderPolicy + { + public override int Order => 100; + + public IComparer Comparer => EndpointMetadataComparer.Default; + + public bool AppliesToNode(IReadOnlyList endpoints) + { + return endpoints.Any(e => e.Metadata.GetMetadata() != null); + } + + public PolicyJumpTable BuildJumpTable(int exitDestination, IReadOnlyList edges) + { + throw new NotImplementedException(); + } + + public IReadOnlyList GetEdges(IReadOnlyList endpoints) + { + return endpoints + .GroupBy(e => e.Metadata.GetMetadata().State) + .Select(g => new PolicyNodeEdge(g.Key, g.ToArray())) + .ToArray(); + } + } + + private class TestMetadata2 + { + public TestMetadata2() + { + } + + public TestMetadata2(bool state) + { + State = state; + } + + public bool State { get; set; } + } + + private class TestMetadata2MatcherPolicy : MatcherPolicy, IEndpointComparerPolicy, INodeBuilderPolicy + { + public override int Order => 101; + + public IComparer Comparer => EndpointMetadataComparer.Default; + + public bool AppliesToNode(IReadOnlyList endpoints) + { + return endpoints.Any(e => e.Metadata.GetMetadata() != null); + } + + public PolicyJumpTable BuildJumpTable(int exitDestination, IReadOnlyList edges) + { + throw new NotImplementedException(); + } + + public IReadOnlyList GetEdges(IReadOnlyList endpoints) + { + return endpoints + .GroupBy(e => e.Metadata.GetMetadata().State) + .Select(g => new PolicyNodeEdge(g.Key, g.ToArray())) + .ToArray(); + } + } } } diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/DfaMatcherTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/DfaMatcherTest.cs index 821a0d0806..ee6d64cfc1 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/DfaMatcherTest.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/DfaMatcherTest.cs @@ -1,16 +1,12 @@ // 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.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing.EndpointConstraints; using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Options; -using Moq; using Xunit; namespace Microsoft.AspNetCore.Routing.Matchers @@ -51,7 +47,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers CreateEndpoint("/{p:int}", 0) }); - var treeMatcher = CreateDfaMatcher(endpointDataSource); + var matcher = CreateDfaMatcher(endpointDataSource); var httpContext = new DefaultHttpContext(); httpContext.Request.Path = "/1"; @@ -59,7 +55,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers var endpointFeature = new EndpointFeature(); // Act - await treeMatcher.MatchAsync(httpContext, endpointFeature); + await matcher.MatchAsync(httpContext, endpointFeature); // Assert Assert.NotNull(endpointFeature.Endpoint); @@ -74,7 +70,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers CreateEndpoint("/{p:int}", 0) }); - var treeMatcher = CreateDfaMatcher(endpointDataSource); + var matcher = CreateDfaMatcher(endpointDataSource); var httpContext = new DefaultHttpContext(); httpContext.Request.Path = "/One"; @@ -82,7 +78,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers var endpointFeature = new EndpointFeature(); // Act - await treeMatcher.MatchAsync(httpContext, endpointFeature); + await matcher.MatchAsync(httpContext, endpointFeature); // Assert Assert.Null(endpointFeature.Endpoint); @@ -101,7 +97,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers lowerOrderEndpoint }); - var treeMatcher = CreateDfaMatcher(endpointDataSource); + var matcher = CreateDfaMatcher(endpointDataSource); var httpContext = new DefaultHttpContext(); httpContext.Request.Path = "/Teams"; @@ -109,7 +105,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers var endpointFeature = new EndpointFeature(); // Act - await treeMatcher.MatchAsync(httpContext, endpointFeature); + await matcher.MatchAsync(httpContext, endpointFeature); // Assert Assert.Equal(lowerOrderEndpoint, endpointFeature.Endpoint); @@ -131,7 +127,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers endpointWithConstraint }); - var treeMatcher = CreateDfaMatcher(endpointDataSource); + var matcher = CreateDfaMatcher(endpointDataSource); var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "POST"; @@ -140,7 +136,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers var endpointFeature = new EndpointFeature(); // Act - await treeMatcher.MatchAsync(httpContext, endpointFeature); + await matcher.MatchAsync(httpContext, endpointFeature); // Assert Assert.Equal(endpointWithConstraint, endpointFeature.Endpoint); diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/EndpointMetadataComparerTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/EndpointMetadataComparerTest.cs new file mode 100644 index 0000000000..4a6ca5babd --- /dev/null +++ b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/EndpointMetadataComparerTest.cs @@ -0,0 +1,91 @@ +// 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.TestObjects; +using Xunit; + +namespace Microsoft.AspNetCore.Routing.Matchers +{ + public class EndpointMetadataComparerTest + { + [Fact] + public void Compare_EndpointWithMetadata_MoreSpecific() + { + // Arrange + var endpoint1 = new TestEndpoint(new EndpointMetadataCollection(new object[] { new TestMetadata(), }), "test1"); + var endpoint2 = new TestEndpoint(new EndpointMetadataCollection(new object[] { }), "test2"); + + // Act + var result = EndpointMetadataComparer.Default.Compare(endpoint1, endpoint2); + + // Assert + Assert.Equal(-1, result); + } + + [Fact] + public void Compare_EndpointWithMetadata_ReverseOrder_MoreSpecific() + { + // Arrange + var endpoint1 = new TestEndpoint(new EndpointMetadataCollection(new object[] { }), "test1"); + var endpoint2 = new TestEndpoint(new EndpointMetadataCollection(new object[] { new TestMetadata(), }), "test2"); + + // Act + var result = EndpointMetadataComparer.Default.Compare(endpoint1, endpoint2); + + // Assert + Assert.Equal(1, result); + } + + [Fact] + public void Compare_BothEndpointsWithMetadata_Equal() + { + // Arrange + var endpoint1 = new TestEndpoint(new EndpointMetadataCollection(new object[] { new TestMetadata(), }), "test1"); + var endpoint2 = new TestEndpoint(new EndpointMetadataCollection(new object[] { new TestMetadata(), }), "test2"); + + // Act + var result = EndpointMetadataComparer.Default.Compare(endpoint1, endpoint2); + + // Assert + Assert.Equal(0, result); + } + + [Fact] + public void Compare_BothEndpointsWithoutMetadata_Equal() + { + // Arrange + var endpoint1 = new TestEndpoint(new EndpointMetadataCollection(new object[] { }), "test1"); + var endpoint2 = new TestEndpoint(new EndpointMetadataCollection(new object[] { }), "test2"); + + // Act + var result = EndpointMetadataComparer.Default.Compare(endpoint1, endpoint2); + + // Assert + Assert.Equal(0, result); + } + + [Fact] + public void Sort_EndpointWithMetadata_FirstInList() + { + // Arrange + var endpoint1 = new TestEndpoint(new EndpointMetadataCollection(new object[] { new TestMetadata(), }), "test1"); + var endpoint2 = new TestEndpoint(new EndpointMetadataCollection(new object[] { }), "test2"); + + var list = new List() { endpoint2, endpoint1, }; + + // Act + list.Sort(EndpointMetadataComparer.Default); + + // Assert + Assert.Collection( + list, + e => Assert.Same(endpoint1, e), + e => Assert.Same(endpoint2, e)); + } + + private class TestMetadata + { + } + } +} diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/HttpMethodMatcherPolicyIntegrationTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/HttpMethodMatcherPolicyIntegrationTest.cs new file mode 100644 index 0000000000..26bf2173de --- /dev/null +++ b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/HttpMethodMatcherPolicyIntegrationTest.cs @@ -0,0 +1,232 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing.Metadata; +using Microsoft.AspNetCore.Routing.Patterns; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.AspNetCore.Routing.Matchers +{ + // End-to-end tests for the HTTP method matching functionality + public class HttpMethodMatcherPolicyIntegrationTest + { + [Fact] + public async Task Match_HttpMethod() + { + // Arrange + var endpoint = CreateEndpoint("/hello", httpMethods: new string[] { "GET", }); + + var matcher = CreateMatcher(endpoint); + var (httpContext, feature) = CreateContext("/hello", "GET"); + + // Act + await matcher.MatchAsync(httpContext, feature); + + // Assert + MatcherAssert.AssertMatch(feature, endpoint); + } + + [Fact] + public async Task Match_HttpMethod_CaseInsensitive() + { + // Arrange + var endpoint = CreateEndpoint("/hello", httpMethods: new string[] { "GeT", }); + + var matcher = CreateMatcher(endpoint); + var (httpContext, feature) = CreateContext("/hello", "GET"); + + // Act + await matcher.MatchAsync(httpContext, feature); + + // Assert + MatcherAssert.AssertMatch(feature, endpoint); + } + + [Fact] + public async Task Match_NoMetadata_MatchesAnyHttpMethod() + { + // Arrange + var endpoint = CreateEndpoint("/hello"); + + var matcher = CreateMatcher(endpoint); + var (httpContext, feature) = CreateContext("/hello", "GET"); + + // Act + await matcher.MatchAsync(httpContext, feature); + + // Assert + MatcherAssert.AssertMatch(feature, endpoint); + } + + [Fact] + public async Task Match_EmptyMethodList_MatchesAnyHttpMethod() + { + // Arrange + var endpoint = CreateEndpoint("/hello", httpMethods: new string[] { }); + + var matcher = CreateMatcher(endpoint); + var (httpContext, feature) = CreateContext("/hello", "GET"); + + // Act + await matcher.MatchAsync(httpContext, feature); + + // Assert + MatcherAssert.AssertMatch(feature, endpoint); + } + + [Fact] // When all of the candidates handles specific verbs, use a 405 endpoint + public async Task NotMatch_HttpMethod_Returns405Endpoint() + { + // Arrange + var endpoint1 = CreateEndpoint("/hello", httpMethods: new string[] { "GET", "PUT" }); + var endpoint2 = CreateEndpoint("/hello", httpMethods: new string[] { "DELETE" }); + + var matcher = CreateMatcher(endpoint1, endpoint2); + var (httpContext, feature) = CreateContext("/hello", "POST"); + + // Act + await matcher.MatchAsync(httpContext, feature); + + // Assert + Assert.NotSame(endpoint1, feature.Endpoint); + Assert.NotSame(endpoint2, feature.Endpoint); + + Assert.Same(HttpMethodEndpointSelectorPolicy.Http405EndpointDisplayName, feature.Endpoint.DisplayName); + + // Invoke the endpoint + await feature.Invoker((c) => Task.CompletedTask)(httpContext); + Assert.Equal(405, httpContext.Response.StatusCode); + Assert.Equal("DELETE, GET, PUT", httpContext.Response.Headers["Allow"]); + } + + [Fact] // When one of the candidates handles all verbs, dont use a 405 endpoint + public async Task NotMatch_HttpMethod_WithAllMethodEndpoint_DoesNotReturn405() + { + // Arrange + var endpoint1 = CreateEndpoint("/{x:int}", httpMethods: new string[] { }); + var endpoint2 = CreateEndpoint("/hello", httpMethods: new string[] { "DELETE" }); + + var matcher = CreateMatcher(endpoint1, endpoint2); + var (httpContext, feature) = CreateContext("/hello", "POST"); + + // Act + await matcher.MatchAsync(httpContext, feature); + + // Assert + MatcherAssert.AssertNotMatch(feature); + } + + [Fact] + public async Task Match_EndpointWithHttpMethodPreferred() + { + // Arrange + var endpoint1 = CreateEndpoint("/hello", httpMethods: new string[] { "GET", }); + var endpoint2 = CreateEndpoint("/bar"); + + var matcher = CreateMatcher(endpoint1, endpoint2); + var (httpContext, feature) = CreateContext("/hello", "GET"); + + // Act + await matcher.MatchAsync(httpContext, feature); + + // Assert + MatcherAssert.AssertMatch(feature, endpoint1); + } + + [Fact] + public async Task Match_EndpointWithHttpMethodPreferred_EmptyList() + { + // Arrange + var endpoint1 = CreateEndpoint("/hello", httpMethods: new string[] { "GET", }); + var endpoint2 = CreateEndpoint("/bar", httpMethods: new string[] { }); + + var matcher = CreateMatcher(endpoint1, endpoint2); + var (httpContext, feature) = CreateContext("/hello", "GET"); + + // Act + await matcher.MatchAsync(httpContext, feature); + + // Assert + MatcherAssert.AssertMatch(feature, endpoint1); + } + + [Fact] // The non-http-method-specific endpoint is part of the same candidate set + public async Task Match_EndpointWithHttpMethodPreferred_FallsBackToNonSpecific() + { + // Arrange + var endpoint1 = CreateEndpoint("/{x}", httpMethods: new string[] { "GET", }); + var endpoint2 = CreateEndpoint("/{x}", httpMethods: new string[] { }); + + var matcher = CreateMatcher(endpoint1, endpoint2); + var (httpContext, feature) = CreateContext("/hello", "POST"); + + // Act + await matcher.MatchAsync(httpContext, feature); + + // Assert + MatcherAssert.AssertMatch(feature, endpoint2, ignoreValues: true); + } + + private static Matcher CreateMatcher(params MatcherEndpoint[] endpoints) + { + var services = new ServiceCollection() + .AddOptions() + .AddLogging() + .AddRouting() + .BuildServiceProvider(); + + var builder = services.GetRequiredService(); + for (var i = 0; i < endpoints.Length; i++) + { + builder.AddEndpoint(endpoints[i]); + } + + return builder.Build(); + } + + internal static (HttpContext httpContext, IEndpointFeature feature) CreateContext(string path, string httpMethod) + { + var httpContext = new DefaultHttpContext(); + httpContext.Request.Method = httpMethod; + httpContext.Request.Path = path; + + var feature = new EndpointFeature(); + httpContext.Features.Set(feature); + + return (httpContext, feature); + } + internal static MatcherEndpoint CreateEndpoint( + string template, + object defaults = null, + object constraints = null, + int order = 0, + string[] httpMethods = null) + { + var metadata = new List(); + if (httpMethods != null) + { + metadata.Add(new HttpMethodMetadata(httpMethods)); + } + + var displayName = "endpoint: " + template + " " + string.Join(", ", httpMethods ?? new[] { "(any)" }); + return new MatcherEndpoint( + MatcherEndpoint.EmptyInvoker, + RoutePatternFactory.Parse(template, defaults, constraints), + new RouteValueDictionary(), + order, + new EndpointMetadataCollection(metadata), + displayName); + } + + internal (Matcher matcher, MatcherEndpoint endpoint) CreateMatcher(string template) + { + var endpoint = CreateEndpoint(template); + return (CreateMatcher(endpoint), endpoint); + } + } +} diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/HttpMethodMatcherPolicyTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/HttpMethodMatcherPolicyTest.cs new file mode 100644 index 0000000000..d4e3149852 --- /dev/null +++ b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/HttpMethodMatcherPolicyTest.cs @@ -0,0 +1,171 @@ +// 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; +using Microsoft.AspNetCore.Routing.Metadata; +using Microsoft.AspNetCore.Routing.Patterns; +using Xunit; + +namespace Microsoft.AspNetCore.Routing.Matchers +{ + public class HttpMethodMatcherPolicyTest + { + [Fact] + public void AppliesToNode_EndpointWithoutMetadata_ReturnsFalse() + { + // Arrange + var endpoints = new[] { CreateEndpoint("/", null), }; + + var policy = CreatePolicy(); + + // Act + var result = policy.AppliesToNode(endpoints); + + // Assert + Assert.False(result); + } + + [Fact] + public void AppliesToNode_EndpointWithoutHttpMethods_ReturnsFalse() + { + // Arrange + var endpoints = new[] { CreateEndpoint("/", Array.Empty()), }; + + var policy = CreatePolicy(); + + // Act + var result = policy.AppliesToNode(endpoints); + + // Assert + Assert.False(result); + } + + [Fact] + public void AppliesToNode_EndpointHasHttpMethods_ReturnsTrue() + { + // Arrange + var endpoints = new[] { CreateEndpoint("/", Array.Empty()), CreateEndpoint("/", new[] { "GET", })}; + + var policy = CreatePolicy(); + + // Act + var result = policy.AppliesToNode(endpoints); + + // Assert + Assert.True(result); + } + + [Fact] + public void GetEdges_GroupsByHttpMethod() + { + // Arrange + var endpoints = new[] + { + // These are arrange in an order that we won't actually see in a product scenario. It's done + // this way so we can verify that ordering is preserved by GetEdges. + CreateEndpoint("/", new[] { "GET", }), + CreateEndpoint("/", Array.Empty()), + CreateEndpoint("/", new[] { "GET", "PUT", "POST" }), + CreateEndpoint("/", new[] { "PUT", "POST" }), + CreateEndpoint("/", Array.Empty()), + }; + + var policy = CreatePolicy(); + + // Act + var edges = policy.GetEdges(endpoints); + + // Assert + Assert.Collection( + edges.OrderBy(e => e.State), + e => + { + Assert.Equal(HttpMethodEndpointSelectorPolicy.AnyMethod, e.State); + Assert.Equal(new[] { endpoints[1], endpoints[4], }, e.Endpoints.ToArray()); + }, + e => + { + Assert.Equal("GET", e.State); + Assert.Equal(new[] { endpoints[0], endpoints[1], endpoints[2], endpoints[4], }, e.Endpoints.ToArray()); + }, + e => + { + Assert.Equal("POST", e.State); + Assert.Equal(new[] { endpoints[1], endpoints[2], endpoints[3], endpoints[4], }, e.Endpoints.ToArray()); + }, + e => + { + Assert.Equal("PUT", e.State); + Assert.Equal(new[] { endpoints[1], endpoints[2], endpoints[3], endpoints[4], }, e.Endpoints.ToArray()); + }); + } + + [Fact] // See explanation in GetEdges for how this case is different + public void GetEdges_GroupsByHttpMethod_CreatesHttp405Endpoint() + { + // Arrange + var endpoints = new[] + { + // These are arrange in an order that we won't actually see in a product scenario. It's done + // this way so we can verify that ordering is preserved by GetEdges. + CreateEndpoint("/", new[] { "GET", }), + CreateEndpoint("/", new[] { "GET", "PUT", "POST" }), + CreateEndpoint("/", new[] { "PUT", "POST" }), + }; + + var policy = CreatePolicy(); + + // Act + var edges = policy.GetEdges(endpoints); + + // Assert + Assert.Collection( + edges.OrderBy(e => e.State), + e => + { + Assert.Equal(HttpMethodEndpointSelectorPolicy.AnyMethod, e.State); + Assert.Equal(HttpMethodEndpointSelectorPolicy.Http405EndpointDisplayName, e.Endpoints.Single().DisplayName); + }, + e => + { + Assert.Equal("GET", e.State); + Assert.Equal(new[] { endpoints[0], endpoints[1], }, e.Endpoints.ToArray()); + }, + e => + { + Assert.Equal("POST", e.State); + Assert.Equal(new[] { endpoints[1], endpoints[2], }, e.Endpoints.ToArray()); + }, + e => + { + Assert.Equal("PUT", e.State); + Assert.Equal(new[] { endpoints[1], endpoints[2], }, e.Endpoints.ToArray()); + }); + } + + private static MatcherEndpoint CreateEndpoint(string template, string[] httpMethods) + { + var metadata = new List(); + if (httpMethods != null) + { + metadata.Add(new HttpMethodMetadata(httpMethods)); + } + + return new MatcherEndpoint( + MatcherEndpoint.EmptyInvoker, + RoutePatternFactory.Parse(template), + new RouteValueDictionary(), + 0, + new EndpointMetadataCollection(metadata), + $"test: {template}"); + } + + private static HttpMethodEndpointSelectorPolicy CreatePolicy() + { + return new HttpMethodEndpointSelectorPolicy(); + } + } +} diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/MatcherEndpointComparerTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/MatcherEndpointComparerTest.cs new file mode 100644 index 0000000000..383a2153e3 --- /dev/null +++ b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/MatcherEndpointComparerTest.cs @@ -0,0 +1,254 @@ +// 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.Patterns; +using Xunit; + +namespace Microsoft.AspNetCore.Routing.Matchers +{ + public class MatcherEndpointComparerTest + { + [Fact] + public void Compare_PrefersOrder_IfDifferent() + { + // Arrange + var endpoint1 = CreateEndpoint("/", order: 1); + var endpoint2 = CreateEndpoint("/api/foo", order: -1); + + var comparer = CreateComparer(); + + // Act + var result = comparer.Compare(endpoint1, endpoint2); + + // Assert + Assert.Equal(1, result); + } + + [Fact] + public void Compare_PrefersPrecedence_IfOrderIsSame() + { + // Arrange + var endpoint1 = CreateEndpoint("/api/foo", order: 1); + var endpoint2 = CreateEndpoint("/", order: 1); + + var comparer = CreateComparer(); + + // Act + var result = comparer.Compare(endpoint1, endpoint2); + + // Assert + Assert.Equal(1, result); + } + + [Fact] + public void Compare_PrefersPolicy_IfPrecedenceIsSame() + { + // Arrange + var endpoint1 = CreateEndpoint("/", order: 1, new TestMetadata1()); + var endpoint2 = CreateEndpoint("/", order: 1); + + var comparer = CreateComparer(new TestMetadata1Policy()); + + // Act + var result = comparer.Compare(endpoint1, endpoint2); + + // Assert + Assert.Equal(-1, result); + } + + [Fact] + public void Compare_PrefersSecondPolicy_IfFirstPolicyIsSame() + { + // Arrange + var endpoint1 = CreateEndpoint("/", order: 1, new TestMetadata1()); + var endpoint2 = CreateEndpoint("/", order: 1, new TestMetadata1(), new TestMetadata2()); + + var comparer = CreateComparer(new TestMetadata1Policy(), new TestMetadata2Policy()); + + // Act + var result = comparer.Compare(endpoint1, endpoint2); + + // Assert + Assert.Equal(1, result); + } + + [Fact] + public void Compare_PrefersTemplate_IfOtherCriteriaIsSame() + { + // Arrange + var endpoint1 = CreateEndpoint("/foo", order: 1, new TestMetadata1()); + var endpoint2 = CreateEndpoint("/bar", order: 1, new TestMetadata1()); + + var comparer = CreateComparer(new TestMetadata1Policy(), new TestMetadata2Policy()); + + // Act + var result = comparer.Compare(endpoint1, endpoint2); + + // Assert + Assert.Equal(1, result); + } + + [Fact] + public void Compare_ReturnsZero_WhenIdentical() + { + // Arrange + var endpoint1 = CreateEndpoint("/foo", order: 1, new TestMetadata1()); + var endpoint2 = CreateEndpoint("/foo", order: 1, new TestMetadata1()); + + var comparer = CreateComparer(new TestMetadata1Policy(), new TestMetadata2Policy()); + + // Act + var result = comparer.Compare(endpoint1, endpoint2); + + // Assert + Assert.Equal(0, result); + } + + [Fact] + public void Equals_NotEqual_IfOrderDifferent() + { + // Arrange + var endpoint1 = CreateEndpoint("/", order: 1); + var endpoint2 = CreateEndpoint("/api/foo", order: -1); + + var comparer = CreateComparer(); + + // Act + var result = comparer.Equals(endpoint1, endpoint2); + + // Assert + Assert.False(result); + } + + [Fact] + public void Equals_NotEqual_IfPrecedenceDifferent() + { + // Arrange + var endpoint1 = CreateEndpoint("/api/foo", order: 1); + var endpoint2 = CreateEndpoint("/", order: 1); + + var comparer = CreateComparer(); + + // Act + var result = comparer.Equals(endpoint1, endpoint2); + + // Assert + Assert.False(result); + } + + [Fact] + public void Equals_NotEqual_IfFirstPolicyDifferent() + { + // Arrange + var endpoint1 = CreateEndpoint("/", order: 1, new TestMetadata1()); + var endpoint2 = CreateEndpoint("/", order: 1); + + var comparer = CreateComparer(new TestMetadata1Policy()); + + // Act + var result = comparer.Equals(endpoint1, endpoint2); + + // Assert + Assert.False(result); + } + + [Fact] + public void Equals_NotEqual_IfSecondPolicyDifferent() + { + // Arrange + var endpoint1 = CreateEndpoint("/", order: 1, new TestMetadata1()); + var endpoint2 = CreateEndpoint("/", order: 1, new TestMetadata1(), new TestMetadata2()); + + var comparer = CreateComparer(new TestMetadata1Policy(), new TestMetadata2Policy()); + + // Act + var result = comparer.Equals(endpoint1, endpoint2); + + // Assert + Assert.False(result); + } + + [Fact] + public void Equals_Equals_WhenTemplateIsDifferent() + { + // Arrange + var endpoint1 = CreateEndpoint("/foo", order: 1, new TestMetadata1()); + var endpoint2 = CreateEndpoint("/bar", order: 1, new TestMetadata1()); + + var comparer = CreateComparer(new TestMetadata1Policy(), new TestMetadata2Policy()); + + // Act + var result = comparer.Equals(endpoint1, endpoint2); + + // Assert + Assert.True(result); + } + + [Fact] + public void Sort_MoreSpecific_FirstInList() + { + // Arrange + var endpoint1 = CreateEndpoint("/foo", order: -1); + var endpoint2 = CreateEndpoint("/bar/{baz}", order: -1); + var endpoint3 = CreateEndpoint("/bar", order: 0, new TestMetadata1()); + var endpoint4 = CreateEndpoint("/foo", order: 0, new TestMetadata2()); + var endpoint5 = CreateEndpoint("/foo", order: 0); + var endpoint6 = CreateEndpoint("/a{baz}", order: 0, new TestMetadata1(), new TestMetadata2()); + var endpoint7 = CreateEndpoint("/bar{baz}", order: 0, new TestMetadata1(), new TestMetadata2()); + + // Endpoints listed in reverse of the desired order. + var list = new List() { endpoint7, endpoint6, endpoint5, endpoint4, endpoint3, endpoint2, endpoint1, }; + + var comparer = CreateComparer(new TestMetadata1Policy(), new TestMetadata2Policy()); + + // Act + list.Sort(comparer); + + // Assert + Assert.Collection( + list, + e => Assert.Same(endpoint1, e), + e => Assert.Same(endpoint2, e), + e => Assert.Same(endpoint3, e), + e => Assert.Same(endpoint4, e), + e => Assert.Same(endpoint5, e), + e => Assert.Same(endpoint6, e), + e => Assert.Same(endpoint7, e)); + } + + private static MatcherEndpoint CreateEndpoint(string template, int order, params object[] metadata) + { + return new MatcherEndpoint( + MatcherEndpoint.EmptyInvoker, + RoutePatternFactory.Parse(template), + new RouteValueDictionary(), + order, + new EndpointMetadataCollection(metadata), + "test: " + template); + } + + private static MatcherEndpointComparer CreateComparer(params IEndpointComparerPolicy[] policies) + { + return new MatcherEndpointComparer(policies); + } + + private class TestMetadata1 + { + } + + private class TestMetadata1Policy : IEndpointComparerPolicy + { + public IComparer Comparer => EndpointMetadataComparer.Default; + } + + private class TestMetadata2 + { + } + + private class TestMetadata2Policy : IEndpointComparerPolicy + { + public IComparer Comparer => EndpointMetadataComparer.Default; + } + } +} diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/RouteMatcherBuilder.cs b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/RouteMatcherBuilder.cs index 3748454b92..a57eea4387 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/RouteMatcherBuilder.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/RouteMatcherBuilder.cs @@ -7,7 +7,6 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Routing.EndpointConstraints; using Microsoft.AspNetCore.Routing.Patterns; -using Microsoft.AspNetCore.Routing.Template; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; @@ -16,39 +15,37 @@ namespace Microsoft.AspNetCore.Routing.Matchers internal class RouteMatcherBuilder : MatcherBuilder { private readonly IInlineConstraintResolver _constraintResolver; - private readonly List _entries; + private readonly List _endpoints; public RouteMatcherBuilder() { _constraintResolver = new DefaultInlineConstraintResolver(Options.Create(new RouteOptions())); - _entries = new List(); + _endpoints = new List(); } public override void AddEndpoint(MatcherEndpoint endpoint) { - _entries.Add(new MatcherBuilderEntry(endpoint)); + _endpoints.Add(endpoint); } public override Matcher Build() { - _entries.Sort(); - var cache = new EndpointConstraintCache( new CompositeEndpointDataSource(Array.Empty()), new[] { new DefaultEndpointConstraintProvider(), }); - var selector = new EndpointSelector(null, cache, NullLoggerFactory.Instance); + var selector = new EndpointConstraintEndpointSelector(null, cache, NullLoggerFactory.Instance); - var groups = _entries - .GroupBy(e => (e.Order, e.Precedence, e.Endpoint.RoutePattern.RawText)) + var groups = _endpoints + .GroupBy(e => (e.Order, e.RoutePattern.InboundPrecedence, e.RoutePattern.RawText)) .OrderBy(g => g.Key.Order) - .ThenBy(g => g.Key.Precedence); + .ThenBy(g => g.Key.InboundPrecedence); var routes = new RouteCollection(); foreach (var group in groups) { - var candidates = group.Select(e => e.Endpoint).ToArray(); - var endpoint = group.First().Endpoint; + var candidates = group.ToArray(); + var endpoint = group.First(); // RoutePattern.Defaults contains the default values parsed from the template // as well as those specified with a literal. We need to separate those @@ -81,12 +78,15 @@ namespace Microsoft.AspNetCore.Routing.Matchers private class SelectorRouter : IRouter { private readonly EndpointSelector _selector; - private readonly Endpoint[] _candidates; + private readonly MatcherEndpoint[] _candidates; + private readonly int[] _scores; - public SelectorRouter(EndpointSelector selector, Endpoint[] candidates) + public SelectorRouter(EndpointSelector selector, MatcherEndpoint[] candidates) { _selector = selector; _candidates = candidates; + + _scores = new int[_candidates.Length]; } public VirtualPathData GetVirtualPath(VirtualPathContext context) @@ -94,15 +94,19 @@ namespace Microsoft.AspNetCore.Routing.Matchers throw new NotImplementedException(); } - public Task RouteAsync(RouteContext context) + public async Task RouteAsync(RouteContext context) { - var endpoint = _selector.SelectBestCandidate(context.HttpContext, _candidates); - if (endpoint != null) + var feature = context.HttpContext.Features.Get(); + + // This is needed due to a quirk of our tests - they reuse the endpoint feature + // across requests. + feature.Endpoint = null; + + await _selector.SelectAsync(context.HttpContext, feature, new CandidateSet(_candidates, _scores)); + if (feature.Endpoint != null) { - context.HttpContext.Features.Get().Endpoint = endpoint; context.Handler = (_) => Task.CompletedTask; } - return Task.CompletedTask; } } } diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/TreeRouterMatcherBuilder.cs b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/TreeRouterMatcherBuilder.cs index 3ed3162666..949b0a7df2 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/TreeRouterMatcherBuilder.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/TreeRouterMatcherBuilder.cs @@ -17,22 +17,20 @@ namespace Microsoft.AspNetCore.Routing.Matchers { internal class TreeRouterMatcherBuilder : MatcherBuilder { - private readonly List _entries; + private readonly List _endpoints; public TreeRouterMatcherBuilder() { - _entries = new List(); + _endpoints = new List(); } public override void AddEndpoint(MatcherEndpoint endpoint) { - _entries.Add(new MatcherBuilderEntry(endpoint)); + _endpoints.Add(endpoint); } public override Matcher Build() { - _entries.Sort(); - var builder = new TreeRouteBuilder( NullLoggerFactory.Instance, new DefaultObjectPool(new UriBuilderContextPooledObjectPolicy()), @@ -41,23 +39,23 @@ namespace Microsoft.AspNetCore.Routing.Matchers var cache = new EndpointConstraintCache( new CompositeEndpointDataSource(Array.Empty()), new[] { new DefaultEndpointConstraintProvider(), }); - var selector = new EndpointSelector(null, cache, NullLoggerFactory.Instance); + var selector = new EndpointConstraintEndpointSelector(null, cache, NullLoggerFactory.Instance); - var groups = _entries - .GroupBy(e => (e.Order, e.Precedence, e.Endpoint.RoutePattern.RawText)) + var groups = _endpoints + .GroupBy(e => (e.Order, e.RoutePattern.InboundPrecedence, e.RoutePattern.RawText)) .OrderBy(g => g.Key.Order) - .ThenBy(g => g.Key.Precedence); + .ThenBy(g => g.Key.InboundPrecedence); var routes = new RouteCollection(); foreach (var group in groups) { - var candidates = group.Select(e => e.Endpoint).ToArray(); + var candidates = group.ToArray(); // MatcherEndpoint.Values contains the default values parsed from the template // as well as those specified with a literal. We need to separate those // for legacy cases. - var endpoint = group.First().Endpoint; + var endpoint = group.First(); var defaults = new RouteValueDictionary(endpoint.RoutePattern.Defaults); for (var i = 0; i < endpoint.RoutePattern.Parameters.Count; i++) { @@ -81,12 +79,15 @@ namespace Microsoft.AspNetCore.Routing.Matchers private class SelectorRouter : IRouter { private readonly EndpointSelector _selector; - private readonly Endpoint[] _candidates; + private readonly MatcherEndpoint[] _candidates; + private readonly int[] _scores; - public SelectorRouter(EndpointSelector selector, Endpoint[] candidates) + public SelectorRouter(EndpointSelector selector, MatcherEndpoint[] candidates) { _selector = selector; _candidates = candidates; + + _scores = new int[_candidates.Length]; } public VirtualPathData GetVirtualPath(VirtualPathContext context) @@ -94,15 +95,18 @@ namespace Microsoft.AspNetCore.Routing.Matchers throw new NotImplementedException(); } - public Task RouteAsync(RouteContext context) + public async Task RouteAsync(RouteContext context) { - var endpoint = _selector.SelectBestCandidate(context.HttpContext, _candidates); - if (endpoint != null) + var feature = context.HttpContext.Features.Get(); + + // This is needed due to a quirk of our tests - they reuse the endpoint feature. + feature.Endpoint = null; + + await _selector.SelectAsync(context.HttpContext, feature, new CandidateSet(_candidates, _scores)); + if (feature.Endpoint != null) { - context.HttpContext.Features.Get().Endpoint = endpoint; context.Handler = (_) => Task.CompletedTask; } - return Task.CompletedTask; } } } From fdff66054f665c7955f289d5cf8db28cd69acf5b Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Wed, 25 Jul 2018 14:35:41 +1200 Subject: [PATCH 17/20] Missing ChangeToken -> GetChangeToken updates (#660) --- .../CompositeEndpointDataSource.cs | 11 +++++------ .../DefaultEndpointDataSource.cs | 4 +++- .../CompositeEndpointDataSourceTest.cs | 3 ++- .../TestObjects/DynamicEndpointDataSource.cs | 4 +++- 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/Microsoft.AspNetCore.Routing/CompositeEndpointDataSource.cs b/src/Microsoft.AspNetCore.Routing/CompositeEndpointDataSource.cs index 5c6a95500a..2e6559817c 100644 --- a/src/Microsoft.AspNetCore.Routing/CompositeEndpointDataSource.cs +++ b/src/Microsoft.AspNetCore.Routing/CompositeEndpointDataSource.cs @@ -34,13 +34,12 @@ namespace Microsoft.AspNetCore.Routing _lock = new object(); } - public override IChangeToken ChangeToken + public override IChangeToken ChangeToken => GetChangeToken(); + + public override IChangeToken GetChangeToken() { - get - { - EnsureInitialized(); - return _consumerChangeToken; - } + EnsureInitialized(); + return _consumerChangeToken; } public override IReadOnlyList Endpoints diff --git a/src/Microsoft.AspNetCore.Routing/DefaultEndpointDataSource.cs b/src/Microsoft.AspNetCore.Routing/DefaultEndpointDataSource.cs index 6dfbdbb38c..971f499afa 100644 --- a/src/Microsoft.AspNetCore.Routing/DefaultEndpointDataSource.cs +++ b/src/Microsoft.AspNetCore.Routing/DefaultEndpointDataSource.cs @@ -23,7 +23,9 @@ namespace Microsoft.AspNetCore.Routing _endpoints.AddRange(endpoints); } - public override IChangeToken ChangeToken { get; } = NullChangeToken.Singleton; + public override IChangeToken ChangeToken => GetChangeToken(); + + public override IChangeToken GetChangeToken() => NullChangeToken.Singleton; public override IReadOnlyList Endpoints => _endpoints; } diff --git a/test/Microsoft.AspNetCore.Routing.Tests/CompositeEndpointDataSourceTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/CompositeEndpointDataSourceTest.cs index 52ba52dafc..16a04cc587 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/CompositeEndpointDataSourceTest.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/CompositeEndpointDataSourceTest.cs @@ -173,7 +173,8 @@ namespace Microsoft.AspNetCore.Routing _token = new CancellationChangeToken(_cts.Token); } - public override IChangeToken ChangeToken => _token; + public override IChangeToken GetChangeToken() => _token; + public override IChangeToken ChangeToken => GetChangeToken(); public override IReadOnlyList Endpoints => Array.Empty(); } } diff --git a/test/Microsoft.AspNetCore.Routing.Tests/TestObjects/DynamicEndpointDataSource.cs b/test/Microsoft.AspNetCore.Routing.Tests/TestObjects/DynamicEndpointDataSource.cs index d434b9c924..79e71babdb 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/TestObjects/DynamicEndpointDataSource.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/TestObjects/DynamicEndpointDataSource.cs @@ -23,7 +23,9 @@ namespace Microsoft.AspNetCore.Routing.TestObjects CreateChangeToken(); } - public override IChangeToken ChangeToken => _changeToken; + public override IChangeToken GetChangeToken() => _changeToken; + + public override IChangeToken ChangeToken => GetChangeToken(); public override IReadOnlyList Endpoints => _endpoints; From 1340f9c26bacda3fd30793b5a1992bc5d28febc6 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Wed, 25 Jul 2018 16:03:26 +1200 Subject: [PATCH 18/20] Add EndpointSelectorCandidate ctor to not break MVC (#661) --- .../EndpointConstraints/IEndpointConstraint.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Microsoft.AspNetCore.Routing/EndpointConstraints/IEndpointConstraint.cs b/src/Microsoft.AspNetCore.Routing/EndpointConstraints/IEndpointConstraint.cs index 92a1f17388..87fc9407ad 100644 --- a/src/Microsoft.AspNetCore.Routing/EndpointConstraints/IEndpointConstraint.cs +++ b/src/Microsoft.AspNetCore.Routing/EndpointConstraints/IEndpointConstraint.cs @@ -46,6 +46,14 @@ namespace Microsoft.AspNetCore.Routing.EndpointConstraints Constraints = constraints; } + // Temporarily added to not break MVC build + public EndpointSelectorCandidate( + Endpoint endpoint, + IReadOnlyList constraints) + { + throw new NotSupportedException(); + } + public Endpoint Endpoint { get; } public int Score { get; } From 19f24cad16e52f0bda126b21494c86f3a5e8d50e Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Tue, 24 Jul 2018 21:54:24 -0700 Subject: [PATCH 19/20] fix silly constructor --- .../EndpointConstraints/IEndpointConstraint.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.AspNetCore.Routing/EndpointConstraints/IEndpointConstraint.cs b/src/Microsoft.AspNetCore.Routing/EndpointConstraints/IEndpointConstraint.cs index 87fc9407ad..1a71ec738d 100644 --- a/src/Microsoft.AspNetCore.Routing/EndpointConstraints/IEndpointConstraint.cs +++ b/src/Microsoft.AspNetCore.Routing/EndpointConstraints/IEndpointConstraint.cs @@ -51,7 +51,15 @@ namespace Microsoft.AspNetCore.Routing.EndpointConstraints Endpoint endpoint, IReadOnlyList constraints) { - throw new NotSupportedException(); + if (endpoint == null) + { + throw new ArgumentNullException(nameof(endpoint)); + } + + Endpoint = endpoint; + Score = 0; + Values = null; + Constraints = constraints; } public Endpoint Endpoint { get; } From e0294f1d1b71b4df3227289ddf3d77c54f6c206e Mon Sep 17 00:00:00 2001 From: Kiran Challa Date: Wed, 25 Jul 2018 06:17:54 -0700 Subject: [PATCH 20/20] Upgraded dependencies.props --- build/dependencies.props | 52 ++++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index 1596743b87..5fc7fe9aad 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -5,32 +5,32 @@ 0.10.13 2.2.0-preview1-17099 - 2.2.0-preview1-34755 - 2.2.0-preview1-34755 - 2.2.0-preview1-34755 - 2.2.0-preview1-34755 - 2.2.0-preview1-34755 - 2.2.0-preview1-34755 - 2.2.0-preview1-34755 - 2.2.0-preview1-34755 - 2.2.0-preview1-34755 - 2.2.0-preview1-34755 - 2.2.0-preview1-34755 - 2.2.0-preview1-34755 - 2.2.0-preview1-34755 - 2.2.0-preview1-34755 - 2.2.0-preview1-34755 - 2.2.0-preview1-34755 - 2.2.0-preview1-34755 - 2.2.0-preview1-34755 - 2.2.0-preview1-34755 - 2.2.0-preview1-34755 - 2.2.0-preview1-34755 - 2.2.0-preview1-34755 - 2.2.0-preview1-34755 - 2.2.0-preview1-34755 - 2.2.0-preview1-34755 - 2.2.0-preview1-34755 + 2.2.0-preview1-34784 + 2.2.0-preview1-34784 + 2.2.0-preview1-34784 + 2.2.0-preview1-34784 + 2.2.0-preview1-34784 + 2.2.0-preview1-34784 + 2.2.0-preview1-34784 + 2.2.0-preview1-34784 + 2.2.0-preview1-34784 + 2.2.0-preview1-34784 + 2.2.0-preview1-34784 + 2.2.0-preview1-34784 + 2.2.0-preview1-34784 + 2.2.0-preview1-34784 + 2.2.0-preview1-34784 + 2.2.0-preview1-34784 + 2.2.0-preview1-34784 + 2.2.0-preview1-34784 + 2.2.0-preview1-34784 + 2.2.0-preview1-34784 + 2.2.0-preview1-34784 + 2.2.0-preview1-34784 + 2.2.0-preview1-34784 + 2.2.0-preview1-34784 + 2.2.0-preview1-34784 + 2.2.0-preview1-34784 2.0.9 2.1.2 2.2.0-preview1-26618-02