diff --git a/src/Routing/.gitignore b/src/Routing/.gitignore new file mode 100644 index 0000000000..5f8dfa9f48 --- /dev/null +++ b/src/Routing/.gitignore @@ -0,0 +1,41 @@ +[Oo]bj/ +[Bb]in/ +TestResults/ +.nuget/ +*.sln.ide/ +_ReSharper.*/ +packages/ +artifacts/ +PublishProfiles/ +.vs/ +.build/ +.testPublish/ +bower_components/ +node_modules/ +**/wwwroot/lib/ +debugSettings.json +project.lock.json +*.user +*.suo +*.cache +*.docstates +_ReSharper.* +nuget.exe +*net45.csproj +*net451.csproj +*k10.csproj +*.psess +*.vsp +*.pidb +*.userprefs +*DS_Store +*.ncrunchsolution +*.*sdf +*.ipch +.settings +*.sln.ide +node_modules +**/[Cc]ompiler/[Rr]esources/**/*.js +*launchSettings.json +global.json +BenchmarkDotNet.Artifacts/ diff --git a/src/Routing/Directory.Build.props b/src/Routing/Directory.Build.props new file mode 100644 index 0000000000..fc39fae5b4 --- /dev/null +++ b/src/Routing/Directory.Build.props @@ -0,0 +1,20 @@ + + + + + + + + + Microsoft ASP.NET Core + https://github.com/aspnet/Routing + git + $(MSBuildThisFileDirectory) + $(MSBuildThisFileDirectory)build\Key.snk + true + true + true + + diff --git a/src/Routing/Directory.Build.targets b/src/Routing/Directory.Build.targets new file mode 100644 index 0000000000..53b3f6e1da --- /dev/null +++ b/src/Routing/Directory.Build.targets @@ -0,0 +1,7 @@ + + + $(MicrosoftNETCoreApp20PackageVersion) + $(MicrosoftNETCoreApp21PackageVersion) + $(NETStandardLibrary20PackageVersion) + + diff --git a/src/Routing/NuGetPackageVerifier.json b/src/Routing/NuGetPackageVerifier.json new file mode 100644 index 0000000000..9ed455034c --- /dev/null +++ b/src/Routing/NuGetPackageVerifier.json @@ -0,0 +1,13 @@ +{ + "adx-nonshipping": { + "rules": [], + "packages": { + "Microsoft.AspNetCore.Routing.DecisionTree.Sources": {} + } + }, + "Default": { + "rules": [ + "DefaultCompositeRule" + ] + } +} \ No newline at end of file diff --git a/src/Routing/README.md b/src/Routing/README.md new file mode 100644 index 0000000000..3c0794570b --- /dev/null +++ b/src/Routing/README.md @@ -0,0 +1,10 @@ +ASP.NET Routing +=== + +AppVeyor: [![AppVeyor](https://ci.appveyor.com/api/projects/status/fe4o5h1s9ve86nyv/branch/dev?svg=true)](https://ci.appveyor.com/project/aspnetci/Routing/branch/dev) + +Travis: [![Travis](https://travis-ci.org/aspnet/Routing.svg?branch=dev)](https://travis-ci.org/aspnet/Routing) + +Contains routing middleware for routing requests to application logic. + +This project is part of ASP.NET Core. You can find samples, documentation and getting started instructions for ASP.NET Core at the [Home](https://github.com/aspnet/home) repo. diff --git a/src/Routing/Routing.sln b/src/Routing/Routing.sln new file mode 100644 index 0000000000..13657187fa --- /dev/null +++ b/src/Routing/Routing.sln @@ -0,0 +1,165 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.27106.3000 +MinimumVisualStudioVersion = 15.0.26730.03 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{0E966C37-7334-4D96-AAF6-9F49FBD166E3}" + ProjectSection(SolutionItems) = preProject + src\Directory.Build.props = src\Directory.Build.props + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{C3ADD55B-B9C7-4061-8AD4-6A70D1AE3B2E}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{95359B4B-4C85-4B44-A75B-0621905C4CF6}" + ProjectSection(SolutionItems) = preProject + test\Directory.Build.props = test\Directory.Build.props + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Routing", "src\Microsoft.AspNetCore.Routing\Microsoft.AspNetCore.Routing.csproj", "{1EE54D32-6CED-4206-ACF5-3DC1DD39D228}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Routing.Tests", "test\Microsoft.AspNetCore.Routing.Tests\Microsoft.AspNetCore.Routing.Tests.csproj", "{636D79ED-7B32-487C-BDA5-D2A1AAA97371}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RoutingSample.Web", "samples\RoutingSample.Web\RoutingSample.Web.csproj", "{DB94E647-C73A-4F52-A126-AA7544CCF33B}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{C430C499-382D-47BD-B351-CF8F89C08CD2}" + ProjectSection(SolutionItems) = preProject + global.json = global.json + NuGet.config = NuGet.config + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Routing.DecisionTree.Sources.Tests", "test\Microsoft.AspNetCore.Routing.DecisionTree.Sources.Tests\Microsoft.AspNetCore.Routing.DecisionTree.Sources.Tests.csproj", "{09C2933C-23AC-41B7-994D-E8A5184A629C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Routing.Abstractions", "src\Microsoft.AspNetCore.Routing.Abstractions\Microsoft.AspNetCore.Routing.Abstractions.csproj", "{ED253B01-24F1-43D1-AA0B-079391E105A9}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests", "test\Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests\Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests.csproj", "{741B0B05-CE96-473B-B962-6B0A347DF79A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Routing.FunctionalTests", "test\Microsoft.AspNetCore.Routing.FunctionalTests\Microsoft.AspNetCore.Routing.FunctionalTests.csproj", "{5C73140B-41F3-466F-A07B-3614E4D80DF9}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{6DC6B416-C8C4-4BFA-8C1E-A55A6D7EFD08}" + ProjectSection(SolutionItems) = preProject + build\dependencies.props = build\dependencies.props + build\Key.snk = build\Key.snk + build\repo.props = build\repo.props + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Routing.Performance", "benchmarks\Microsoft.AspNetCore.Routing.Performance\Microsoft.AspNetCore.Routing.Performance.csproj", "{F3D86714-4E64-41A6-9B36-A47B3683CF5D}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "benchmarks", "benchmarks", "{D5F39F59-5725-4127-82E7-67028D006185}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|Mixed Platforms = Debug|Mixed Platforms + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|Mixed Platforms = Release|Mixed Platforms + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {1EE54D32-6CED-4206-ACF5-3DC1DD39D228}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1EE54D32-6CED-4206-ACF5-3DC1DD39D228}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1EE54D32-6CED-4206-ACF5-3DC1DD39D228}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {1EE54D32-6CED-4206-ACF5-3DC1DD39D228}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {1EE54D32-6CED-4206-ACF5-3DC1DD39D228}.Debug|x86.ActiveCfg = Debug|Any CPU + {1EE54D32-6CED-4206-ACF5-3DC1DD39D228}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1EE54D32-6CED-4206-ACF5-3DC1DD39D228}.Release|Any CPU.Build.0 = Release|Any CPU + {1EE54D32-6CED-4206-ACF5-3DC1DD39D228}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {1EE54D32-6CED-4206-ACF5-3DC1DD39D228}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {1EE54D32-6CED-4206-ACF5-3DC1DD39D228}.Release|x86.ActiveCfg = Release|Any CPU + {636D79ED-7B32-487C-BDA5-D2A1AAA97371}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {636D79ED-7B32-487C-BDA5-D2A1AAA97371}.Debug|Any CPU.Build.0 = Debug|Any CPU + {636D79ED-7B32-487C-BDA5-D2A1AAA97371}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {636D79ED-7B32-487C-BDA5-D2A1AAA97371}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {636D79ED-7B32-487C-BDA5-D2A1AAA97371}.Debug|x86.ActiveCfg = Debug|Any CPU + {636D79ED-7B32-487C-BDA5-D2A1AAA97371}.Release|Any CPU.ActiveCfg = Release|Any CPU + {636D79ED-7B32-487C-BDA5-D2A1AAA97371}.Release|Any CPU.Build.0 = Release|Any CPU + {636D79ED-7B32-487C-BDA5-D2A1AAA97371}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {636D79ED-7B32-487C-BDA5-D2A1AAA97371}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {636D79ED-7B32-487C-BDA5-D2A1AAA97371}.Release|x86.ActiveCfg = Release|Any CPU + {DB94E647-C73A-4F52-A126-AA7544CCF33B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DB94E647-C73A-4F52-A126-AA7544CCF33B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DB94E647-C73A-4F52-A126-AA7544CCF33B}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {DB94E647-C73A-4F52-A126-AA7544CCF33B}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {DB94E647-C73A-4F52-A126-AA7544CCF33B}.Debug|x86.ActiveCfg = Debug|Any CPU + {DB94E647-C73A-4F52-A126-AA7544CCF33B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DB94E647-C73A-4F52-A126-AA7544CCF33B}.Release|Any CPU.Build.0 = Release|Any CPU + {DB94E647-C73A-4F52-A126-AA7544CCF33B}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {DB94E647-C73A-4F52-A126-AA7544CCF33B}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {DB94E647-C73A-4F52-A126-AA7544CCF33B}.Release|x86.ActiveCfg = Release|Any CPU + {09C2933C-23AC-41B7-994D-E8A5184A629C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {09C2933C-23AC-41B7-994D-E8A5184A629C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {09C2933C-23AC-41B7-994D-E8A5184A629C}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {09C2933C-23AC-41B7-994D-E8A5184A629C}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {09C2933C-23AC-41B7-994D-E8A5184A629C}.Debug|x86.ActiveCfg = Debug|Any CPU + {09C2933C-23AC-41B7-994D-E8A5184A629C}.Debug|x86.Build.0 = Debug|Any CPU + {09C2933C-23AC-41B7-994D-E8A5184A629C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {09C2933C-23AC-41B7-994D-E8A5184A629C}.Release|Any CPU.Build.0 = Release|Any CPU + {09C2933C-23AC-41B7-994D-E8A5184A629C}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {09C2933C-23AC-41B7-994D-E8A5184A629C}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {09C2933C-23AC-41B7-994D-E8A5184A629C}.Release|x86.ActiveCfg = Release|Any CPU + {09C2933C-23AC-41B7-994D-E8A5184A629C}.Release|x86.Build.0 = Release|Any CPU + {ED253B01-24F1-43D1-AA0B-079391E105A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ED253B01-24F1-43D1-AA0B-079391E105A9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ED253B01-24F1-43D1-AA0B-079391E105A9}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {ED253B01-24F1-43D1-AA0B-079391E105A9}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {ED253B01-24F1-43D1-AA0B-079391E105A9}.Debug|x86.ActiveCfg = Debug|Any CPU + {ED253B01-24F1-43D1-AA0B-079391E105A9}.Debug|x86.Build.0 = Debug|Any CPU + {ED253B01-24F1-43D1-AA0B-079391E105A9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ED253B01-24F1-43D1-AA0B-079391E105A9}.Release|Any CPU.Build.0 = Release|Any CPU + {ED253B01-24F1-43D1-AA0B-079391E105A9}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {ED253B01-24F1-43D1-AA0B-079391E105A9}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {ED253B01-24F1-43D1-AA0B-079391E105A9}.Release|x86.ActiveCfg = Release|Any CPU + {ED253B01-24F1-43D1-AA0B-079391E105A9}.Release|x86.Build.0 = Release|Any CPU + {741B0B05-CE96-473B-B962-6B0A347DF79A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {741B0B05-CE96-473B-B962-6B0A347DF79A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {741B0B05-CE96-473B-B962-6B0A347DF79A}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {741B0B05-CE96-473B-B962-6B0A347DF79A}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {741B0B05-CE96-473B-B962-6B0A347DF79A}.Debug|x86.ActiveCfg = Debug|Any CPU + {741B0B05-CE96-473B-B962-6B0A347DF79A}.Debug|x86.Build.0 = Debug|Any CPU + {741B0B05-CE96-473B-B962-6B0A347DF79A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {741B0B05-CE96-473B-B962-6B0A347DF79A}.Release|Any CPU.Build.0 = Release|Any CPU + {741B0B05-CE96-473B-B962-6B0A347DF79A}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {741B0B05-CE96-473B-B962-6B0A347DF79A}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {741B0B05-CE96-473B-B962-6B0A347DF79A}.Release|x86.ActiveCfg = Release|Any CPU + {741B0B05-CE96-473B-B962-6B0A347DF79A}.Release|x86.Build.0 = Release|Any CPU + {5C73140B-41F3-466F-A07B-3614E4D80DF9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5C73140B-41F3-466F-A07B-3614E4D80DF9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5C73140B-41F3-466F-A07B-3614E4D80DF9}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {5C73140B-41F3-466F-A07B-3614E4D80DF9}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {5C73140B-41F3-466F-A07B-3614E4D80DF9}.Debug|x86.ActiveCfg = Debug|Any CPU + {5C73140B-41F3-466F-A07B-3614E4D80DF9}.Debug|x86.Build.0 = Debug|Any CPU + {5C73140B-41F3-466F-A07B-3614E4D80DF9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5C73140B-41F3-466F-A07B-3614E4D80DF9}.Release|Any CPU.Build.0 = Release|Any CPU + {5C73140B-41F3-466F-A07B-3614E4D80DF9}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {5C73140B-41F3-466F-A07B-3614E4D80DF9}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {5C73140B-41F3-466F-A07B-3614E4D80DF9}.Release|x86.ActiveCfg = Release|Any CPU + {5C73140B-41F3-466F-A07B-3614E4D80DF9}.Release|x86.Build.0 = Release|Any CPU + {F3D86714-4E64-41A6-9B36-A47B3683CF5D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F3D86714-4E64-41A6-9B36-A47B3683CF5D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F3D86714-4E64-41A6-9B36-A47B3683CF5D}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {F3D86714-4E64-41A6-9B36-A47B3683CF5D}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {F3D86714-4E64-41A6-9B36-A47B3683CF5D}.Debug|x86.ActiveCfg = Debug|Any CPU + {F3D86714-4E64-41A6-9B36-A47B3683CF5D}.Debug|x86.Build.0 = Debug|Any CPU + {F3D86714-4E64-41A6-9B36-A47B3683CF5D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F3D86714-4E64-41A6-9B36-A47B3683CF5D}.Release|Any CPU.Build.0 = Release|Any CPU + {F3D86714-4E64-41A6-9B36-A47B3683CF5D}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {F3D86714-4E64-41A6-9B36-A47B3683CF5D}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {F3D86714-4E64-41A6-9B36-A47B3683CF5D}.Release|x86.ActiveCfg = Release|Any CPU + {F3D86714-4E64-41A6-9B36-A47B3683CF5D}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {1EE54D32-6CED-4206-ACF5-3DC1DD39D228} = {0E966C37-7334-4D96-AAF6-9F49FBD166E3} + {636D79ED-7B32-487C-BDA5-D2A1AAA97371} = {95359B4B-4C85-4B44-A75B-0621905C4CF6} + {DB94E647-C73A-4F52-A126-AA7544CCF33B} = {C3ADD55B-B9C7-4061-8AD4-6A70D1AE3B2E} + {09C2933C-23AC-41B7-994D-E8A5184A629C} = {95359B4B-4C85-4B44-A75B-0621905C4CF6} + {ED253B01-24F1-43D1-AA0B-079391E105A9} = {0E966C37-7334-4D96-AAF6-9F49FBD166E3} + {741B0B05-CE96-473B-B962-6B0A347DF79A} = {95359B4B-4C85-4B44-A75B-0621905C4CF6} + {5C73140B-41F3-466F-A07B-3614E4D80DF9} = {95359B4B-4C85-4B44-A75B-0621905C4CF6} + {F3D86714-4E64-41A6-9B36-A47B3683CF5D} = {D5F39F59-5725-4127-82E7-67028D006185} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {36C8D815-B7F1-479D-894B-E606FB8DECDA} + EndGlobalSection +EndGlobal diff --git a/src/Routing/benchmarks/Microsoft.AspNetCore.Routing.Performance/Configs/CoreConfig.cs b/src/Routing/benchmarks/Microsoft.AspNetCore.Routing.Performance/Configs/CoreConfig.cs new file mode 100644 index 0000000000..bbeb805e6d --- /dev/null +++ b/src/Routing/benchmarks/Microsoft.AspNetCore.Routing.Performance/Configs/CoreConfig.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. + +using BenchmarkDotNet.Columns; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Engines; +using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Validators; + +namespace Microsoft.AspNetCore.Routing.Performance +{ + public class CoreConfig : ManualConfig + { + public CoreConfig() + { + Add(JitOptimizationsValidator.FailOnError); + Add(MemoryDiagnoser.Default); + Add(StatisticColumn.OperationsPerSecond); + + Add(Job.Default + .With(BenchmarkDotNet.Environments.Runtime.Core) + .WithRemoveOutliers(false) + .With(new GcMode() { Server = true }) + .With(RunStrategy.Throughput) + .WithLaunchCount(3) + .WithWarmupCount(5) + .WithTargetCount(10)); + } + } +} \ No newline at end of file diff --git a/src/Routing/benchmarks/Microsoft.AspNetCore.Routing.Performance/Microsoft.AspNetCore.Routing.Performance.csproj b/src/Routing/benchmarks/Microsoft.AspNetCore.Routing.Performance/Microsoft.AspNetCore.Routing.Performance.csproj new file mode 100644 index 0000000000..453925e513 --- /dev/null +++ b/src/Routing/benchmarks/Microsoft.AspNetCore.Routing.Performance/Microsoft.AspNetCore.Routing.Performance.csproj @@ -0,0 +1,22 @@ + + + + netcoreapp2.0;net461 + netcoreapp2.0 + Exe + true + true + false + + + + + + + + + + + + + diff --git a/src/Routing/benchmarks/Microsoft.AspNetCore.Routing.Performance/Program.cs b/src/Routing/benchmarks/Microsoft.AspNetCore.Routing.Performance/Program.cs new file mode 100644 index 0000000000..b510a13a28 --- /dev/null +++ b/src/Routing/benchmarks/Microsoft.AspNetCore.Routing.Performance/Program.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 System; +using System.Reflection; +using BenchmarkDotNet.Running; + +namespace Microsoft.AspNetCore.Routing.Performance +{ + public class Program + { + public static void Main(string[] args) + { + BenchmarkSwitcher.FromAssembly(typeof(Program).GetTypeInfo().Assembly).Run(args); + } + } +} \ No newline at end of file diff --git a/src/Routing/benchmarks/Microsoft.AspNetCore.Routing.Performance/RoutingBenchmark.cs b/src/Routing/benchmarks/Microsoft.AspNetCore.Routing.Performance/RoutingBenchmark.cs new file mode 100644 index 0000000000..d499fd706a --- /dev/null +++ b/src/Routing/benchmarks/Microsoft.AspNetCore.Routing.Performance/RoutingBenchmark.cs @@ -0,0 +1,114 @@ +// 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 System.Text.Encodings.Web; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing.Internal; +using Microsoft.AspNetCore.Routing.Template; +using Microsoft.AspNetCore.Routing.Tree; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.ObjectPool; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Routing.Performance +{ + public class RoutingBenchmark + { + private const int NumberOfRequestTypes = 3; + private const int Iterations = 100; + + private readonly IRouter _treeRouter; + private readonly RequestEntry[] _requests; + + public RoutingBenchmark() + { + var handler = new RouteHandler((next) => Task.FromResult(null)); + + var treeBuilder = new TreeRouteBuilder( + NullLoggerFactory.Instance, + new DefaultObjectPool(new UriBuilderContextPooledObjectPolicy()), + new DefaultInlineConstraintResolver(new OptionsManager(new OptionsFactory(Enumerable.Empty>(), Enumerable.Empty>())))); + + treeBuilder.MapInbound(handler, TemplateParser.Parse("api/Widgets"), "default", 0); + treeBuilder.MapInbound(handler, TemplateParser.Parse("api/Widgets/{id}"), "default", 0); + treeBuilder.MapInbound(handler, TemplateParser.Parse("api/Widgets/search/{term}"), "default", 0); + treeBuilder.MapInbound(handler, TemplateParser.Parse("admin/users/{id}"), "default", 0); + treeBuilder.MapInbound(handler, TemplateParser.Parse("admin/users/{id}/manage"), "default", 0); + + _treeRouter = treeBuilder.Build(); + + _requests = new RequestEntry[NumberOfRequestTypes]; + + _requests[0].HttpContext = new DefaultHttpContext(); + _requests[0].HttpContext.Request.Path = "/api/Widgets/5"; + _requests[0].IsMatch = true; + _requests[0].Values = new RouteValueDictionary(new { id = 5 }); + + _requests[1].HttpContext = new DefaultHttpContext(); + _requests[1].HttpContext.Request.Path = "/admin/users/17/mAnage"; + _requests[1].IsMatch = true; + _requests[1].Values = new RouteValueDictionary(new { id = 17 }); + + _requests[2].HttpContext = new DefaultHttpContext(); + _requests[2].HttpContext.Request.Path = "/api/Widgets/search/dldldldldld/ddld"; + _requests[2].IsMatch = false; + _requests[2].Values = new RouteValueDictionary(); + } + + [Benchmark(Description = "Attribute Routing", OperationsPerInvoke = Iterations * NumberOfRequestTypes)] + public async Task AttributeRouting() + { + for (var i = 0; i < Iterations; i++) + { + for (var j = 0; j < _requests.Length; j++) + { + var context = new RouteContext(_requests[j].HttpContext); + + await _treeRouter.RouteAsync(context); + + Verify(context, j); + } + } + } + + private void Verify(RouteContext context, int i) + { + if (_requests[i].IsMatch) + { + if (context.Handler == null) + { + throw new InvalidOperationException($"Failed {i}"); + } + + var values = _requests[i].Values; + if (values.Count != context.RouteData.Values.Count) + { + throw new InvalidOperationException($"Failed {i}"); + } + } + else + { + if (context.Handler != null) + { + throw new InvalidOperationException($"Failed {i}"); + } + + if (context.RouteData.Values.Count != 0) + { + throw new InvalidOperationException($"Failed {i}"); + } + } + } + + private struct RequestEntry + { + public HttpContext HttpContext; + public bool IsMatch; + public RouteValueDictionary Values; + } + } +} \ No newline at end of file diff --git a/src/Routing/benchmarks/Microsoft.AspNetCore.Routing.Performance/readme.md b/src/Routing/benchmarks/Microsoft.AspNetCore.Routing.Performance/readme.md new file mode 100644 index 0000000000..38ce0ff71a --- /dev/null +++ b/src/Routing/benchmarks/Microsoft.AspNetCore.Routing.Performance/readme.md @@ -0,0 +1,11 @@ +Compile the solution in Release mode (so binaries are available in release) + +To run a specific benchmark add it as parameter. +``` +dotnet run -c Release +``` + +If you run without any parameters, you'll be offered the list of all benchmarks and get to choose. +``` +dotnet run -c Release +``` \ No newline at end of file diff --git a/src/Routing/build/Key.snk b/src/Routing/build/Key.snk new file mode 100644 index 0000000000..e10e4889c1 Binary files /dev/null and b/src/Routing/build/Key.snk differ diff --git a/src/Routing/build/dependencies.props b/src/Routing/build/dependencies.props new file mode 100644 index 0000000000..6aafe4467f --- /dev/null +++ b/src/Routing/build/dependencies.props @@ -0,0 +1,46 @@ + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + + + + + 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 + 2.1.1 + 2.1.1 + 2.1.1 + 2.1.2 + 2.1.1 + 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 + + \ No newline at end of file diff --git a/src/Routing/build/repo.props b/src/Routing/build/repo.props new file mode 100644 index 0000000000..dab1601c88 --- /dev/null +++ b/src/Routing/build/repo.props @@ -0,0 +1,15 @@ + + + + + + Internal.AspNetCore.Universe.Lineup + 2.1.0-rc1-* + https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json + + + + + + + diff --git a/src/Routing/build/sources.props b/src/Routing/build/sources.props new file mode 100644 index 0000000000..9215df9751 --- /dev/null +++ b/src/Routing/build/sources.props @@ -0,0 +1,17 @@ + + + + + $(DotNetRestoreSources) + + $(RestoreSources); + https://dotnet.myget.org/F/dotnet-core/api/v3/index.json; + https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json; + https://dotnet.myget.org/F/aspnetcore-tools/api/v3/index.json; + + + $(RestoreSources); + https://api.nuget.org/v3/index.json; + + + diff --git a/src/Routing/samples/RoutingSample.Web/PrefixRoute.cs b/src/Routing/samples/RoutingSample.Web/PrefixRoute.cs new file mode 100644 index 0000000000..908e8a5462 --- /dev/null +++ b/src/Routing/samples/RoutingSample.Web/PrefixRoute.cs @@ -0,0 +1,62 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Routing; + +namespace RoutingSample.Web +{ + public class PrefixRoute : IRouter + { + private readonly IRouteHandler _target; + private readonly string _prefix; + + public PrefixRoute(IRouteHandler target, string prefix) + { + _target = target; + + if (prefix == null) + { + prefix = "/"; + } + else if (prefix.Length > 0 && prefix[0] != '/') + { + // owin.RequestPath starts with a / + prefix = "/" + prefix; + } + + if (prefix.Length > 1 && prefix[prefix.Length - 1] == '/') + { + prefix = prefix.Substring(0, prefix.Length - 1); + } + + _prefix = prefix; + } + + public Task RouteAsync(RouteContext context) + { + var requestPath = context.HttpContext.Request.Path.Value ?? string.Empty; + if (requestPath.StartsWith(_prefix, StringComparison.OrdinalIgnoreCase)) + { + if (requestPath.Length > _prefix.Length) + { + var lastCharacter = requestPath[_prefix.Length]; + if (lastCharacter != '/' && lastCharacter != '#' && lastCharacter != '?') + { + return Task.FromResult(0); + } + } + + context.Handler = _target.GetRequestHandler(context.HttpContext, context.RouteData); + } + + return Task.FromResult(0); + } + + public VirtualPathData GetVirtualPath(VirtualPathContext context) + { + return null; + } + } +} \ No newline at end of file diff --git a/src/Routing/samples/RoutingSample.Web/Program.cs b/src/Routing/samples/RoutingSample.Web/Program.cs new file mode 100644 index 0000000000..27a1a8b55e --- /dev/null +++ b/src/Routing/samples/RoutingSample.Web/Program.cs @@ -0,0 +1,50 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Text.RegularExpressions; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Constraints; +using Microsoft.Extensions.DependencyInjection; + +namespace RoutingSample.Web +{ + public class Program + { + private static readonly TimeSpan RegexMatchTimeout = TimeSpan.FromSeconds(10); + + public static void Main(string[] args) + { + var webHost = GetWebHostBuilder().Build(); + webHost.Run(); + } + + // For unit testing + public static IWebHostBuilder GetWebHostBuilder() + { + return new WebHostBuilder() + .UseKestrel() + .UseIISIntegration() + .ConfigureServices(services => services.AddRouting()) + .Configure(app => app.UseRouter(routes => + { + routes.DefaultHandler = new RouteHandler((httpContext) => + { + var request = httpContext.Request; + return httpContext.Response.WriteAsync($"Verb = {request.Method.ToUpperInvariant()} - Path = {request.Path} - Route values - {string.Join(", ", httpContext.GetRouteData().Values)}"); + }); + + routes.MapGet("api/get/{id}", (request, response, routeData) => response.WriteAsync($"API Get {routeData.Values["id"]}")) + .MapMiddlewareRoute("api/middleware", (appBuilder) => appBuilder.Use((httpContext, next) => httpContext.Response.WriteAsync("Middleware!"))) + .MapRoute( + name: "AllVerbs", + template: "api/all/{name}/{lastName?}", + defaults: new { lastName = "Doe" }, + constraints: new { lastName = new RegexRouteConstraint(new Regex("[a-zA-Z]{3}", RegexOptions.CultureInvariant, RegexMatchTimeout)) }); + })); + } + } +} diff --git a/src/Routing/samples/RoutingSample.Web/RouteBuilderExtensions.cs b/src/Routing/samples/RoutingSample.Web/RouteBuilderExtensions.cs new file mode 100644 index 0000000000..d81735bbc2 --- /dev/null +++ b/src/Routing/samples/RoutingSample.Web/RouteBuilderExtensions.cs @@ -0,0 +1,43 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace RoutingSample.Web +{ + public static class RouteBuilderExtensions + { + public static IRouteBuilder AddPrefixRoute( + this IRouteBuilder routeBuilder, + string prefix, + IRouteHandler handler) + { + routeBuilder.Routes.Add(new PrefixRoute(handler, prefix)); + return routeBuilder; + } + + public static IRouteBuilder MapLocaleRoute( + this IRouteBuilder routeBuilder, + string locale, + string routeTemplate, + object defaults) + { + var defaultsDictionary = new RouteValueDictionary(defaults); + defaultsDictionary.Add("locale", locale); + + var constraintResolver = routeBuilder.ServiceProvider.GetService(); + + var route = new Route( + target: routeBuilder.DefaultHandler, + routeTemplate: routeTemplate, + defaults: defaultsDictionary, + constraints: null, + dataTokens: null, + inlineConstraintResolver: constraintResolver); + routeBuilder.Routes.Add(route); + + return routeBuilder; + } + } +} \ No newline at end of file diff --git a/src/Routing/samples/RoutingSample.Web/RoutingSample.Web.csproj b/src/Routing/samples/RoutingSample.Web/RoutingSample.Web.csproj new file mode 100644 index 0000000000..9c3e55c557 --- /dev/null +++ b/src/Routing/samples/RoutingSample.Web/RoutingSample.Web.csproj @@ -0,0 +1,17 @@ + + + + netcoreapp2.1;netcoreapp2.0 + $(TargetFrameworks);net461 + + + + + + + + + + + + diff --git a/src/Routing/shared/Microsoft.AspNetCore.Routing.DecisionTree.Sources/DecisionCriterion.cs b/src/Routing/shared/Microsoft.AspNetCore.Routing.DecisionTree.Sources/DecisionCriterion.cs new file mode 100644 index 0000000000..efc9c742d2 --- /dev/null +++ b/src/Routing/shared/Microsoft.AspNetCore.Routing.DecisionTree.Sources/DecisionCriterion.cs @@ -0,0 +1,14 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace Microsoft.AspNetCore.Routing.DecisionTree +{ + internal class DecisionCriterion + { + public string Key { get; set; } + + public Dictionary> Branches { get; set; } + } +} \ No newline at end of file diff --git a/src/Routing/shared/Microsoft.AspNetCore.Routing.DecisionTree.Sources/DecisionCriterionValue.cs b/src/Routing/shared/Microsoft.AspNetCore.Routing.DecisionTree.Sources/DecisionCriterionValue.cs new file mode 100644 index 0000000000..e5ed7a69f8 --- /dev/null +++ b/src/Routing/shared/Microsoft.AspNetCore.Routing.DecisionTree.Sources/DecisionCriterionValue.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. + +namespace Microsoft.AspNetCore.Routing.DecisionTree +{ + internal struct DecisionCriterionValue + { + private readonly object _value; + + public DecisionCriterionValue(object value) + { + _value = value; + } + + public object Value + { + get { return _value; } + } + } +} \ No newline at end of file diff --git a/src/Routing/shared/Microsoft.AspNetCore.Routing.DecisionTree.Sources/DecisionCriterionValueEqualityComparer.cs b/src/Routing/shared/Microsoft.AspNetCore.Routing.DecisionTree.Sources/DecisionCriterionValueEqualityComparer.cs new file mode 100644 index 0000000000..949b7b613c --- /dev/null +++ b/src/Routing/shared/Microsoft.AspNetCore.Routing.DecisionTree.Sources/DecisionCriterionValueEqualityComparer.cs @@ -0,0 +1,27 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace Microsoft.AspNetCore.Routing.DecisionTree +{ + internal class DecisionCriterionValueEqualityComparer : IEqualityComparer + { + public DecisionCriterionValueEqualityComparer(IEqualityComparer innerComparer) + { + InnerComparer = innerComparer; + } + + public IEqualityComparer InnerComparer { get; private set; } + + public bool Equals(DecisionCriterionValue x, DecisionCriterionValue y) + { + return InnerComparer.Equals(x.Value, y.Value); + } + + public int GetHashCode(DecisionCriterionValue obj) + { + return InnerComparer.GetHashCode(obj.Value); + } + } +} \ No newline at end of file diff --git a/src/Routing/shared/Microsoft.AspNetCore.Routing.DecisionTree.Sources/DecisionTreeBuilder.cs b/src/Routing/shared/Microsoft.AspNetCore.Routing.DecisionTree.Sources/DecisionTreeBuilder.cs new file mode 100644 index 0000000000..76792f6793 --- /dev/null +++ b/src/Routing/shared/Microsoft.AspNetCore.Routing.DecisionTree.Sources/DecisionTreeBuilder.cs @@ -0,0 +1,225 @@ +// 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.DecisionTree +{ + // This code generates a minimal tree of decision criteria that map known categorical data + // (key-value-pairs) to a set of inputs. Action Selection is the best example of how this + // can be used, so the comments here will describe the process from the point-of-view, + // though the decision tree is generally applicable to like-problems. + // + // Care has been taken here to keep the performance of building the data-structure at a + // reasonable level, as this has an impact on startup cost for action selection. Additionally + // we want to hold on to the minimal amount of memory needed once we've built the tree. + // + // Ex: + // Given actions like the following, create a decision tree that will help action + // selection work efficiently. + // + // Given any set of route data it should be possible to traverse the tree using the + // presence our route data keys (like action), and whether or not they match any of + // the known values for that route data key, to find the set of actions that match + // the route data. + // + // Actions: + // + // { controller = "Home", action = "Index" } + // { controller = "Products", action = "Index" } + // { controller = "Products", action = "Buy" } + // { area = "Admin", controller = "Users", action = "AddUser" } + // + // The generated tree looks like this (json-like-notation): + // + // { + // action : { + // "AddUser" : { + // controller : { + // "Users" : { + // area : { + // "Admin" : match { area = "Admin", controller = "Users", action = "AddUser" } + // } + // } + // } + // }, + // "Buy" : { + // controller : { + // "Products" : { + // area : { + // null : match { controller = "Products", action = "Buy" } + // } + // } + // } + // }, + // "Index" : { + // controller : { + // "Home" : { + // area : { + // null : match { controller = "Home", action = "Index" } + // } + // } + // "Products" : { + // area : { + // "null" : match { controller = "Products", action = "Index" } + // } + // } + // } + // } + // } + // } + internal static class DecisionTreeBuilder + { + public static DecisionTreeNode GenerateTree(IReadOnlyList items, IClassifier classifier) + { + var itemDescriptors = new List>(); + for (var i = 0; i < items.Count; i++) + { + itemDescriptors.Add(new ItemDescriptor() + { + Criteria = classifier.GetCriteria(items[i]), + Index = i, + Item = items[i], + }); + } + + var comparer = new DecisionCriterionValueEqualityComparer(classifier.ValueComparer); + return GenerateNode( + new TreeBuilderContext(), + comparer, + itemDescriptors); + } + + private static DecisionTreeNode GenerateNode( + TreeBuilderContext context, + DecisionCriterionValueEqualityComparer comparer, + IList> items) + { + // The extreme use of generics here is intended to reduce the number of intermediate + // allocations of wrapper classes. Performance testing found that building these trees allocates + // significant memory that we can avoid and that it has a real impact on startup. + var criteria = new Dictionary(StringComparer.OrdinalIgnoreCase); + + // Matches are items that have no remaining criteria - at this point in the tree + // they are considered accepted. + var matches = new List(); + + // For each item in the working set, we want to map it to it's possible criteria-branch + // pairings, then reduce that tree to the minimal set. + foreach (var item in items) + { + var unsatisfiedCriteria = 0; + + foreach (var kvp in item.Criteria) + { + // context.CurrentCriteria is the logical 'stack' of criteria that we've already processed + // on this branch of the tree. + if (context.CurrentCriteria.Contains(kvp.Key)) + { + continue; + } + + unsatisfiedCriteria++; + + Criterion criterion; + if (!criteria.TryGetValue(kvp.Key, out criterion)) + { + criterion = new Criterion(comparer); + criteria.Add(kvp.Key, criterion); + } + + List> branch; + if (!criterion.TryGetValue(kvp.Value, out branch)) + { + branch = new List>(); + criterion.Add(kvp.Value, branch); + } + + branch.Add(item); + } + + // If all of the criteria on item are satisfied by the 'stack' then this item is a match. + if (unsatisfiedCriteria == 0) + { + matches.Add(item.Item); + } + } + + // Iterate criteria in order of branchiness to determine which one to explore next. If a criterion + // has no 'new' matches under it then we can just eliminate that part of the tree. + var reducedCriteria = new List>(); + foreach (var criterion in criteria.OrderByDescending(c => c.Value.Count)) + { + var reducedBranches = new Dictionary>(comparer.InnerComparer); + + foreach (var branch in criterion.Value) + { + var reducedItems = new List>(); + foreach (var item in branch.Value) + { + if (context.MatchedItems.Add(item)) + { + reducedItems.Add(item); + } + } + + if (reducedItems.Count > 0) + { + var childContext = new TreeBuilderContext(context); + childContext.CurrentCriteria.Add(criterion.Key); + + var newBranch = GenerateNode(childContext, comparer, branch.Value); + reducedBranches.Add(branch.Key.Value, newBranch); + } + } + + if (reducedBranches.Count > 0) + { + var newCriterion = new DecisionCriterion() + { + Key = criterion.Key, + Branches = reducedBranches, + }; + + reducedCriteria.Add(newCriterion); + } + } + + return new DecisionTreeNode() + { + Criteria = reducedCriteria.ToList(), + Matches = matches, + }; + } + + private class TreeBuilderContext + { + public TreeBuilderContext() + { + CurrentCriteria = new HashSet(StringComparer.OrdinalIgnoreCase); + MatchedItems = new HashSet>(); + } + + public TreeBuilderContext(TreeBuilderContext other) + { + CurrentCriteria = new HashSet(other.CurrentCriteria, StringComparer.OrdinalIgnoreCase); + MatchedItems = new HashSet>(); + } + + public HashSet CurrentCriteria { get; private set; } + + public HashSet> MatchedItems { get; private set; } + } + + // Subclass just to give a logical name to a mess of generics + private class Criterion : Dictionary>> + { + public Criterion(DecisionCriterionValueEqualityComparer comparer) + : base(comparer) + { + } + } + } +} diff --git a/src/Routing/shared/Microsoft.AspNetCore.Routing.DecisionTree.Sources/DecisionTreeNode.cs b/src/Routing/shared/Microsoft.AspNetCore.Routing.DecisionTree.Sources/DecisionTreeNode.cs new file mode 100644 index 0000000000..1be3064c62 --- /dev/null +++ b/src/Routing/shared/Microsoft.AspNetCore.Routing.DecisionTree.Sources/DecisionTreeNode.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.DecisionTree +{ + // Data structure representing a node in a decision tree. These are created in DecisionTreeBuilder + // and walked to find a set of items matching some input criteria. + internal class DecisionTreeNode + { + // The list of matches for the current node. This represents a set of items that have had all + // of their criteria matched if control gets to this point in the tree. + public IList Matches { get; set; } + + // Additional criteria that further branch out from this node. Walk these to fine more items + // matching the input data. + public IList> Criteria { get; set; } + } +} \ No newline at end of file diff --git a/src/Routing/shared/Microsoft.AspNetCore.Routing.DecisionTree.Sources/IClassifier.cs b/src/Routing/shared/Microsoft.AspNetCore.Routing.DecisionTree.Sources/IClassifier.cs new file mode 100644 index 0000000000..3008fdfa75 --- /dev/null +++ b/src/Routing/shared/Microsoft.AspNetCore.Routing.DecisionTree.Sources/IClassifier.cs @@ -0,0 +1,14 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace Microsoft.AspNetCore.Routing.DecisionTree +{ + internal interface IClassifier + { + IDictionary GetCriteria(TItem item); + + IEqualityComparer ValueComparer { get; } + } +} \ No newline at end of file diff --git a/src/Routing/shared/Microsoft.AspNetCore.Routing.DecisionTree.Sources/ItemDescriptor.cs b/src/Routing/shared/Microsoft.AspNetCore.Routing.DecisionTree.Sources/ItemDescriptor.cs new file mode 100644 index 0000000000..84a6279c27 --- /dev/null +++ b/src/Routing/shared/Microsoft.AspNetCore.Routing.DecisionTree.Sources/ItemDescriptor.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.DecisionTree +{ + internal class ItemDescriptor + { + public IDictionary Criteria { get; set; } + + public int Index { get; set; } + + public TItem Item { get; set; } + } +} \ No newline at end of file diff --git a/src/Routing/src/Directory.Build.props b/src/Routing/src/Directory.Build.props new file mode 100644 index 0000000000..1e0980f663 --- /dev/null +++ b/src/Routing/src/Directory.Build.props @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/IRouteConstraint.cs b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/IRouteConstraint.cs new file mode 100644 index 0000000000..00eb916510 --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/IRouteConstraint.cs @@ -0,0 +1,33 @@ +// 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 +{ + /// + /// Defines the contract that a class must implement in order to check whether a URL parameter + /// value is valid for a constraint. + /// + public interface IRouteConstraint + { + /// + /// Determines whether the URL parameter contains a valid value for this constraint. + /// + /// An object that encapsulates information about the HTTP request. + /// The router that this constraint belongs to. + /// The name of the parameter that is being checked. + /// A dictionary that contains the parameters for the URL. + /// + /// An object that indicates whether the constraint check is being performed + /// when an incoming request is being handled or when a URL is being generated. + /// + /// true if the URL parameter contains a valid value; otherwise, false. + bool Match( + HttpContext httpContext, + IRouter route, + string routeKey, + RouteValueDictionary values, + RouteDirection routeDirection); + } +} diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/IRouteHandler.cs b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/IRouteHandler.cs new file mode 100644 index 0000000000..15aaaeda8b --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/IRouteHandler.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 Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Routing +{ + /// + /// Defines a contract for a handler of a route. + /// + public interface IRouteHandler + { + /// + /// Gets a to handle the request, based on the provided + /// . + /// + /// The associated with the current request. + /// The associated with the current routing match. + /// + /// A , or null if the handler cannot handle this request. + /// + RequestDelegate GetRequestHandler(HttpContext httpContext, RouteData routeData); + } +} diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/IRouter.cs b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/IRouter.cs new file mode 100644 index 0000000000..06a62bc9ba --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/IRouter.cs @@ -0,0 +1,14 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Routing +{ + public interface IRouter + { + Task RouteAsync(RouteContext context); + + VirtualPathData GetVirtualPath(VirtualPathContext context); + } +} diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/IRoutingFeature.cs b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/IRoutingFeature.cs new file mode 100644 index 0000000000..bf1897c8ee --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/IRoutingFeature.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. + +namespace Microsoft.AspNetCore.Routing +{ + /// + /// A feature interface for routing functionality. + /// + public interface IRoutingFeature + { + /// + /// Gets or sets the associated with the current request. + /// + RouteData RouteData { get; set; } + } +} diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/Microsoft.AspNetCore.Routing.Abstractions.csproj b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/Microsoft.AspNetCore.Routing.Abstractions.csproj new file mode 100644 index 0000000000..633a49d503 --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/Microsoft.AspNetCore.Routing.Abstractions.csproj @@ -0,0 +1,18 @@ + + + + ASP.NET Core abstractions for routing requests to application logic and for generating links. +Commonly used types: +Microsoft.AspNetCore.Routing.IRouter +Microsoft.AspNetCore.Routing.RouteData + netstandard2.0 + $(NoWarn);CS1591 + true + aspnetcore;routing + + + + + + + diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/Properties/AssemblyInfo.cs b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..848ab925ed --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/Properties/AssemblyInfo.cs @@ -0,0 +1,6 @@ +// 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.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] \ No newline at end of file diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/Properties/Resources.Designer.cs b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/Properties/Resources.Designer.cs new file mode 100644 index 0000000000..8fdf6b8715 --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/Properties/Resources.Designer.cs @@ -0,0 +1,58 @@ +// +namespace Microsoft.AspNetCore.Routing.Abstractions +{ + using System.Globalization; + using System.Reflection; + using System.Resources; + + internal static class Resources + { + private static readonly ResourceManager _resourceManager + = new ResourceManager("Microsoft.AspNetCore.Routing.Abstractions.Resources", typeof(Resources).GetTypeInfo().Assembly); + + /// + /// An element with the key '{0}' already exists in the {1}. + /// + internal static string RouteValueDictionary_DuplicateKey + { + get => GetString("RouteValueDictionary_DuplicateKey"); + } + + /// + /// An element with the key '{0}' already exists in the {1}. + /// + internal static string FormatRouteValueDictionary_DuplicateKey(object p0, object p1) + => string.Format(CultureInfo.CurrentCulture, GetString("RouteValueDictionary_DuplicateKey"), p0, p1); + + /// + /// The type '{0}' defines properties '{1}' and '{2}' which differ only by casing. This is not supported by {3} which uses case-insensitive comparisons. + /// + internal static string RouteValueDictionary_DuplicatePropertyName + { + get => GetString("RouteValueDictionary_DuplicatePropertyName"); + } + + /// + /// The type '{0}' defines properties '{1}' and '{2}' which differ only by casing. This is not supported by {3} which uses case-insensitive comparisons. + /// + internal static string FormatRouteValueDictionary_DuplicatePropertyName(object p0, object p1, object p2, object p3) + => string.Format(CultureInfo.CurrentCulture, GetString("RouteValueDictionary_DuplicatePropertyName"), p0, p1, p2, p3); + + private static string GetString(string name, params string[] formatterNames) + { + var value = _resourceManager.GetString(name); + + System.Diagnostics.Debug.Assert(value != null); + + if (formatterNames != null) + { + for (var i = 0; i < formatterNames.Length; i++) + { + value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}"); + } + } + + return value; + } + } +} diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/Resources.resx b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/Resources.resx new file mode 100644 index 0000000000..40e651af14 --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/Resources.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + An element with the key '{0}' already exists in the {1}. + + + The type '{0}' defines properties '{1}' and '{2}' which differ only by casing. This is not supported by {3} which uses case-insensitive comparisons. + + \ No newline at end of file diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/RouteContext.cs b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/RouteContext.cs new file mode 100644 index 0000000000..767f39b1ec --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/RouteContext.cs @@ -0,0 +1,58 @@ +// 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.Http; + +namespace Microsoft.AspNetCore.Routing +{ + /// + /// A context object for . + /// + public class RouteContext + { + private RouteData _routeData; + + /// + /// Creates a new for the provided . + /// + /// The associated with the current request. + public RouteContext(HttpContext httpContext) + { + HttpContext = httpContext; + + RouteData = new RouteData(); + } + + /// + /// Gets or sets the handler for the request. An should set + /// when it matches. + /// + public RequestDelegate Handler { get; set; } + + /// + /// Gets the associated with the current request. + /// + public HttpContext HttpContext { get; } + + /// + /// Gets or sets the associated with the current context. + /// + public RouteData RouteData + { + get + { + return _routeData; + } + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(RouteData)); + } + + _routeData = value; + } + } + } +} diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/RouteData.cs b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/RouteData.cs new file mode 100644 index 0000000000..0ece2b91de --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/RouteData.cs @@ -0,0 +1,296 @@ +// 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 +{ + /// + /// Information about the current routing path. + /// + public class RouteData + { + private RouteValueDictionary _dataTokens; + private List _routers; + private RouteValueDictionary _values; + + /// + /// Creates a new instance. + /// + public RouteData() + { + // Perf: Avoid allocating collections unless needed. + } + + /// + /// Creates a new instance with values copied from . + /// + /// The other instance to copy. + public RouteData(RouteData other) + { + if (other == null) + { + throw new ArgumentNullException(nameof(other)); + } + + // Perf: Avoid allocating collections unless we need to make a copy. + if (other._routers != null) + { + _routers = new List(other.Routers); + } + + if (other._dataTokens != null) + { + _dataTokens = new RouteValueDictionary(other._dataTokens); + } + + if (other._values != null) + { + _values = new RouteValueDictionary(other._values); + } + } + + /// + /// Gets the data tokens produced by routes on the current routing path. + /// + public RouteValueDictionary DataTokens + { + get + { + if (_dataTokens == null) + { + _dataTokens = new RouteValueDictionary(); + } + + return _dataTokens; + } + } + + /// + /// Gets the list of instances on the current routing path. + /// + public IList Routers + { + get + { + if (_routers == null) + { + _routers = new List(); + } + + return _routers; + } + } + + /// + /// Gets the set of values produced by routes on the current routing path. + /// + public RouteValueDictionary Values + { + get + { + if (_values == null) + { + _values = new RouteValueDictionary(); + } + + return _values; + } + } + + /// + /// + /// Creates a snapshot of the current state of the before appending + /// to , merging into + /// , and merging into . + /// + /// + /// Call to restore the state of this + /// to the state at the time of calling + /// . + /// + /// + /// + /// An to append to . If null, then + /// will not be changed. + /// + /// + /// A to merge into . If null, then + /// will not be changed. + /// + /// + /// A to merge into . If null, then + /// will not be changed. + /// + /// A that captures the current state. + public RouteDataSnapshot PushState(IRouter router, RouteValueDictionary values, RouteValueDictionary dataTokens) + { + // Perf: this is optimized for small list sizes, in particular to avoid overhead of a native call in + // Array.CopyTo inside the List(IEnumerable) constructor. + List routers = null; + var count = _routers?.Count; + if (count > 0) + { + routers = new List(count.Value); + for (var i = 0; i < count.Value; i++) + { + routers.Add(_routers[i]); + } + } + + var snapshot = new RouteDataSnapshot( + this, + _dataTokens?.Count > 0 ? new RouteValueDictionary(_dataTokens) : null, + routers, + _values?.Count > 0 ? new RouteValueDictionary(_values) : null); + + if (router != null) + { + Routers.Add(router); + } + + if (values != null) + { + foreach (var kvp in values) + { + if (kvp.Value != null) + { + Values[kvp.Key] = kvp.Value; + } + } + } + + if (dataTokens != null) + { + foreach (var kvp in dataTokens) + { + DataTokens[kvp.Key] = kvp.Value; + } + } + + return snapshot; + } + + /// + /// A snapshot of the state of a instance. + /// + public struct RouteDataSnapshot + { + private readonly RouteData _routeData; + private readonly RouteValueDictionary _dataTokens; + private readonly IList _routers; + private readonly RouteValueDictionary _values; + + /// + /// Creates a new for . + /// + /// The . + /// The data tokens. + /// The routers. + /// The route values. + public RouteDataSnapshot( + RouteData routeData, + RouteValueDictionary dataTokens, + IList routers, + RouteValueDictionary values) + { + if (routeData == null) + { + throw new ArgumentNullException(nameof(routeData)); + } + + _routeData = routeData; + _dataTokens = dataTokens; + _routers = routers; + _values = values; + } + + /// + /// Restores the to the captured state. + /// + public void Restore() + { + if (_routeData._dataTokens == null && _dataTokens == null) + { + // Do nothing + } + else if (_dataTokens == null) + { + _routeData._dataTokens.Clear(); + } + else + { + _routeData._dataTokens.Clear(); + + foreach (var kvp in _dataTokens) + { + _routeData._dataTokens.Add(kvp.Key, kvp.Value); + } + } + + if (_routeData._routers == null && _routers == null) + { + // Do nothing + } + else if (_routers == null) + { + // Perf: this is optimized for small list sizes, in particular to avoid overhead of a native call in + // Array.Clear inside the List.Clear() method. + var routers = _routeData._routers; + for (var i = routers.Count - 1; i >= 0 ; i--) + { + routers.RemoveAt(i); + } + } + else + { + // Perf: this is optimized for small list sizes, in particular to avoid overhead of a native call in + // Array.Clear inside the List.Clear() method. + // + // We want to basically copy the contents of _routers in _routeData._routers - this change does + // that with the minimal number of reads/writes and without calling Clear(). + var routers = _routeData._routers; + var snapshotRouters = _routers; + + // This is made more complicated by the fact that List[int] throws if i == Count, so we have + // to do two loops and call Add for those cases. + var i = 0; + for (; i < snapshotRouters.Count && i < routers.Count; i++) + { + routers[i] = snapshotRouters[i]; + } + + for (; i < snapshotRouters.Count; i++) + { + routers.Add(snapshotRouters[i]); + } + + // Trim excess - again avoiding RemoveRange because it uses native methods. + for (i = routers.Count - 1; i >= snapshotRouters.Count; i--) + { + routers.RemoveAt(i); + } + } + + if (_routeData._values == null && _values == null) + { + // Do nothing + } + else if (_values == null) + { + _routeData._values.Clear(); + } + else + { + _routeData._values.Clear(); + + foreach (var kvp in _values) + { + _routeData._values.Add(kvp.Key, kvp.Value); + } + } + } + } + } +} \ No newline at end of file diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/RouteDirection.cs b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/RouteDirection.cs new file mode 100644 index 0000000000..f19ac2b4ec --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/RouteDirection.cs @@ -0,0 +1,21 @@ +// 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 +{ + /// + /// Indicates whether ASP.NET routing is processing a URL from an HTTP request or generating a URL. + /// + public enum RouteDirection + { + /// + /// A URL from a client is being processed. + /// + IncomingRequest, + + /// + /// A URL is being created based on the route definition. + /// + UrlGeneration, + } +} diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/RouteValueDictionary.cs b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/RouteValueDictionary.cs new file mode 100644 index 0000000000..d0bc8e9ecf --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/RouteValueDictionary.cs @@ -0,0 +1,790 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using Microsoft.AspNetCore.Routing.Abstractions; +using Microsoft.Extensions.Internal; + +namespace Microsoft.AspNetCore.Routing +{ + /// + /// An type for route values. + /// + public class RouteValueDictionary : IDictionary, IReadOnlyDictionary + { + internal Storage _storage; + + /// + /// Creates an empty . + /// + public RouteValueDictionary() + { + _storage = EmptyStorage.Instance; + } + + /// + /// Creates a initialized with the specified . + /// + /// An object to initialize the dictionary. The value can be of type + /// or + /// or an object with public properties as key-value pairs. + /// + /// + /// If the value is a dictionary or other of , + /// then its entries are copied. Otherwise the object is interpreted as a set of key-value pairs where the + /// property names are keys, and property values are the values, and copied into the dictionary. + /// Only public instance non-index properties are considered. + /// + public RouteValueDictionary(object values) + { + var dictionary = values as RouteValueDictionary; + if (dictionary != null) + { + var listStorage = dictionary._storage as ListStorage; + if (listStorage != null) + { + _storage = new ListStorage(listStorage); + return; + } + + var propertyStorage = dictionary._storage as PropertyStorage; + if (propertyStorage != null) + { + // PropertyStorage is immutable so we can just copy it. + _storage = dictionary._storage; + return; + } + + // If we get here, it's an EmptyStorage. + _storage = EmptyStorage.Instance; + return; + } + + var keyValueEnumerable = values as IEnumerable>; + if (keyValueEnumerable != null) + { + var listStorage = new ListStorage(); + _storage = listStorage; + foreach (var kvp in keyValueEnumerable) + { + if (listStorage.ContainsKey(kvp.Key)) + { + var message = Resources.FormatRouteValueDictionary_DuplicateKey(kvp.Key, nameof(RouteValueDictionary)); + throw new ArgumentException(message, nameof(values)); + } + + listStorage.Add(kvp); + } + + return; + } + + var stringValueEnumerable = values as IEnumerable>; + if (stringValueEnumerable != null) + { + var listStorage = new ListStorage(); + _storage = listStorage; + foreach (var kvp in stringValueEnumerable) + { + if (listStorage.ContainsKey(kvp.Key)) + { + var message = Resources.FormatRouteValueDictionary_DuplicateKey(kvp.Key, nameof(RouteValueDictionary)); + throw new ArgumentException(message, nameof(values)); + } + + listStorage.Add(new KeyValuePair(kvp.Key, kvp.Value)); + } + + return; + } + + if (values != null) + { + _storage = new PropertyStorage(values); + return; + } + + _storage = EmptyStorage.Instance; + } + + /// + public object this[string key] + { + get + { + if (string.IsNullOrEmpty(key)) + { + throw new ArgumentNullException(nameof(key)); + } + + object value; + TryGetValue(key, out value); + return value; + } + + set + { + if (string.IsNullOrEmpty(key)) + { + throw new ArgumentNullException(nameof(key)); + } + + if (!_storage.TrySetValue(key, value)) + { + Upgrade(); + _storage.TrySetValue(key, value); + } + } + } + + /// + /// Gets the comparer for this dictionary. + /// + /// + /// This will always be a reference to + /// + public IEqualityComparer Comparer => StringComparer.OrdinalIgnoreCase; + + /// + public int Count => _storage.Count; + + /// + bool ICollection>.IsReadOnly => false; + + /// + public ICollection Keys + { + get + { + Upgrade(); + + var list = (ListStorage)_storage; + var keys = new string[list.Count]; + for (var i = 0; i < keys.Length; i++) + { + keys[i] = list[i].Key; + } + + return keys; + } + } + + IEnumerable IReadOnlyDictionary.Keys + { + get + { + return Keys; + } + } + + /// + public ICollection Values + { + get + { + Upgrade(); + + var list = (ListStorage)_storage; + var values = new object[list.Count]; + for (var i = 0; i < values.Length; i++) + { + values[i] = list[i].Value; + } + + return values; + } + } + + IEnumerable IReadOnlyDictionary.Values + { + get + { + return Values; + } + } + + /// + void ICollection>.Add(KeyValuePair item) + { + Add(item.Key, item.Value); + } + + /// + public void Add(string key, object value) + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + Upgrade(); + + var list = (ListStorage)_storage; + for (var i = 0; i < list.Count; i++) + { + if (string.Equals(list[i].Key, key, StringComparison.OrdinalIgnoreCase)) + { + var message = Resources.FormatRouteValueDictionary_DuplicateKey(key, nameof(RouteValueDictionary)); + throw new ArgumentException(message, nameof(key)); + } + } + + list.Add(new KeyValuePair(key, value)); + } + + /// + public void Clear() + { + if (_storage.Count == 0) + { + return; + } + + Upgrade(); + + var list = (ListStorage)_storage; + list.Clear(); + } + + /// + bool ICollection>.Contains(KeyValuePair item) + { + if (_storage.Count == 0) + { + return false; + } + + Upgrade(); + + var list = (ListStorage)_storage; + for (var i = 0; i < list.Count; i++) + { + if (string.Equals(list[i].Key, item.Key, StringComparison.OrdinalIgnoreCase)) + { + return EqualityComparer.Default.Equals(list[i].Value, item.Value); + } + } + + return false; + } + + /// + public bool ContainsKey(string key) + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + return _storage.ContainsKey(key); + } + + /// + void ICollection>.CopyTo( + KeyValuePair[] array, + int arrayIndex) + { + if (array == null) + { + throw new ArgumentNullException(nameof(array)); + } + + if (arrayIndex < 0 || arrayIndex > array.Length || array.Length - arrayIndex < this.Count) + { + throw new ArgumentOutOfRangeException(nameof(arrayIndex)); + } + + if (_storage.Count == 0) + { + return; + } + + Upgrade(); + + var list = (ListStorage)_storage; + list.CopyTo(array, arrayIndex); + } + + /// + public Enumerator GetEnumerator() + { + return new Enumerator(this); + } + + /// + IEnumerator> IEnumerable>.GetEnumerator() + { + return GetEnumerator(); + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// + bool ICollection>.Remove(KeyValuePair item) + { + if (_storage.Count == 0) + { + return false; + } + + Upgrade(); + + var list = (ListStorage)_storage; + for (var i = 0; i < list.Count; i++) + { + if (string.Equals(list[i].Key, item.Key, StringComparison.OrdinalIgnoreCase) && + EqualityComparer.Default.Equals(list[i].Value, item.Value)) + { + list.RemoveAt(i); + return true; + } + } + + return false; + } + + /// + public bool Remove(string key) + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + if (_storage.Count == 0) + { + return false; + } + + Upgrade(); + + var list = (ListStorage)_storage; + for (var i = 0; i < list.Count; i++) + { + if (string.Equals(list[i].Key, key, StringComparison.OrdinalIgnoreCase)) + { + list.RemoveAt(i); + return true; + } + } + + return false; + } + + /// + public bool TryGetValue(string key, out object value) + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + return _storage.TryGetValue(key, out value); + } + + private void Upgrade() + { + _storage.Upgrade(ref _storage); + } + + public struct Enumerator : IEnumerator> + { + private readonly Storage _storage; + private int _index; + + public Enumerator(RouteValueDictionary dictionary) + { + if (dictionary == null) + { + throw new ArgumentNullException(); + } + + _storage = dictionary._storage; + + Current = default(KeyValuePair); + _index = -1; + } + + public KeyValuePair Current { get; private set; } + + object IEnumerator.Current => Current; + + public void Dispose() + { + } + + public bool MoveNext() + { + if (++_index < _storage.Count) + { + Current = _storage[_index]; + return true; + } + + Current = default(KeyValuePair); + return false; + } + + public void Reset() + { + Current = default(KeyValuePair); + _index = -1; + } + } + + // Storage and its subclasses are internal for testing. + internal abstract class Storage + { + public abstract int Count { get; } + + public abstract KeyValuePair this[int index] { get; set; } + + public abstract void Upgrade(ref Storage storage); + + public abstract bool TryGetValue(string key, out object value); + + public abstract bool ContainsKey(string key); + + public abstract bool TrySetValue(string key, object value); + } + + internal class ListStorage : Storage + { + private KeyValuePair[] _items; + private int _count; + + private static readonly KeyValuePair[] _emptyArray = new KeyValuePair[0]; + + public ListStorage() + { + _items = _emptyArray; + } + + public ListStorage(int capacity) + { + if (capacity == 0) + { + _items = _emptyArray; + } + else + { + _items = new KeyValuePair[capacity]; + } + } + + public ListStorage(ListStorage other) + { + if (other.Count == 0) + { + _items = _emptyArray; + } + else + { + _items = new KeyValuePair[other.Count]; + for (var i = 0; i < other.Count; i++) + { + this.Add(other[i]); + } + } + } + + public int Capacity => _items.Length; + + public override int Count => _count; + + public override KeyValuePair this[int index] + { + get + { + if (index < 0 || index >= _count) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } + + return _items[index]; + } + set + { + if (index < 0 || index >= _count) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } + + _items[index] = value; + } + } + + public void Add(KeyValuePair item) + { + if (_count == _items.Length) + { + EnsureCapacity(_count + 1); + } + + _items[_count++] = item; + } + + public void RemoveAt(int index) + { + _count--; + + for (var i = index; i < _count; i++) + { + _items[i] = _items[i + 1]; + } + + _items[_count] = default(KeyValuePair); + } + + public void Clear() + { + for (var i = 0; i < _count; i++) + { + _items[i] = default(KeyValuePair); + } + + _count = 0; + } + + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + for (var i = 0; i < _count; i++) + { + array[arrayIndex++] = _items[i]; + } + } + + public override bool ContainsKey(string key) + { + for (var i = 0; i < Count; i++) + { + var kvp = _items[i]; + if (string.Equals(key, kvp.Key, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + return false; + } + + public override bool TrySetValue(string key, object value) + { + for (var i = 0; i < Count; i++) + { + var kvp = _items[i]; + if (string.Equals(key, kvp.Key, StringComparison.OrdinalIgnoreCase)) + { + _items[i] = new KeyValuePair(key, value); + return true; + } + } + + Add(new KeyValuePair(key, value)); + return true; + } + + public override bool TryGetValue(string key, out object value) + { + for (var i = 0; i < Count; i++) + { + var kvp = _items[i]; + if (string.Equals(key, kvp.Key, StringComparison.OrdinalIgnoreCase)) + { + value = kvp.Value; + return true; + } + } + + value = null; + return false; + } + + public override void Upgrade(ref Storage storage) + { + // Do nothing. + } + + private void EnsureCapacity(int min) + { + var newLength = _items.Length == 0 ? 4 : _items.Length * 2; + var newItems = new KeyValuePair[newLength]; + for (var i = 0; i < _count; i++) + { + newItems[i] = _items[i]; + } + + _items = newItems; + } + } + + internal class PropertyStorage : Storage + { + private static readonly PropertyCache _propertyCache = new PropertyCache(); + + internal readonly object _value; + internal readonly PropertyHelper[] _properties; + + public PropertyStorage(object value) + { + Debug.Assert(value != null); + _value = value; + + // Cache the properties so we can know if we've already validated them for duplicates. + var type = _value.GetType(); + if (!_propertyCache.TryGetValue(type, out _properties)) + { + _properties = PropertyHelper.GetVisibleProperties(type); + ValidatePropertyNames(type, _properties); + _propertyCache.TryAdd(type, _properties); + } + } + + public PropertyStorage(PropertyStorage propertyStorage) + { + _value = propertyStorage._value; + _properties = propertyStorage._properties; + } + + public override int Count => _properties.Length; + + public override KeyValuePair this[int index] + { + get + { + var property = _properties[index]; + return new KeyValuePair(property.Name, property.GetValue(_value)); + } + set + { + // PropertyStorage never sets a value. + throw new NotImplementedException(); + } + } + + public override bool TryGetValue(string key, out object value) + { + for (var i = 0; i < _properties.Length; i++) + { + var property = _properties[i]; + if (string.Equals(key, property.Name, StringComparison.OrdinalIgnoreCase)) + { + value = property.GetValue(_value); + return true; + } + } + + value = null; + return false; + } + + public override bool ContainsKey(string key) + { + for (var i = 0; i < _properties.Length; i++) + { + var property = _properties[i]; + if (string.Equals(key, property.Name, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + return false; + } + + public override bool TrySetValue(string key, object value) + { + // PropertyStorage never sets a value. + return false; + } + + public override void Upgrade(ref Storage storage) + { + storage = new ListStorage(Count); + for (var i = 0; i < _properties.Length; i++) + { + var property = _properties[i]; + storage.TrySetValue(property.Name, property.GetValue(_value)); + } + } + + private static void ValidatePropertyNames(Type type, PropertyHelper[] properties) + { + var names = new Dictionary(StringComparer.OrdinalIgnoreCase); + for (var i = 0; i < properties.Length; i++) + { + var property = properties[i]; + + PropertyHelper duplicate; + if (names.TryGetValue(property.Name, out duplicate)) + { + var message = Resources.FormatRouteValueDictionary_DuplicatePropertyName( + type.FullName, + property.Name, + duplicate.Name, + nameof(RouteValueDictionary)); + throw new InvalidOperationException(message); + } + + names.Add(property.Name, property); + } + } + } + + internal class EmptyStorage : Storage + { + public static readonly EmptyStorage Instance = new EmptyStorage(); + + private EmptyStorage() + { + } + + public override int Count => 0; + + public override KeyValuePair this[int index] + { + get + { + throw new NotImplementedException(); + } + set + { + throw new NotImplementedException(); + } + } + + public override bool ContainsKey(string key) + { + return false; + } + + public override bool TryGetValue(string key, out object value) + { + value = null; + return false; + } + + public override bool TrySetValue(string key, object value) + { + return false; + } + + public override void Upgrade(ref Storage storage) + { + storage = new ListStorage(); + } + } + + private class PropertyCache : ConcurrentDictionary + { + } + } +} diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/RoutingHttpContextExtensions.cs b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/RoutingHttpContextExtensions.cs new file mode 100644 index 0000000000..de121852ab --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/RoutingHttpContextExtensions.cs @@ -0,0 +1,53 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Routing +{ + /// + /// Extension methods for related to routing. + /// + public static class RoutingHttpContextExtensions + { + /// + /// Gets the associated with the provided . + /// + /// The associated with the current request. + /// The , or null. + public static RouteData GetRouteData(this HttpContext httpContext) + { + if (httpContext == null) + { + throw new ArgumentNullException(nameof(httpContext)); + } + + var routingFeature = httpContext.Features[typeof(IRoutingFeature)] as IRoutingFeature; + return routingFeature?.RouteData; + } + + /// + /// Gets a route value from associated with the provided + /// . + /// + /// The associated with the current request. + /// The key of the route value. + /// The corresponding route value, or null. + public static object GetRouteValue(this HttpContext httpContext, string key) + { + if (httpContext == null) + { + throw new ArgumentNullException(nameof(httpContext)); + } + + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + var routingFeature = httpContext.Features[typeof(IRoutingFeature)] as IRoutingFeature; + return routingFeature?.RouteData.Values[key]; + } + } +} diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/VirtualPathContext.cs b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/VirtualPathContext.cs new file mode 100644 index 0000000000..036aa445f8 --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/VirtualPathContext.cs @@ -0,0 +1,66 @@ +// 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 +{ + /// + /// A context for virtual path generation operations. + /// + public class VirtualPathContext + { + /// + /// Creates a new . + /// + /// The associated with the current request. + /// The set of route values associated with the current request. + /// The set of new values provided for virtual path generation. + public VirtualPathContext( + HttpContext httpContext, + RouteValueDictionary ambientValues, + RouteValueDictionary values) + : this(httpContext, ambientValues, values, null) + { + } + + /// + /// Creates a new . + /// + /// The associated with the current request. + /// The set of route values associated with the current request. + /// The set of new values provided for virtual path generation. + /// The name of the route to use for virtual path generation. + public VirtualPathContext( + HttpContext httpContext, + RouteValueDictionary ambientValues, + RouteValueDictionary values, + string routeName) + { + HttpContext = httpContext; + AmbientValues = ambientValues; + Values = values; + RouteName = routeName; + } + + /// + /// Gets the set of route values associated with the current request. + /// + public RouteValueDictionary AmbientValues { get; } + + /// + /// Gets the associated with the current request. + /// + public HttpContext HttpContext { get; } + + /// + /// Gets the name of the route to use for virtual path generation. + /// + public string RouteName { get; } + + /// + /// Gets or sets the set of new values provided for virtual path generation. + /// + public RouteValueDictionary Values { get; set; } + } +} diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/VirtualPathData.cs b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/VirtualPathData.cs new file mode 100644 index 0000000000..d6a2db1232 --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/VirtualPathData.cs @@ -0,0 +1,99 @@ +// 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; + +namespace Microsoft.AspNetCore.Routing +{ + /// + /// Represents information about the route and virtual path that are the result of + /// generating a URL with the ASP.NET routing middleware. + /// + public class VirtualPathData + { + private RouteValueDictionary _dataTokens; + private string _virtualPath; + + /// + /// Initializes a new instance of the class. + /// + /// The object that is used to generate the URL. + /// The generated URL. + public VirtualPathData(IRouter router, string virtualPath) + : this(router, virtualPath, dataTokens: null) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The object that is used to generate the URL. + /// The generated URL. + /// The collection of custom values. + public VirtualPathData( + IRouter router, + string virtualPath, + RouteValueDictionary dataTokens) + { + if (router == null) + { + throw new ArgumentNullException(nameof(router)); + } + + Router = router; + VirtualPath = virtualPath; + _dataTokens = dataTokens == null ? null : new RouteValueDictionary(dataTokens); + } + + /// + /// Gets the collection of custom values for the . + /// + public RouteValueDictionary DataTokens + { + get + { + if (_dataTokens == null) + { + _dataTokens = new RouteValueDictionary(); + } + + return _dataTokens; + } + } + + /// + /// Gets or sets the that was used to generate the URL. + /// + public IRouter Router { get; set; } + + /// + /// Gets or sets the URL that was generated from the . + /// + public string VirtualPath + { + get + { + return _virtualPath; + } + set + { + _virtualPath = NormalizePath(value); + } + } + + private static string NormalizePath(string path) + { + if (string.IsNullOrEmpty(path)) + { + return string.Empty; + } + + if (!path.StartsWith("/", StringComparison.Ordinal)) + { + return "/" + path; + } + + return path; + } + } +} \ No newline at end of file diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/baseline.netcore.json b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/baseline.netcore.json new file mode 100644 index 0000000000..8f8a0cc67d --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/baseline.netcore.json @@ -0,0 +1,849 @@ +{ + "AssemblyIdentity": "Microsoft.AspNetCore.Routing.Abstractions, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "Types": [ + { + "Name": "Microsoft.AspNetCore.Routing.IRouteConstraint", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Match", + "Parameters": [ + { + "Name": "httpContext", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "route", + "Type": "Microsoft.AspNetCore.Routing.IRouter" + }, + { + "Name": "routeKey", + "Type": "System.String" + }, + { + "Name": "values", + "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary" + }, + { + "Name": "routeDirection", + "Type": "Microsoft.AspNetCore.Routing.RouteDirection" + } + ], + "ReturnType": "System.Boolean", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Routing.IRouteHandler", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "GetRequestHandler", + "Parameters": [ + { + "Name": "httpContext", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "routeData", + "Type": "Microsoft.AspNetCore.Routing.RouteData" + } + ], + "ReturnType": "Microsoft.AspNetCore.Http.RequestDelegate", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Routing.IRouter", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "RouteAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Routing.RouteContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetVirtualPath", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Routing.VirtualPathContext" + } + ], + "ReturnType": "Microsoft.AspNetCore.Routing.VirtualPathData", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Routing.IRoutingFeature", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_RouteData", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Routing.RouteData", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RouteData", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Routing.RouteData" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Routing.RouteContext", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Handler", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.RequestDelegate", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Handler", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.RequestDelegate" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_HttpContext", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.HttpContext", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_RouteData", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Routing.RouteData", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RouteData", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Routing.RouteData" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "httpContext", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Routing.RouteData", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_DataTokens", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Routing.RouteValueDictionary", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Routers", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IList", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Values", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Routing.RouteValueDictionary", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "PushState", + "Parameters": [ + { + "Name": "router", + "Type": "Microsoft.AspNetCore.Routing.IRouter" + }, + { + "Name": "values", + "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary" + }, + { + "Name": "dataTokens", + "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary" + } + ], + "ReturnType": "Microsoft.AspNetCore.Routing.RouteData+RouteDataSnapshot", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "other", + "Type": "Microsoft.AspNetCore.Routing.RouteData" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Routing.RouteDirection", + "Visibility": "Public", + "Kind": "Enumeration", + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Field", + "Name": "IncomingRequest", + "Parameters": [], + "GenericParameter": [], + "Literal": "0" + }, + { + "Kind": "Field", + "Name": "UrlGeneration", + "Parameters": [], + "GenericParameter": [], + "Literal": "1" + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Routing.RouteValueDictionary", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "System.Collections.Generic.IDictionary", + "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", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + } + ], + "ReturnType": "System.Object", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.Collections.Generic.IDictionary", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Item", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + }, + { + "Name": "value", + "Type": "System.Object" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.Collections.Generic.IDictionary", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Comparer", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IEqualityComparer", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Keys", + "Parameters": [], + "ReturnType": "System.Collections.Generic.ICollection", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.Collections.Generic.IDictionary", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Values", + "Parameters": [], + "ReturnType": "System.Collections.Generic.ICollection", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.Collections.Generic.IDictionary", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Add", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + }, + { + "Name": "value", + "Type": "System.Object" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.Collections.Generic.IDictionary", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ContainsKey", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + } + ], + "ReturnType": "System.Boolean", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.Collections.Generic.IDictionary", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetEnumerator", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Routing.RouteValueDictionary+Enumerator", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Remove", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + } + ], + "ReturnType": "System.Boolean", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.Collections.Generic.IDictionary", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TryGetValue", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + }, + { + "Name": "value", + "Type": "System.Object", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.Collections.Generic.IDictionary", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "values", + "Type": "System.Object" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Routing.RoutingHttpContextExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "GetRouteData", + "Parameters": [ + { + "Name": "httpContext", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + } + ], + "ReturnType": "Microsoft.AspNetCore.Routing.RouteData", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetRouteValue", + "Parameters": [ + { + "Name": "httpContext", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "key", + "Type": "System.String" + } + ], + "ReturnType": "System.Object", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Routing.VirtualPathContext", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_AmbientValues", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Routing.RouteValueDictionary", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_HttpContext", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.HttpContext", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_RouteName", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Values", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Routing.RouteValueDictionary", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Values", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "httpContext", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "ambientValues", + "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary" + }, + { + "Name": "values", + "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "httpContext", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "ambientValues", + "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary" + }, + { + "Name": "values", + "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary" + }, + { + "Name": "routeName", + "Type": "System.String" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Routing.VirtualPathData", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_DataTokens", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Routing.RouteValueDictionary", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Router", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Routing.IRouter", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Router", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Routing.IRouter" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_VirtualPath", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_VirtualPath", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "router", + "Type": "Microsoft.AspNetCore.Routing.IRouter" + }, + { + "Name": "virtualPath", + "Type": "System.String" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "router", + "Type": "Microsoft.AspNetCore.Routing.IRouter" + }, + { + "Name": "virtualPath", + "Type": "System.String" + }, + { + "Name": "dataTokens", + "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Routing.RouteData+RouteDataSnapshot", + "Visibility": "Public", + "Kind": "Struct", + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Restore", + "Parameters": [], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "routeData", + "Type": "Microsoft.AspNetCore.Routing.RouteData" + }, + { + "Name": "dataTokens", + "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary" + }, + { + "Name": "routers", + "Type": "System.Collections.Generic.IList" + }, + { + "Name": "values", + "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Routing.RouteValueDictionary+Enumerator", + "Visibility": "Public", + "Kind": "Struct", + "Sealed": true, + "ImplementedInterfaces": [ + "System.Collections.Generic.IEnumerator>" + ], + "Members": [ + { + "Kind": "Method", + "Name": "Dispose", + "Parameters": [], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.IDisposable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "MoveNext", + "Parameters": [], + "ReturnType": "System.Boolean", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.Collections.IEnumerator", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Reset", + "Parameters": [], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.Collections.IEnumerator", + "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", + "Parameters": [ + { + "Name": "dictionary", + "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + } + ] +} \ No newline at end of file diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/AlphaRouteConstraint.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/AlphaRouteConstraint.cs new file mode 100644 index 0000000000..7f2748e5c3 --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/AlphaRouteConstraint.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.Constraints +{ + /// + /// Constrains a route parameter to contain only lowercase or uppercase letters A through Z in the English alphabet. + /// + public class AlphaRouteConstraint : RegexRouteConstraint + { + /// + /// Initializes a new instance of the class. + /// + public AlphaRouteConstraint() : base(@"^[a-z]*$") + { + } + } +} \ No newline at end of file diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/BoolRouteConstraint.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/BoolRouteConstraint.cs new file mode 100644 index 0000000000..a65e88ef67 --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/BoolRouteConstraint.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.Globalization; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Routing.Constraints +{ + /// + /// Constrains a route parameter to represent only Boolean values. + /// + public class BoolRouteConstraint : IRouteConstraint + { + /// + public bool Match( + HttpContext httpContext, + IRouter route, + string routeKey, + RouteValueDictionary values, + RouteDirection routeDirection) + { + if (httpContext == null) + { + throw new ArgumentNullException(nameof(httpContext)); + } + + if (route == null) + { + throw new ArgumentNullException(nameof(route)); + } + + if (routeKey == null) + { + throw new ArgumentNullException(nameof(routeKey)); + } + + if (values == null) + { + throw new ArgumentNullException(nameof(values)); + } + + object value; + if (values.TryGetValue(routeKey, out value) && value != null) + { + if (value is bool) + { + return true; + } + + bool result; + var valueString = Convert.ToString(value, CultureInfo.InvariantCulture); + return bool.TryParse(valueString, out result); + } + + return false; + } + } +} \ No newline at end of file diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/CompositeRouteConstraint.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/CompositeRouteConstraint.cs new file mode 100644 index 0000000000..5acff08294 --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/CompositeRouteConstraint.cs @@ -0,0 +1,73 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Routing.Constraints +{ + /// + /// Constrains a route by several child constraints. + /// + public class CompositeRouteConstraint : IRouteConstraint + { + /// + /// Initializes a new instance of the class. + /// + /// The child constraints that must match for this constraint to match. + public CompositeRouteConstraint(IEnumerable constraints) + { + if (constraints == null) + { + throw new ArgumentNullException(nameof(constraints)); + } + + Constraints = constraints; + } + + /// + /// Gets the child constraints that must match for this constraint to match. + /// + public IEnumerable Constraints { get; private set; } + + /// + public bool Match( + HttpContext httpContext, + IRouter route, + string routeKey, + RouteValueDictionary values, + RouteDirection routeDirection) + { + if (httpContext == null) + { + throw new ArgumentNullException(nameof(httpContext)); + } + + if (route == null) + { + throw new ArgumentNullException(nameof(route)); + } + + if (routeKey == null) + { + throw new ArgumentNullException(nameof(routeKey)); + } + + if (values == null) + { + throw new ArgumentNullException(nameof(values)); + } + + foreach (var constraint in Constraints) + { + if (!constraint.Match(httpContext, route, routeKey, values, routeDirection)) + { + return false; + } + } + + return true; + } + } +} \ No newline at end of file diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/DateTimeRouteConstraint.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/DateTimeRouteConstraint.cs new file mode 100644 index 0000000000..9015dc64be --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/DateTimeRouteConstraint.cs @@ -0,0 +1,65 @@ +// 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.Globalization; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Routing.Constraints +{ + /// + /// Constrains a route parameter to represent only values. + /// + /// + /// This constraint tries to parse strings by using all of the formats returned by the + /// CultureInfo.InvariantCulture.DateTimeFormat.GetAllDateTimePatterns() method. + /// For a sample on how to list all formats which are considered, please visit + /// http://msdn.microsoft.com/en-us/library/aszyst2c(v=vs.110).aspx + /// + public class DateTimeRouteConstraint : IRouteConstraint + { + /// + public bool Match( + HttpContext httpContext, + IRouter route, + string routeKey, + RouteValueDictionary values, + RouteDirection routeDirection) + { + if (httpContext == null) + { + throw new ArgumentNullException(nameof(httpContext)); + } + + if (route == null) + { + throw new ArgumentNullException(nameof(route)); + } + + if (routeKey == null) + { + throw new ArgumentNullException(nameof(routeKey)); + } + + if (values == null) + { + throw new ArgumentNullException(nameof(values)); + } + + object value; + if (values.TryGetValue(routeKey, out value) && value != null) + { + if (value is DateTime) + { + return true; + } + + DateTime result; + var valueString = Convert.ToString(value, CultureInfo.InvariantCulture); + return DateTime.TryParse(valueString, CultureInfo.InvariantCulture, DateTimeStyles.None, out result); + } + + return false; + } + } +} \ No newline at end of file diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/DecimalRouteConstraint.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/DecimalRouteConstraint.cs new file mode 100644 index 0000000000..ff29d98574 --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/DecimalRouteConstraint.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.Globalization; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Routing.Constraints +{ + /// + /// Constrains a route parameter to represent only decimal values. + /// + public class DecimalRouteConstraint : IRouteConstraint + { + /// + public bool Match( + HttpContext httpContext, + IRouter route, + string routeKey, + RouteValueDictionary values, + RouteDirection routeDirection) + { + if (httpContext == null) + { + throw new ArgumentNullException(nameof(httpContext)); + } + + if (route == null) + { + throw new ArgumentNullException(nameof(route)); + } + + if (routeKey == null) + { + throw new ArgumentNullException(nameof(routeKey)); + } + + if (values == null) + { + throw new ArgumentNullException(nameof(values)); + } + + object value; + if (values.TryGetValue(routeKey, out value) && value != null) + { + if (value is decimal) + { + return true; + } + + decimal result; + var valueString = Convert.ToString(value, CultureInfo.InvariantCulture); + return decimal.TryParse(valueString, NumberStyles.Number, CultureInfo.InvariantCulture, out result); + } + + return false; + } + } +} \ No newline at end of file diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/DoubleRouteConstraint.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/DoubleRouteConstraint.cs new file mode 100644 index 0000000000..e7259d0bf3 --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/DoubleRouteConstraint.cs @@ -0,0 +1,63 @@ +// 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.Globalization; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Routing.Constraints +{ + /// + /// Constrains a route parameter to represent only 64-bit floating-point values. + /// + public class DoubleRouteConstraint : IRouteConstraint + { + /// + public bool Match( + HttpContext httpContext, + IRouter route, + string routeKey, + RouteValueDictionary values, + RouteDirection routeDirection) + { + if (httpContext == null) + { + throw new ArgumentNullException(nameof(httpContext)); + } + + if (route == null) + { + throw new ArgumentNullException(nameof(route)); + } + + if (routeKey == null) + { + throw new ArgumentNullException(nameof(routeKey)); + } + + if (values == null) + { + throw new ArgumentNullException(nameof(values)); + } + + object value; + if (values.TryGetValue(routeKey, out value) && value != null) + { + if (value is double) + { + return true; + } + + double result; + var valueString = Convert.ToString(value, CultureInfo.InvariantCulture); + return double.TryParse( + valueString, + NumberStyles.Float | NumberStyles.AllowThousands, + CultureInfo.InvariantCulture, + out result); + } + + return false; + } + } +} \ No newline at end of file diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/FloatRouteConstraint.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/FloatRouteConstraint.cs new file mode 100644 index 0000000000..5db0e65c28 --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/FloatRouteConstraint.cs @@ -0,0 +1,63 @@ +// 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.Globalization; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Routing.Constraints +{ + /// + /// Constrains a route parameter to represent only 32-bit floating-point values. + /// + public class FloatRouteConstraint : IRouteConstraint + { + /// + public bool Match( + HttpContext httpContext, + IRouter route, + string routeKey, + RouteValueDictionary values, + RouteDirection routeDirection) + { + if (httpContext == null) + { + throw new ArgumentNullException(nameof(httpContext)); + } + + if (route == null) + { + throw new ArgumentNullException(nameof(route)); + } + + if (routeKey == null) + { + throw new ArgumentNullException(nameof(routeKey)); + } + + if (values == null) + { + throw new ArgumentNullException(nameof(values)); + } + + object value; + if (values.TryGetValue(routeKey, out value) && value != null) + { + if (value is float) + { + return true; + } + + float result; + var valueString = Convert.ToString(value, CultureInfo.InvariantCulture); + return float.TryParse( + valueString, + NumberStyles.Float | NumberStyles.AllowThousands, + CultureInfo.InvariantCulture, + out result); + } + + return false; + } + } +} \ No newline at end of file diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/GuidRouteConstraint.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/GuidRouteConstraint.cs new file mode 100644 index 0000000000..00d451767f --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/GuidRouteConstraint.cs @@ -0,0 +1,61 @@ +// 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.Globalization; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Routing.Constraints +{ + /// + /// Constrains a route parameter to represent only values. + /// Matches values specified in any of the five formats "N", "D", "B", "P", or "X", + /// supported by Guid.ToString(string) and Guid.ToString(String, IFormatProvider) methods. + /// + public class GuidRouteConstraint : IRouteConstraint + { + /// + public bool Match( + HttpContext httpContext, + IRouter route, + string routeKey, + RouteValueDictionary values, + RouteDirection routeDirection) + { + if (httpContext == null) + { + throw new ArgumentNullException(nameof(httpContext)); + } + + if (route == null) + { + throw new ArgumentNullException(nameof(route)); + } + + if (routeKey == null) + { + throw new ArgumentNullException(nameof(routeKey)); + } + + if (values == null) + { + throw new ArgumentNullException(nameof(values)); + } + + object value; + if (values.TryGetValue(routeKey, out value) && value != null) + { + if (value is Guid) + { + return true; + } + + Guid result; + var valueString = Convert.ToString(value, CultureInfo.InvariantCulture); + return Guid.TryParse(valueString, out result); + } + + return false; + } + } +} \ No newline at end of file diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/HttpMethodRouteConstraint.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/HttpMethodRouteConstraint.cs new file mode 100644 index 0000000000..d01140157d --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/HttpMethodRouteConstraint.cs @@ -0,0 +1,96 @@ +// 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; + +namespace Microsoft.AspNetCore.Routing.Constraints +{ + /// + /// Constrains the HTTP method of request or a route. + /// + public class HttpMethodRouteConstraint : IRouteConstraint + { + /// + /// Creates a new that accepts the HTTP methods specified + /// by . + /// + /// The allowed HTTP methods. + public HttpMethodRouteConstraint(params string[] allowedMethods) + { + if (allowedMethods == null) + { + throw new ArgumentNullException(nameof(allowedMethods)); + } + + AllowedMethods = new List(allowedMethods); + } + + /// + /// Gets the HTTP methods allowed by the constraint. + /// + public IList AllowedMethods { get; } + + /// + public virtual bool Match( + HttpContext httpContext, + IRouter route, + string routeKey, + RouteValueDictionary values, + RouteDirection routeDirection) + { + if (httpContext == null) + { + throw new ArgumentNullException(nameof(httpContext)); + } + + if (route == null) + { + throw new ArgumentNullException(nameof(route)); + } + + if (routeKey == null) + { + throw new ArgumentNullException(nameof(routeKey)); + } + + if (values == null) + { + throw new ArgumentNullException(nameof(values)); + } + + switch (routeDirection) + { + case RouteDirection.IncomingRequest: + return AllowedMethods.Contains(httpContext.Request.Method, StringComparer.OrdinalIgnoreCase); + + case RouteDirection.UrlGeneration: + // We need to see if the user specified the HTTP method explicitly. Consider these two routes: + // + // a) Route: template = "/{foo}", Constraints = { httpMethod = new HttpMethodRouteConstraint("GET") } + // b) Route: template = "/{foo}", Constraints = { httpMethod = new HttpMethodRouteConstraint("POST") } + // + // A user might know ahead of time that a URI he/she is generating might be used with a particular HTTP + // method. If a URI will be used for an HTTP POST but we match on (a) while generating the URI, then + // the HTTP GET-specific route will be used for URI generation, which might have undesired behavior. + // + // To prevent this, a user might call GetVirtualPath(..., { httpMethod = "POST" }) to + // signal that he is generating a URI that will be used for an HTTP POST, so he wants the URI + // generation to be performed by the (b) route instead of the (a) route, consistent with what would + // happen on incoming requests. + object obj; + if (!values.TryGetValue(routeKey, out obj)) + { + return true; + } + + return AllowedMethods.Contains(Convert.ToString(obj), StringComparer.OrdinalIgnoreCase); + + default: + throw new ArgumentOutOfRangeException(nameof(routeDirection)); + } + } + } +} diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/IntRouteConstraint.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/IntRouteConstraint.cs new file mode 100644 index 0000000000..83b08533bd --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/IntRouteConstraint.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.Globalization; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Routing.Constraints +{ + /// + /// Constrains a route parameter to represent only 32-bit integer values. + /// + public class IntRouteConstraint : IRouteConstraint + { + /// + public bool Match( + HttpContext httpContext, + IRouter route, + string routeKey, + RouteValueDictionary values, + RouteDirection routeDirection) + { + if (httpContext == null) + { + throw new ArgumentNullException(nameof(httpContext)); + } + + if (route == null) + { + throw new ArgumentNullException(nameof(route)); + } + + if (routeKey == null) + { + throw new ArgumentNullException(nameof(routeKey)); + } + + if (values == null) + { + throw new ArgumentNullException(nameof(values)); + } + + object value; + if (values.TryGetValue(routeKey, out value) && value != null) + { + if (value is int) + { + return true; + } + + int result; + var valueString = Convert.ToString(value, CultureInfo.InvariantCulture); + return int.TryParse(valueString, NumberStyles.Integer, CultureInfo.InvariantCulture, out result); + } + + return false; + } + } +} \ No newline at end of file diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/LengthRouteConstraint.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/LengthRouteConstraint.cs new file mode 100644 index 0000000000..f876c03cbc --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/LengthRouteConstraint.cs @@ -0,0 +1,111 @@ +// 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.Globalization; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Routing.Constraints +{ + /// + /// Constrains a route parameter to be a string of a given length or within a given range of lengths. + /// + public class LengthRouteConstraint : IRouteConstraint + { + /// + /// Initializes a new instance of the class that constrains + /// a route parameter to be a string of a given length. + /// + /// The length of the route parameter. + public LengthRouteConstraint(int length) + { + if (length < 0) + { + var errorMessage = Resources.FormatArgumentMustBeGreaterThanOrEqualTo(0); + throw new ArgumentOutOfRangeException(nameof(length), length, errorMessage); + } + + MinLength = MaxLength = length; + } + + /// + /// Initializes a new instance of the class that constrains + /// a route parameter to be a string of a given length. + /// + /// The minimum length allowed for the route parameter. + /// The maximum length allowed for the route parameter. + public LengthRouteConstraint(int minLength, int maxLength) + { + if (minLength < 0) + { + var errorMessage = Resources.FormatArgumentMustBeGreaterThanOrEqualTo(0); + throw new ArgumentOutOfRangeException(nameof(minLength), minLength, errorMessage); + } + + if (maxLength < 0) + { + var errorMessage = Resources.FormatArgumentMustBeGreaterThanOrEqualTo(0); + throw new ArgumentOutOfRangeException(nameof(maxLength), maxLength, errorMessage); + } + + if (minLength > maxLength) + { + var errorMessage = + Resources.FormatRangeConstraint_MinShouldBeLessThanOrEqualToMax("minLength", "maxLength"); + throw new ArgumentOutOfRangeException(nameof(minLength), minLength, errorMessage); + } + + MinLength = minLength; + MaxLength = maxLength; + } + + /// + /// Gets the minimum length allowed for the route parameter. + /// + public int MinLength { get; } + + /// + /// Gets the maximum length allowed for the route parameter. + /// + public int MaxLength { get; } + + /// + public bool Match( + HttpContext httpContext, + IRouter route, + string routeKey, + RouteValueDictionary values, + RouteDirection routeDirection) + { + if (httpContext == null) + { + throw new ArgumentNullException(nameof(httpContext)); + } + + if (route == null) + { + throw new ArgumentNullException(nameof(route)); + } + + if (routeKey == null) + { + throw new ArgumentNullException(nameof(routeKey)); + } + + if (values == null) + { + throw new ArgumentNullException(nameof(values)); + } + + object value; + if (values.TryGetValue(routeKey, out value) && value != null) + { + var valueString = Convert.ToString(value, CultureInfo.InvariantCulture); + var length = valueString.Length; + return length >= MinLength && length <= MaxLength; + } + + return false; + } + } +} \ No newline at end of file diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/LongRouteConstraint.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/LongRouteConstraint.cs new file mode 100644 index 0000000000..a76a4de885 --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/LongRouteConstraint.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.Globalization; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Routing.Constraints +{ + /// + /// Constrains a route parameter to represent only 64-bit integer values. + /// + public class LongRouteConstraint : IRouteConstraint + { + /// + public bool Match( + HttpContext httpContext, + IRouter route, + string routeKey, + RouteValueDictionary values, + RouteDirection routeDirection) + { + if (httpContext == null) + { + throw new ArgumentNullException(nameof(httpContext)); + } + + if (route == null) + { + throw new ArgumentNullException(nameof(route)); + } + + if (routeKey == null) + { + throw new ArgumentNullException(nameof(routeKey)); + } + + if (values == null) + { + throw new ArgumentNullException(nameof(values)); + } + + object value; + if (values.TryGetValue(routeKey, out value) && value != null) + { + if (value is long) + { + return true; + } + + long result; + var valueString = Convert.ToString(value, CultureInfo.InvariantCulture); + return long.TryParse(valueString, NumberStyles.Integer, CultureInfo.InvariantCulture, out result); + } + + return false; + } + } +} \ No newline at end of file diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/MaxLengthRouteConstraint.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/MaxLengthRouteConstraint.cs new file mode 100644 index 0000000000..42dde182ed --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/MaxLengthRouteConstraint.cs @@ -0,0 +1,73 @@ +// 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.Globalization; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Routing.Constraints +{ + /// + /// Constrains a route parameter to be a string with a maximum length. + /// + public class MaxLengthRouteConstraint : IRouteConstraint + { + /// + /// Initializes a new instance of the class. + /// + /// The maximum length allowed for the route parameter. + public MaxLengthRouteConstraint(int maxLength) + { + if (maxLength < 0) + { + var errorMessage = Resources.FormatArgumentMustBeGreaterThanOrEqualTo(0); + throw new ArgumentOutOfRangeException(nameof(maxLength), maxLength, errorMessage); + } + + MaxLength = maxLength; + } + + /// + /// Gets the maximum length allowed for the route parameter. + /// + public int MaxLength { get; } + + /// + public bool Match( + HttpContext httpContext, + IRouter route, + string routeKey, + RouteValueDictionary values, + RouteDirection routeDirection) + { + if (httpContext == null) + { + throw new ArgumentNullException(nameof(httpContext)); + } + + if (route == null) + { + throw new ArgumentNullException(nameof(route)); + } + + if (routeKey == null) + { + throw new ArgumentNullException(nameof(routeKey)); + } + + if (values == null) + { + throw new ArgumentNullException(nameof(values)); + } + + object value; + if (values.TryGetValue(routeKey, out value) && value != null) + { + var valueString = Convert.ToString(value, CultureInfo.InvariantCulture); + return valueString.Length <= MaxLength; + } + + return false; + } + } +} \ No newline at end of file diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/MaxRouteConstraint.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/MaxRouteConstraint.cs new file mode 100644 index 0000000000..e43dac85fe --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/MaxRouteConstraint.cs @@ -0,0 +1,71 @@ +// 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.Globalization; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Routing.Constraints +{ + /// + /// Constrains a route parameter to be an integer with a maximum value. + /// + public class MaxRouteConstraint : IRouteConstraint + { + /// + /// Initializes a new instance of the class. + /// + /// The maximum value allowed for the route parameter. + public MaxRouteConstraint(long max) + { + Max = max; + } + + /// + /// Gets the maximum allowed value of the route parameter. + /// + public long Max { get; private set; } + + /// + public bool Match( + HttpContext httpContext, + IRouter route, + string routeKey, + RouteValueDictionary values, + RouteDirection routeDirection) + { + if (httpContext == null) + { + throw new ArgumentNullException(nameof(httpContext)); + } + + if (route == null) + { + throw new ArgumentNullException(nameof(route)); + } + + if (routeKey == null) + { + throw new ArgumentNullException(nameof(routeKey)); + } + + if (values == null) + { + throw new ArgumentNullException(nameof(values)); + } + + object value; + if (values.TryGetValue(routeKey, out value) && value != null) + { + long longValue; + var valueString = Convert.ToString(value, CultureInfo.InvariantCulture); + if (long.TryParse(valueString, NumberStyles.Integer, CultureInfo.InvariantCulture, out longValue)) + { + return longValue <= Max; + } + } + + return false; + } + } +} \ No newline at end of file diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/MinLengthRouteConstraint.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/MinLengthRouteConstraint.cs new file mode 100644 index 0000000000..1ea64ae216 --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/MinLengthRouteConstraint.cs @@ -0,0 +1,73 @@ +// 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.Globalization; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Routing.Constraints +{ + /// + /// Constrains a route parameter to be a string with a minimum length. + /// + public class MinLengthRouteConstraint : IRouteConstraint + { + /// + /// Initializes a new instance of the class. + /// + /// The minimum length allowed for the route parameter. + public MinLengthRouteConstraint(int minLength) + { + if (minLength < 0) + { + var errorMessage = Resources.FormatArgumentMustBeGreaterThanOrEqualTo(0); + throw new ArgumentOutOfRangeException(nameof(minLength), minLength, errorMessage); + } + + MinLength = minLength; + } + + /// + /// Gets the minimum length allowed for the route parameter. + /// + public int MinLength { get; private set; } + + /// + public bool Match( + HttpContext httpContext, + IRouter route, + string routeKey, + RouteValueDictionary values, + RouteDirection routeDirection) + { + if (httpContext == null) + { + throw new ArgumentNullException(nameof(httpContext)); + } + + if (route == null) + { + throw new ArgumentNullException(nameof(route)); + } + + if (routeKey == null) + { + throw new ArgumentNullException(nameof(routeKey)); + } + + if (values == null) + { + throw new ArgumentNullException(nameof(values)); + } + + object value; + if (values.TryGetValue(routeKey, out value) && value != null) + { + var valueString = Convert.ToString(value, CultureInfo.InvariantCulture); + return valueString.Length >= MinLength; + } + + return false; + } + } +} \ No newline at end of file diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/MinRouteConstraint.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/MinRouteConstraint.cs new file mode 100644 index 0000000000..68357c59e7 --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/MinRouteConstraint.cs @@ -0,0 +1,71 @@ +// 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.Globalization; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Routing.Constraints +{ + /// + /// Constrains a route parameter to be a long with a minimum value. + /// + public class MinRouteConstraint : IRouteConstraint + { + /// + /// Initializes a new instance of the class. + /// + /// The minimum value allowed for the route parameter. + public MinRouteConstraint(long min) + { + Min = min; + } + + /// + /// Gets the minimum allowed value of the route parameter. + /// + public long Min { get; } + + /// + public bool Match( + HttpContext httpContext, + IRouter route, + string routeKey, + RouteValueDictionary values, + RouteDirection routeDirection) + { + if (httpContext == null) + { + throw new ArgumentNullException(nameof(httpContext)); + } + + if (route == null) + { + throw new ArgumentNullException(nameof(route)); + } + + if (routeKey == null) + { + throw new ArgumentNullException(nameof(routeKey)); + } + + if (values == null) + { + throw new ArgumentNullException(nameof(values)); + } + + object value; + if (values.TryGetValue(routeKey, out value) && value != null) + { + long longValue; + var valueString = Convert.ToString(value, CultureInfo.InvariantCulture); + if (long.TryParse(valueString, NumberStyles.Integer, CultureInfo.InvariantCulture, out longValue)) + { + return longValue >= Min; + } + } + + return false; + } + } +} \ No newline at end of file diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/OptionalRouteConstraint.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/OptionalRouteConstraint.cs new file mode 100644 index 0000000000..3990376410 --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/OptionalRouteConstraint.cs @@ -0,0 +1,66 @@ +// 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.Http; + +namespace Microsoft.AspNetCore.Routing.Constraints +{ + /// + /// Defines a constraint on an optional parameter. If the parameter is present, then it is constrained by InnerConstraint. + /// + public class OptionalRouteConstraint : IRouteConstraint + { + public OptionalRouteConstraint(IRouteConstraint innerConstraint) + { + if (innerConstraint == null) + { + throw new ArgumentNullException(nameof(innerConstraint)); + } + + InnerConstraint = innerConstraint; + } + + public IRouteConstraint InnerConstraint { get; } + + public bool Match( + HttpContext httpContext, + IRouter route, + string routeKey, + RouteValueDictionary values, + RouteDirection routeDirection) + { + if (httpContext == null) + { + throw new ArgumentNullException(nameof(httpContext)); + } + + if (route == null) + { + throw new ArgumentNullException(nameof(route)); + } + + if (routeKey == null) + { + throw new ArgumentNullException(nameof(routeKey)); + } + + if (values == null) + { + throw new ArgumentNullException(nameof(values)); + } + + object value; + if (values.TryGetValue(routeKey, out value)) + { + return InnerConstraint.Match(httpContext, + route, + routeKey, + values, + routeDirection); + } + + return true; + } + } +} \ No newline at end of file diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/RangeRouteConstraint.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/RangeRouteConstraint.cs new file mode 100644 index 0000000000..301a75f15a --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/RangeRouteConstraint.cs @@ -0,0 +1,85 @@ +// 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.Globalization; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Routing.Constraints +{ + /// + /// Constraints a route parameter to be an integer within a given range of values. + /// + public class RangeRouteConstraint : IRouteConstraint + { + /// + /// Initializes a new instance of the class. + /// + /// The minimum value. + /// The maximum value. + /// The minimum value should be less than or equal to the maximum value. + public RangeRouteConstraint(long min, long max) + { + if (min > max) + { + var errorMessage = Resources.FormatRangeConstraint_MinShouldBeLessThanOrEqualToMax("min", "max"); + throw new ArgumentOutOfRangeException(nameof(min), min, errorMessage); + } + + Min = min; + Max = max; + } + + /// + /// Gets the minimum allowed value of the route parameter. + /// + public long Min { get; private set; } + + /// + /// Gets the maximum allowed value of the route parameter. + /// + public long Max { get; private set; } + + /// + public bool Match( + HttpContext httpContext, + IRouter route, + string routeKey, + RouteValueDictionary values, + RouteDirection routeDirection) + { + if (httpContext == null) + { + throw new ArgumentNullException(nameof(httpContext)); + } + + if (route == null) + { + throw new ArgumentNullException(nameof(route)); + } + + if (routeKey == null) + { + throw new ArgumentNullException(nameof(routeKey)); + } + + if (values == null) + { + throw new ArgumentNullException(nameof(values)); + } + + object value; + if (values.TryGetValue(routeKey, out value) && value != null) + { + long longValue; + var valueString = Convert.ToString(value, CultureInfo.InvariantCulture); + if (Int64.TryParse(valueString, NumberStyles.Integer, CultureInfo.InvariantCulture, out longValue)) + { + return longValue >= Min && longValue <= Max; + } + } + + return false; + } + } +} \ No newline at end of file diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/RegexInlineRouteConstraint.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/RegexInlineRouteConstraint.cs new file mode 100644 index 0000000000..f0a9bc9875 --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/RegexInlineRouteConstraint.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. + +namespace Microsoft.AspNetCore.Routing.Constraints +{ + /// + /// Represents a regex constraint which can be used as an inlineConstraint. + /// + public class RegexInlineRouteConstraint : RegexRouteConstraint + { + /// + /// Initializes a new instance of the class. + /// + /// The regular expression pattern to match. + public RegexInlineRouteConstraint(string regexPattern) + : base(regexPattern) + { + } + } +} diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/RegexRouteConstraint.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/RegexRouteConstraint.cs new file mode 100644 index 0000000000..fb3d2390fe --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/RegexRouteConstraint.cs @@ -0,0 +1,80 @@ +// 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.Globalization; +using System.Text.RegularExpressions; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Routing.Constraints +{ + public class RegexRouteConstraint : IRouteConstraint + { + private static readonly TimeSpan RegexMatchTimeout = TimeSpan.FromSeconds(10); + + public RegexRouteConstraint(Regex regex) + { + if (regex == null) + { + throw new ArgumentNullException(nameof(regex)); + } + + Constraint = regex; + } + + public RegexRouteConstraint(string regexPattern) + { + if (regexPattern == null) + { + throw new ArgumentNullException(nameof(regexPattern)); + } + + Constraint = new Regex( + regexPattern, + RegexOptions.CultureInvariant | RegexOptions.IgnoreCase, + RegexMatchTimeout); + } + + public Regex Constraint { get; private set; } + + public bool Match( + HttpContext httpContext, + IRouter route, + string routeKey, + RouteValueDictionary values, + RouteDirection routeDirection) + { + if (httpContext == null) + { + throw new ArgumentNullException(nameof(httpContext)); + } + + if (route == null) + { + throw new ArgumentNullException(nameof(route)); + } + + if (routeKey == null) + { + throw new ArgumentNullException(nameof(routeKey)); + } + + if (values == null) + { + throw new ArgumentNullException(nameof(values)); + } + + object routeValue; + + if (values.TryGetValue(routeKey, out routeValue) + && routeValue != null) + { + var parameterValueString = Convert.ToString(routeValue, CultureInfo.InvariantCulture); + + return Constraint.IsMatch(parameterValueString); + } + + return false; + } + } +} diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/RequiredRouteConstraint.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/RequiredRouteConstraint.cs new file mode 100644 index 0000000000..e03d618565 --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/RequiredRouteConstraint.cs @@ -0,0 +1,58 @@ +// 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.Globalization; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Routing.Constraints +{ + /// + /// Constraints a route parameter that must have a value. + /// + /// + /// This constraint is primarily used to enforce that a non-parameter value is present during + /// URL generation. + /// + public class RequiredRouteConstraint : IRouteConstraint + { + /// + public bool Match( + HttpContext httpContext, + IRouter route, + string routeKey, + RouteValueDictionary values, + RouteDirection routeDirection) + { + if (httpContext == null) + { + throw new ArgumentNullException(nameof(httpContext)); + } + + if (route == null) + { + throw new ArgumentNullException(nameof(route)); + } + + if (routeKey == null) + { + throw new ArgumentNullException(nameof(routeKey)); + } + + if (values == null) + { + throw new ArgumentNullException(nameof(values)); + } + + object value; + if (values.TryGetValue(routeKey, out value) && value != null) + { + // In routing the empty string is equivalent to null, which is equivalent to an unset value. + var valueString = Convert.ToString(value, CultureInfo.InvariantCulture); + return !string.IsNullOrEmpty(valueString); + } + + return false; + } + } +} \ No newline at end of file diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/StringRouteConstraint.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/StringRouteConstraint.cs new file mode 100644 index 0000000000..e7d92ef13c --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/StringRouteConstraint.cs @@ -0,0 +1,67 @@ +// 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.Globalization; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Routing.Constraints +{ + /// + /// Constrains a route parameter to contain only a specified strign. + /// + public class StringRouteConstraint : IRouteConstraint + { + private string _value; + + /// + /// Initializes a new instance of the class. + /// + /// The constraint value to match. + public StringRouteConstraint(string value) + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + _value = value; + } + + /// + public bool Match(HttpContext httpContext, IRouter route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection) + { + if (httpContext == null) + { + throw new ArgumentNullException(nameof(httpContext)); + } + + if (route == null) + { + throw new ArgumentNullException(nameof(route)); + } + + if (routeKey == null) + { + throw new ArgumentNullException(nameof(routeKey)); + } + + if (values == null) + { + throw new ArgumentNullException(nameof(values)); + } + + object routeValue; + + if (values.TryGetValue(routeKey, out routeValue) + && routeValue != null) + { + var parameterValueString = Convert.ToString(routeValue, CultureInfo.InvariantCulture); + + return parameterValueString.Equals(_value, StringComparison.OrdinalIgnoreCase); + } + + return false; + } + } +} \ No newline at end of file diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/DefaultInlineConstraintResolver.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/DefaultInlineConstraintResolver.cs new file mode 100644 index 0000000000..7426516db2 --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/DefaultInlineConstraintResolver.cs @@ -0,0 +1,156 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Reflection; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Routing +{ + /// + /// The default implementation of . Resolves constraints by parsing + /// a constraint key and constraint arguments, using a map to resolve the constraint type, and calling an + /// appropriate constructor for the constraint type. + /// + public class DefaultInlineConstraintResolver : IInlineConstraintResolver + { + private readonly IDictionary _inlineConstraintMap; + + /// + /// Initializes a new instance of the class. + /// + /// + /// Accessor for containing the constraints of interest. + /// + public DefaultInlineConstraintResolver(IOptions routeOptions) + { + _inlineConstraintMap = routeOptions.Value.ConstraintMap; + } + + /// + /// + /// A typical constraint looks like the following + /// "exampleConstraint(arg1, arg2, 12)". + /// Here if the type registered for exampleConstraint has a single constructor with one argument, + /// The entire string "arg1, arg2, 12" will be treated as a single argument. + /// In all other cases arguments are split at comma. + /// + public virtual IRouteConstraint ResolveConstraint(string inlineConstraint) + { + if (inlineConstraint == null) + { + throw new ArgumentNullException(nameof(inlineConstraint)); + } + + string constraintKey; + string argumentString; + var indexOfFirstOpenParens = inlineConstraint.IndexOf('('); + if (indexOfFirstOpenParens >= 0 && inlineConstraint.EndsWith(")", StringComparison.Ordinal)) + { + constraintKey = inlineConstraint.Substring(0, indexOfFirstOpenParens); + argumentString = inlineConstraint.Substring(indexOfFirstOpenParens + 1, + inlineConstraint.Length - indexOfFirstOpenParens - 2); + } + else + { + constraintKey = inlineConstraint; + argumentString = null; + } + + Type constraintType; + if (!_inlineConstraintMap.TryGetValue(constraintKey, out constraintType)) + { + // Cannot resolve the constraint key + return null; + } + + if (!typeof(IRouteConstraint).GetTypeInfo().IsAssignableFrom(constraintType.GetTypeInfo())) + { + throw new RouteCreationException( + Resources.FormatDefaultInlineConstraintResolver_TypeNotConstraint( + constraintType, constraintKey, typeof(IRouteConstraint).Name)); + } + + try + { + return CreateConstraint(constraintType, argumentString); + } + catch (RouteCreationException) + { + throw; + } + catch (Exception exception) + { + throw new RouteCreationException( + $"An error occurred while trying to create an instance of route constraint '{constraintType.FullName}'.", + exception); + } + } + + private static IRouteConstraint CreateConstraint(Type constraintType, string argumentString) + { + // No arguments - call the default constructor + if (argumentString == null) + { + return (IRouteConstraint)Activator.CreateInstance(constraintType); + } + + var constraintTypeInfo = constraintType.GetTypeInfo(); + ConstructorInfo activationConstructor = null; + object[] parameters = null; + var constructors = constraintTypeInfo.DeclaredConstructors.ToArray(); + + // If there is only one constructor and it has a single parameter, pass the argument string directly + // This is necessary for the Regex RouteConstraint to ensure that patterns are not split on commas. + if (constructors.Length == 1 && constructors[0].GetParameters().Length == 1) + { + activationConstructor = constructors[0]; + parameters = ConvertArguments(activationConstructor.GetParameters(), new string[] { argumentString }); + } + else + { + var arguments = argumentString.Split(',').Select(argument => argument.Trim()).ToArray(); + + var matchingConstructors = constructors.Where(ci => ci.GetParameters().Length == arguments.Length) + .ToArray(); + var constructorMatches = matchingConstructors.Length; + + if (constructorMatches == 0) + { + throw new RouteCreationException( + Resources.FormatDefaultInlineConstraintResolver_CouldNotFindCtor( + constraintTypeInfo.Name, arguments.Length)); + } + else if (constructorMatches == 1) + { + activationConstructor = matchingConstructors[0]; + parameters = ConvertArguments(activationConstructor.GetParameters(), arguments); + } + else + { + throw new RouteCreationException( + Resources.FormatDefaultInlineConstraintResolver_AmbiguousCtors( + constraintTypeInfo.Name, arguments.Length)); + } + } + + return (IRouteConstraint)activationConstructor.Invoke(parameters); + } + + private static object[] ConvertArguments(ParameterInfo[] parameterInfos, string[] arguments) + { + var parameters = new object[parameterInfos.Length]; + for (var i = 0; i < parameterInfos.Length; i++) + { + var parameter = parameterInfos[i]; + var parameterType = parameter.ParameterType; + parameters[i] = Convert.ChangeType(arguments[i], parameterType, CultureInfo.InvariantCulture); + } + + return parameters; + } + } +} diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/DependencyInjection/RoutingServiceCollectionExtensions.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/DependencyInjection/RoutingServiceCollectionExtensions.cs new file mode 100644 index 0000000000..d6883f6c95 --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/DependencyInjection/RoutingServiceCollectionExtensions.cs @@ -0,0 +1,79 @@ +// 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; +using Microsoft.AspNetCore.Routing.Internal; +using Microsoft.AspNetCore.Routing.Tree; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.ObjectPool; + +namespace Microsoft.Extensions.DependencyInjection +{ + /// + /// Contains extension methods to . + /// + public static class RoutingServiceCollectionExtensions + { + /// + /// Adds services required for routing requests. + /// + /// The to add the services to. + /// The so that additional calls can be chained. + public static IServiceCollection AddRouting(this IServiceCollection services) + { + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } + + services.TryAddTransient(); + services.TryAddSingleton>(s => + { + var provider = s.GetRequiredService(); + return provider.Create(new UriBuilderContextPooledObjectPolicy()); + }); + + // The TreeRouteBuilder is a builder for creating routes, it should stay transient because it's + // stateful. + services.TryAdd(ServiceDescriptor.Transient(s => + { + var loggerFactory = s.GetRequiredService(); + var objectPool = s.GetRequiredService>(); + var constraintResolver = s.GetRequiredService(); + return new TreeRouteBuilder(loggerFactory, objectPool, constraintResolver); + })); + + services.TryAddSingleton(typeof(RoutingMarkerService)); + + return services; + } + + /// + /// Adds services required for routing requests. + /// + /// The to add the services to. + /// The routing options to configure the middleware with. + /// The so that additional calls can be chained. + public static IServiceCollection AddRouting( + this IServiceCollection services, + Action configureOptions) + { + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } + + if (configureOptions == null) + { + throw new ArgumentNullException(nameof(configureOptions)); + } + + services.Configure(configureOptions); + services.AddRouting(); + + return services; + } + } +} \ No newline at end of file diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/IInlineConstraintResolver.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/IInlineConstraintResolver.cs new file mode 100644 index 0000000000..d4e0fd028f --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/IInlineConstraintResolver.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 +{ + /// + /// Defines an abstraction for resolving inline constraints as instances of . + /// + public interface IInlineConstraintResolver + { + /// + /// Resolves the inline constraint. + /// + /// The inline constraint to resolve. + /// The the inline constraint was resolved to. + IRouteConstraint ResolveConstraint(string inlineConstraint); + } +} \ No newline at end of file diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/INamedRouter.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/INamedRouter.cs new file mode 100644 index 0000000000..b04b7d2f56 --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/INamedRouter.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 interface INamedRouter : IRouter + { + string Name { get; } + } +} \ No newline at end of file diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/IRouteBuilder.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/IRouteBuilder.cs new file mode 100644 index 0000000000..2969342724 --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/IRouteBuilder.cs @@ -0,0 +1,42 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Builder; + +namespace Microsoft.AspNetCore.Routing +{ + /// + /// Defines a contract for a route builder in an application. A route builder specifies the routes for + /// an application. + /// + public interface IRouteBuilder + { + /// + /// Gets the . + /// + IApplicationBuilder ApplicationBuilder { get; } + + /// + /// Gets or sets the default that is used as a handler if an + /// is added to the list of routes but does not specify its own. + /// + IRouter DefaultHandler { get; set; } + + /// + /// Gets the sets the used to resolve services for routes. + /// + IServiceProvider ServiceProvider { get; } + + /// + /// Gets the routes configured in the builder. + /// + IList Routes { get; } + + /// + /// Builds an that routes the routes specified in the property. + /// + IRouter Build(); + } +} \ No newline at end of file diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/IRouteCollection.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/IRouteCollection.cs new file mode 100644 index 0000000000..084f0aef67 --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/IRouteCollection.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 interface IRouteCollection : IRouter + { + void Add(IRouter router); + } +} diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/InlineRouteParameterParser.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/InlineRouteParameterParser.cs new file mode 100644 index 0000000000..78131eba97 --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/InlineRouteParameterParser.cs @@ -0,0 +1,243 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Routing.Template; + +namespace Microsoft.AspNetCore.Routing +{ + public static class InlineRouteParameterParser + { + public static TemplatePart ParseRouteParameter(string routeParameter) + { + if (routeParameter == null) + { + throw new ArgumentNullException(nameof(routeParameter)); + } + + if (routeParameter.Length == 0) + { + return TemplatePart.CreateParameter( + name: string.Empty, + isCatchAll: false, + isOptional: false, + defaultValue: null, + inlineConstraints: null); + } + + var startIndex = 0; + var endIndex = routeParameter.Length - 1; + + var isCatchAll = false; + var isOptional = false; + + if (routeParameter[0] == '*') + { + isCatchAll = true; + startIndex++; + } + + if (routeParameter[endIndex] == '?') + { + isOptional = true; + endIndex--; + } + + var currentIndex = startIndex; + + // Parse parameter name + var parameterName = string.Empty; + + while (currentIndex <= endIndex) + { + var currentChar = routeParameter[currentIndex]; + + if ((currentChar == ':' || currentChar == '=') && startIndex != currentIndex) + { + // Parameter names are allowed to start with delimiters used to denote constraints or default values. + // i.e. "=foo" or ":bar" would be treated as parameter names rather than default value or constraint + // specifications. + parameterName = routeParameter.Substring(startIndex, currentIndex - startIndex); + + // Roll the index back and move to the constraint parsing stage. + currentIndex--; + break; + } + else if (currentIndex == endIndex) + { + parameterName = routeParameter.Substring(startIndex, currentIndex - startIndex + 1); + } + + currentIndex++; + } + + var parseResults = ParseConstraints(routeParameter, currentIndex, endIndex); + currentIndex = parseResults.CurrentIndex; + + string defaultValue = null; + if (currentIndex <= endIndex && + routeParameter[currentIndex] == '=') + { + defaultValue = routeParameter.Substring(currentIndex + 1, endIndex - currentIndex); + } + + return TemplatePart.CreateParameter(parameterName, + isCatchAll, + isOptional, + defaultValue, + parseResults.Constraints); + } + + private static ConstraintParseResults ParseConstraints( + string routeParameter, + int currentIndex, + int endIndex) + { + var inlineConstraints = new List(); + var state = ParseState.Start; + var startIndex = currentIndex; + do + { + var currentChar = currentIndex > endIndex ? null : (char?)routeParameter[currentIndex]; + switch (state) + { + case ParseState.Start: + switch (currentChar) + { + case null: + state = ParseState.End; + break; + case ':': + state = ParseState.ParsingName; + startIndex = currentIndex + 1; + break; + case '(': + state = ParseState.InsideParenthesis; + break; + case '=': + state = ParseState.End; + currentIndex--; + break; + } + break; + case ParseState.InsideParenthesis: + switch (currentChar) + { + case null: + state = ParseState.End; + var constraintText = routeParameter.Substring(startIndex, currentIndex - startIndex); + inlineConstraints.Add(new InlineConstraint(constraintText)); + break; + case ')': + // Only consume a ')' token if + // (a) it is the last token + // (b) the next character is the start of the new constraint ':' + // (c) the next character is the start of the default value. + + var nextChar = currentIndex + 1 > endIndex ? null : (char?)routeParameter[currentIndex + 1]; + switch (nextChar) + { + case null: + state = ParseState.End; + constraintText = routeParameter.Substring(startIndex, currentIndex - startIndex + 1); + inlineConstraints.Add(new InlineConstraint(constraintText)); + break; + case ':': + state = ParseState.Start; + constraintText = routeParameter.Substring(startIndex, currentIndex - startIndex + 1); + inlineConstraints.Add(new InlineConstraint(constraintText)); + startIndex = currentIndex + 1; + break; + case '=': + state = ParseState.End; + constraintText = routeParameter.Substring(startIndex, currentIndex - startIndex + 1); + inlineConstraints.Add(new InlineConstraint(constraintText)); + break; + } + break; + case ':': + case '=': + // In the original implementation, the Regex would've backtracked if it encountered an + // unbalanced opening bracket followed by (not necessarily immediatiely) a delimiter. + // Simply verifying that the parantheses will eventually be closed should suffice to + // determine if the terminator needs to be consumed as part of the current constraint + // specification. + var indexOfClosingParantheses = routeParameter.IndexOf(')', currentIndex + 1); + if (indexOfClosingParantheses == -1) + { + constraintText = routeParameter.Substring(startIndex, currentIndex - startIndex); + inlineConstraints.Add(new InlineConstraint(constraintText)); + + if (currentChar == ':') + { + state = ParseState.ParsingName; + startIndex = currentIndex + 1; + } + else + { + state = ParseState.End; + currentIndex--; + } + } + else + { + currentIndex = indexOfClosingParantheses; + } + + break; + } + break; + case ParseState.ParsingName: + switch (currentChar) + { + case null: + state = ParseState.End; + var constraintText = routeParameter.Substring(startIndex, currentIndex - startIndex); + inlineConstraints.Add(new InlineConstraint(constraintText)); + break; + case ':': + constraintText = routeParameter.Substring(startIndex, currentIndex - startIndex); + inlineConstraints.Add(new InlineConstraint(constraintText)); + startIndex = currentIndex + 1; + break; + case '(': + state = ParseState.InsideParenthesis; + break; + case '=': + state = ParseState.End; + constraintText = routeParameter.Substring(startIndex, currentIndex - startIndex); + inlineConstraints.Add(new InlineConstraint(constraintText)); + currentIndex--; + break; + } + break; + } + + currentIndex++; + + } while (state != ParseState.End); + + return new ConstraintParseResults + { + CurrentIndex = currentIndex, + Constraints = inlineConstraints + }; + } + + private enum ParseState + { + Start, + ParsingName, + InsideParenthesis, + End + } + + private struct ConstraintParseResults + { + public int CurrentIndex; + + public IEnumerable Constraints; + } + } +} \ No newline at end of file diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Internal/BufferValue.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Internal/BufferValue.cs new file mode 100644 index 0000000000..578c396b4d --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Internal/BufferValue.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.Internal +{ + public struct BufferValue + { + public BufferValue(string value, bool requiresEncoding) + { + Value = value; + RequiresEncoding = requiresEncoding; + } + + public bool RequiresEncoding { get; } + + public string Value { get; } + } +} diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Internal/LinkGenerationDecisionTree.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Internal/LinkGenerationDecisionTree.cs new file mode 100644 index 0000000000..f71e6dd74d --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Internal/LinkGenerationDecisionTree.cs @@ -0,0 +1,163 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.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. + public class LinkGenerationDecisionTree + { + private readonly DecisionTreeNode _root; + + public LinkGenerationDecisionTree(IReadOnlyList entries) + { + _root = DecisionTreeBuilder.GenerateTree( + entries, + new OutboundMatchClassifier()); + } + + public IList GetMatches(VirtualPathContext context) + { + // Perf: Avoid allocation for List if there aren't any Matches or Criteria + if (_root.Matches.Count > 0 || _root.Criteria.Count > 0) + { + var results = new List(); + Walk(results, context, _root, isFallbackPath: false); + results.Sort(OutboundMatchResultComparer.Instance); + return results; + } + + return null; + } + + // We need to recursively walk the decision tree based on the provided route data + // (context.Values + context.AmbientValues) to find all entries that match. This process is + // virtually identical to action selection. + // + // Each entry has a collection of 'required link values' that must be satisfied. These are + // key-value pairs that make up the decision tree. + // + // A 'require link value' is considered satisfied IF: + // 1. The value in context.Values matches the required value OR + // 2. There is no value in context.Values and the value in context.AmbientValues matches OR + // 3. The required value is 'null' and there is no value in context.Values. + // + // Ex: + // entry requires { area = null, controller = Store, action = Buy } + // context.Values = { controller = Store, action = Buy } + // context.AmbientValues = { area = Help, controller = AboutStore, action = HowToBuyThings } + // + // In this case the entry is a match. The 'controller' and 'action' are both supplied by context.Values, + // and the 'area' is satisfied because there's NOT a value in context.Values. It's OK to ignore ambient + // values in link generation. + // + // If another entry existed like { area = Help, controller = Store, action = Buy }, this would also + // match. + // + // The decision tree uses a tree data structure to execute these rules across all candidates at once. + private void Walk( + List results, + VirtualPathContext context, + DecisionTreeNode node, + bool isFallbackPath) + { + // Any entries in node.Matches have had all their required values satisfied, so add them + // to the results. + for (var i = 0; i < node.Matches.Count; i++) + { + results.Add(new OutboundMatchResult(node.Matches[i], isFallbackPath)); + } + + for (var i = 0; i < node.Criteria.Count; i++) + { + var criterion = node.Criteria[i]; + var key = criterion.Key; + + object value; + if (context.Values.TryGetValue(key, out value)) + { + DecisionTreeNode branch; + if (criterion.Branches.TryGetValue(value ?? string.Empty, out branch)) + { + Walk(results, context, branch, isFallbackPath); + } + } + else + { + // If a value wasn't explicitly supplied, match BOTH the ambient value and the empty value + // if an ambient value was supplied. The path explored with the empty value is considered + // the fallback path. + DecisionTreeNode branch; + if (context.AmbientValues.TryGetValue(key, out value) && + !criterion.Branches.Comparer.Equals(value, string.Empty)) + { + if (criterion.Branches.TryGetValue(value, out branch)) + { + Walk(results, context, branch, isFallbackPath); + } + } + + if (criterion.Branches.TryGetValue(string.Empty, out branch)) + { + Walk(results, context, branch, isFallbackPath: true); + } + } + } + } + + private class OutboundMatchClassifier : IClassifier + { + public OutboundMatchClassifier() + { + ValueComparer = new RouteValueEqualityComparer(); + } + + public IEqualityComparer ValueComparer { get; private set; } + + public IDictionary GetCriteria(OutboundMatch item) + { + var results = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var kvp in item.Entry.RequiredLinkValues) + { + results.Add(kvp.Key, new DecisionCriterionValue(kvp.Value ?? string.Empty)); + } + + return results; + } + } + + private class OutboundMatchResultComparer : IComparer + { + public static readonly OutboundMatchResultComparer Instance = new OutboundMatchResultComparer(); + + public int Compare(OutboundMatchResult x, OutboundMatchResult y) + { + // For this comparison lower is better. + if (x.Match.Entry.Order != y.Match.Entry.Order) + { + return x.Match.Entry.Order.CompareTo(y.Match.Entry.Order); + } + + if (x.Match.Entry.Precedence != y.Match.Entry.Precedence) + { + // Reversed because higher is better + return y.Match.Entry.Precedence.CompareTo(x.Match.Entry.Precedence); + } + + if (x.IsFallbackMatch != y.IsFallbackMatch) + { + // A fallback match is worse than a non-fallback + return x.IsFallbackMatch.CompareTo(y.IsFallbackMatch); + } + + return StringComparer.Ordinal.Compare( + x.Match.Entry.RouteTemplate.TemplateText, + y.Match.Entry.RouteTemplate.TemplateText); + } + } + } +} \ No newline at end of file diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Internal/OutboundMatchResult.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Internal/OutboundMatchResult.cs new file mode 100644 index 0000000000..aee505f572 --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Internal/OutboundMatchResult.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 Microsoft.AspNetCore.Routing.Tree; + +namespace Microsoft.AspNetCore.Routing.Internal +{ + public struct OutboundMatchResult + { + public OutboundMatchResult(OutboundMatch match, bool isFallbackMatch) + { + Match = match; + IsFallbackMatch = isFallbackMatch; + } + + public OutboundMatch Match { get; } + + public bool IsFallbackMatch { get; } + } +} \ No newline at end of file diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Internal/PathTokenizer.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Internal/PathTokenizer.cs new file mode 100644 index 0000000000..9418989fdb --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Internal/PathTokenizer.cs @@ -0,0 +1,205 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.Routing.Internal +{ + public struct PathTokenizer : IReadOnlyList + { + private readonly string _path; + private int _count; + + public PathTokenizer(PathString path) + { + _path = path.Value; + _count = -1; + } + + public int Count + { + get + { + if (_count == -1) + { + // We haven't computed the real count of segments yet. + if (_path.Length == 0) + { + // The empty string has length of 0. + _count = 0; + return _count; + } + + // A string of length 1 must be "/" - all PathStrings start with '/' + if (_path.Length == 1) + { + // We treat this as empty - there's nothing to parse here for routing, because routing ignores + // a trailing slash. + Debug.Assert(_path[0] == '/'); + _count = 0; + return _count; + } + + // This is a non-trival PathString + _count = 1; + + // Since a non-empty PathString must begin with a `/`, we can just count the number of occurrences + // of `/` to find the number of segments. However, we don't look at the last character, because + // routing ignores a trailing slash. + for (var i = 1; i < _path.Length - 1; i++) + { + if (_path[i] == '/') + { + _count++; + } + } + } + + return _count; + } + } + + public StringSegment this[int index] + { + get + { + if (index >= Count) + { + throw new IndexOutOfRangeException(); + } + + + var currentSegmentIndex = 0; + var currentSegmentStart = 1; + + // Skip the first `/`. + var delimiterIndex = 1; + while ((delimiterIndex = _path.IndexOf('/', delimiterIndex)) != -1) + { + if (currentSegmentIndex++ == index) + { + return new StringSegment(_path, currentSegmentStart, delimiterIndex - currentSegmentStart); + } + else + { + currentSegmentStart = delimiterIndex + 1; + delimiterIndex++; + } + } + + // If we get here we're at the end of the string. The implementation of .Count should protect us + // from these cases. + Debug.Assert(_path[_path.Length - 1] != '/'); + Debug.Assert(currentSegmentIndex == index); + + return new StringSegment(_path, currentSegmentStart, _path.Length - currentSegmentStart); + } + } + + public Enumerator GetEnumerator() + { + return new Enumerator(this); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public struct Enumerator : IEnumerator + { + private readonly string _path; + + private int _index; + private int _length; + + public Enumerator(PathTokenizer tokenizer) + { + _path = tokenizer._path; + + _index = -1; + _length = -1; + } + + public StringSegment Current + { + get + { + return new StringSegment(_path, _index, _length); + } + } + + object IEnumerator.Current + { + get + { + return Current; + } + } + + public void Dispose() + { + } + + public bool MoveNext() + { + if (_path == null || _path.Length <= 1) + { + return false; + } + + if (_index == -1) + { + // Skip the first `/`. + _index = 1; + } + else + { + // Skip to the end of the previous segment + the separator. + _index += _length + 1; + } + + if (_index >= _path.Length) + { + // We're at the end + return false; + } + + var delimiterIndex = _path.IndexOf('/', _index); + if (delimiterIndex != -1) + { + _length = delimiterIndex - _index; + return true; + } + + // We might have some trailing text after the last separator. + if (_path[_path.Length - 1] == '/') + { + // If the last char is a '/' then it's just a trailing slash, we don't have another segment. + return false; + } + else + { + _length = _path.Length - _index; + return true; + } + } + + public void Reset() + { + _index = -1; + _length = -1; + } + } + } +} diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Internal/RoutingMarkerService.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Internal/RoutingMarkerService.cs new file mode 100644 index 0000000000..b180294316 --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Internal/RoutingMarkerService.cs @@ -0,0 +1,15 @@ +// 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.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Routing.Internal +{ + /// + /// A marker class used to determine if all the routing services were added + /// to the before routing is configured. + /// + public class RoutingMarkerService + { + } +} \ No newline at end of file diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Internal/SegmentState.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Internal/SegmentState.cs new file mode 100644 index 0000000000..35076a0678 --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Internal/SegmentState.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. + +namespace Microsoft.AspNetCore.Routing.Internal +{ + // Segments are treated as all-or-none. We should never output a partial segment. + // If we add any subsegment of this segment to the generated URI, we have to add + // the complete match. For example, if the subsegment is "{p1}-{p2}.xml" and we + // used a value for {p1}, we have to output the entire segment up to the next "/". + // Otherwise we could end up with the partial segment "v1" instead of the entire + // segment "v1-v2.xml". + public enum SegmentState + { + Beginning, + Inside, + } +} diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Internal/UriBuilderContextPooledObjectPolicy.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Internal/UriBuilderContextPooledObjectPolicy.cs new file mode 100644 index 0000000000..953d6a86c4 --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Internal/UriBuilderContextPooledObjectPolicy.cs @@ -0,0 +1,22 @@ +// 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.Text.Encodings.Web; +using Microsoft.Extensions.ObjectPool; + +namespace Microsoft.AspNetCore.Routing.Internal +{ + public class UriBuilderContextPooledObjectPolicy : IPooledObjectPolicy + { + public UriBuildingContext Create() + { + return new UriBuildingContext(UrlEncoder.Default); + } + + public bool Return(UriBuildingContext obj) + { + obj.Clear(); + return true; + } + } +} diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Internal/UriBuildingContext.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Internal/UriBuildingContext.cs new file mode 100644 index 0000000000..3b78fe8c78 --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Internal/UriBuildingContext.cs @@ -0,0 +1,205 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Text; +using System.Text.Encodings.Web; + +namespace Microsoft.AspNetCore.Routing.Internal +{ + [DebuggerDisplay("{DebuggerToString(),nq}")] + public class UriBuildingContext + { + // Holds the 'accepted' parts of the uri. + private readonly StringBuilder _uri; + + // Holds the 'optional' parts of the uri. We need a secondary buffer to handle cases where an optional + // segment is in the middle of the uri. We don't know if we need to write it out - if it's + // followed by other optional segments than we will just throw it away. + private readonly List _buffer; + private readonly UrlEncoder _urlEncoder; + + private bool _hasEmptySegment; + private int _lastValueOffset; + + public UriBuildingContext(UrlEncoder urlEncoder) + { + _urlEncoder = urlEncoder; + _uri = new StringBuilder(); + _buffer = new List(); + Writer = new StringWriter(_uri); + _lastValueOffset = -1; + + BufferState = SegmentState.Beginning; + UriState = SegmentState.Beginning; + } + + public SegmentState BufferState { get; private set; } + + public SegmentState UriState { get; private set; } + + public TextWriter Writer { get; } + + public bool Accept(string value) + { + if (string.IsNullOrEmpty(value)) + { + if (UriState == SegmentState.Inside || BufferState == SegmentState.Inside) + { + // We can't write an 'empty' part inside a segment + return false; + } + else + { + _hasEmptySegment = true; + return true; + } + } + else if (_hasEmptySegment) + { + // We're trying to write text after an empty segment - this is not allowed. + return false; + } + + for (var i = 0; i < _buffer.Count; i++) + { + if (_buffer[i].RequiresEncoding) + { + _urlEncoder.Encode(Writer, _buffer[i].Value); + } + else + { + _uri.Append(_buffer[i].Value); + } + } + _buffer.Clear(); + + if (UriState == SegmentState.Beginning && BufferState == SegmentState.Beginning) + { + if (_uri.Length != 0) + { + _uri.Append("/"); + } + } + + BufferState = SegmentState.Inside; + UriState = SegmentState.Inside; + + _lastValueOffset = _uri.Length; + // Allow the first segment to have a leading slash. + // This prevents the leading slash from PathString segments from being encoded. + if (_uri.Length == 0 && value.Length > 0 && value[0] == '/') + { + _uri.Append("/"); + _urlEncoder.Encode(Writer, value, 1, value.Length - 1); + } + else + { + _urlEncoder.Encode(Writer, value); + } + + return true; + } + + public void Remove(string literal) + { + Debug.Assert(_lastValueOffset != -1, "Cannot invoke Remove more than once."); + _uri.Length = _lastValueOffset; + _lastValueOffset = -1; + } + + public bool Buffer(string value) + { + if (string.IsNullOrEmpty(value)) + { + if (BufferState == SegmentState.Inside) + { + // We can't write an 'empty' part inside a segment + return false; + } + else + { + _hasEmptySegment = true; + return true; + } + } + else if (_hasEmptySegment) + { + // We're trying to write text after an empty segment - this is not allowed. + return false; + } + + if (UriState == SegmentState.Inside) + { + // We've already written part of this segment so there's no point in buffering, we need to + // write out the rest or give up. + var result = Accept(value); + + // We've already checked the conditions that could result in a rejected part, so this should + // always be true. + Debug.Assert(result); + + return result; + } + + if (UriState == SegmentState.Beginning && BufferState == SegmentState.Beginning) + { + if (_uri.Length != 0 || _buffer.Count != 0) + { + _buffer.Add(new BufferValue("/", requiresEncoding: false)); + } + + BufferState = SegmentState.Inside; + } + + _buffer.Add(new BufferValue(value, requiresEncoding: true)); + return true; + } + + public void EndSegment() + { + BufferState = SegmentState.Beginning; + UriState = SegmentState.Beginning; + } + + public void Clear() + { + _uri.Clear(); + if (_uri.Capacity > 128) + { + // We don't want to retain too much memory if this is getting pooled. + _uri.Capacity = 128; + } + + _buffer.Clear(); + if (_buffer.Capacity > 8) + { + _buffer.Capacity = 8; + } + + _hasEmptySegment = false; + _lastValueOffset = -1; + BufferState = SegmentState.Beginning; + UriState = SegmentState.Beginning; + } + + public override string ToString() + { + // We can ignore any currently buffered segments - they are are guaranteed to be 'defaults'. + if (_uri.Length > 0 && _uri[0] != '/') + { + // Normalize generated paths so that they always contain a leading slash. + _uri.Insert(0, '/'); + } + + return _uri.ToString(); + } + + private string DebuggerToString() + { + return string.Format("{{Accepted: '{0}' Buffered: '{1}'}}", _uri, string.Join("", _buffer)); + } + } +} diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Logging/RouteConstraintMatcherExtensions.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Logging/RouteConstraintMatcherExtensions.cs new file mode 100644 index 0000000000..9a19250f30 --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Logging/RouteConstraintMatcherExtensions.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. + +using System; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Routing.Logging +{ + internal static class RouteConstraintMatcherExtensions + { + private static readonly Action _routeValueDoesNotMatchConstraint; + + static RouteConstraintMatcherExtensions() + { + _routeValueDoesNotMatchConstraint = LoggerMessage.Define( + LogLevel.Debug, + 1, + "Route value '{RouteValue}' with key '{RouteKey}' did not match " + + "the constraint '{RouteConstraint}'."); + } + + public static void RouteValueDoesNotMatchConstraint( + this ILogger logger, + object routeValue, + string routeKey, + IRouteConstraint routeConstraint) + { + _routeValueDoesNotMatchConstraint(logger, routeValue, routeKey, routeConstraint, null); + } + } +} diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Logging/RouterMiddlewareLoggerExtensions.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Logging/RouterMiddlewareLoggerExtensions.cs new file mode 100644 index 0000000000..35f05272cd --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Logging/RouterMiddlewareLoggerExtensions.cs @@ -0,0 +1,26 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Routing.Logging +{ + internal static class RouterMiddlewareLoggerExtensions + { + private static readonly Action _requestDidNotMatchRoutes; + + static RouterMiddlewareLoggerExtensions() + { + _requestDidNotMatchRoutes = LoggerMessage.Define( + LogLevel.Debug, + 1, + "Request did not match any routes."); + } + + public static void RequestDidNotMatchRoutes(this ILogger logger) + { + _requestDidNotMatchRoutes(logger, null); + } + } +} diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Logging/TreeRouterLoggerExtensions.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Logging/TreeRouterLoggerExtensions.cs new file mode 100644 index 0000000000..3f6af8e5dc --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Logging/TreeRouterLoggerExtensions.cs @@ -0,0 +1,29 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Routing.Logging +{ + internal static class TreeRouterLoggerExtensions + { + private static readonly Action _matchedRoute; + + static TreeRouterLoggerExtensions() + { + _matchedRoute = LoggerMessage.Define( + LogLevel.Debug, + 1, + "Request successfully matched the route with name '{RouteName}' and template '{RouteTemplate}'."); + } + + public static void MatchedRoute( + this ILogger logger, + string routeName, + string routeTemplate) + { + _matchedRoute(logger, routeName, routeTemplate, null); + } + } +} diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/MapRouteRouteBuilderExtensions.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/MapRouteRouteBuilderExtensions.cs new file mode 100644 index 0000000000..ef55007af3 --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/MapRouteRouteBuilderExtensions.cs @@ -0,0 +1,126 @@ +// 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; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Builder +{ + /// + /// Provides extension methods for to add routes. + /// + public static class MapRouteRouteBuilderExtensions + { + /// + /// Adds a route to the with the specified name and template. + /// + /// The to add the route to. + /// The name of the route. + /// The URL pattern of the route. + /// A reference to this instance after the operation has completed. + public static IRouteBuilder MapRoute( + this IRouteBuilder routeBuilder, + string name, + string template) + { + MapRoute(routeBuilder, name, template, defaults: null); + return routeBuilder; + } + + /// + /// Adds a route to the with the specified name, template, and default values. + /// + /// The to add the route to. + /// The name of the route. + /// The URL pattern of the route. + /// + /// An object that contains default values for route parameters. The object's properties represent the names + /// and values of the default values. + /// + /// A reference to this instance after the operation has completed. + public static IRouteBuilder MapRoute( + this IRouteBuilder routeBuilder, + string name, + string template, + object defaults) + { + return MapRoute(routeBuilder, name, template, defaults, constraints: null); + } + + /// + /// Adds a route to the with the specified name, template, default values, and + /// constraints. + /// + /// The to add the route to. + /// The name of the route. + /// The URL pattern of the route. + /// + /// An object that contains default values for route parameters. The object's properties represent the names + /// and values of the default values. + /// + /// + /// An object that contains constraints for the route. The object's properties represent the names and values + /// of the constraints. + /// + /// A reference to this instance after the operation has completed. + public static IRouteBuilder MapRoute( + this IRouteBuilder routeBuilder, + string name, + string template, + object defaults, + object constraints) + { + return MapRoute(routeBuilder, name, template, defaults, constraints, dataTokens: null); + } + + /// + /// Adds a route to the with the specified name, template, default values, and + /// data tokens. + /// + /// The to add the route to. + /// The name of the route. + /// The URL pattern of the route. + /// + /// An object that contains default values for route parameters. The object's properties represent the names + /// and values of the default values. + /// + /// + /// An object that contains constraints for the route. The object's properties represent the names and values + /// of the constraints. + /// + /// + /// An object that contains data tokens for the route. The object's properties represent the names and values + /// of the data tokens. + /// + /// A reference to this instance after the operation has completed. + public static IRouteBuilder MapRoute( + this IRouteBuilder routeBuilder, + string name, + string template, + object defaults, + object constraints, + object dataTokens) + { + if (routeBuilder.DefaultHandler == null) + { + throw new RouteCreationException(Resources.FormatDefaultHandler_MustBeSet(nameof(IRouteBuilder))); + } + + var inlineConstraintResolver = routeBuilder + .ServiceProvider + .GetRequiredService(); + + routeBuilder.Routes.Add(new Route( + routeBuilder.DefaultHandler, + name, + template, + new RouteValueDictionary(defaults), + new RouteValueDictionary(constraints), + new RouteValueDictionary(dataTokens), + inlineConstraintResolver)); + + return routeBuilder; + } + } +} diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Microsoft.AspNetCore.Routing.csproj b/src/Routing/src/Microsoft.AspNetCore.Routing/Microsoft.AspNetCore.Routing.csproj new file mode 100644 index 0000000000..f1e3d36a55 --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Microsoft.AspNetCore.Routing.csproj @@ -0,0 +1,29 @@ + + + ASP.NET Core middleware for routing requests to application logic and for generating links. +Commonly used types: +Microsoft.AspNetCore.Routing.Route +Microsoft.AspNetCore.Routing.RouteCollection + netstandard2.0 + $(NoWarn);CS1591 + true + aspnetcore;routing + + + + + + + + + + + + + + + + + + + diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Properties/AssemblyInfo.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..d8b03e1556 --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Properties/AssemblyInfo.cs @@ -0,0 +1,6 @@ +// 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.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Routing.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Properties/Resources.Designer.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Properties/Resources.Designer.cs new file mode 100644 index 0000000000..6d2bb0a7c9 --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Properties/Resources.Designer.cs @@ -0,0 +1,422 @@ +// +namespace Microsoft.AspNetCore.Routing +{ + using System.Globalization; + using System.Reflection; + using System.Resources; + + internal static class Resources + { + private static readonly ResourceManager _resourceManager + = new ResourceManager("Microsoft.AspNetCore.Routing.Resources", typeof(Resources).GetTypeInfo().Assembly); + + /// + /// Value must be greater than or equal to {0}. + /// + internal static string ArgumentMustBeGreaterThanOrEqualTo + { + get => GetString("ArgumentMustBeGreaterThanOrEqualTo"); + } + + /// + /// Value must be greater than or equal to {0}. + /// + internal static string FormatArgumentMustBeGreaterThanOrEqualTo(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("ArgumentMustBeGreaterThanOrEqualTo"), p0); + + /// + /// The value for argument '{0}' should be less than or equal to the value for the argument '{1}'. + /// + internal static string RangeConstraint_MinShouldBeLessThanOrEqualToMax + { + get => GetString("RangeConstraint_MinShouldBeLessThanOrEqualToMax"); + } + + /// + /// The value for argument '{0}' should be less than or equal to the value for the argument '{1}'. + /// + internal static string FormatRangeConstraint_MinShouldBeLessThanOrEqualToMax(object p0, object p1) + => string.Format(CultureInfo.CurrentCulture, GetString("RangeConstraint_MinShouldBeLessThanOrEqualToMax"), p0, p1); + + /// + /// The '{0}' property of '{1}' must not be null. + /// + internal static string PropertyOfTypeCannotBeNull + { + get => GetString("PropertyOfTypeCannotBeNull"); + } + + /// + /// The '{0}' property of '{1}' must not be null. + /// + internal static string FormatPropertyOfTypeCannotBeNull(object p0, object p1) + => string.Format(CultureInfo.CurrentCulture, GetString("PropertyOfTypeCannotBeNull"), p0, p1); + + /// + /// The supplied route name '{0}' is ambiguous and matched more than one route. + /// + internal static string NamedRoutes_AmbiguousRoutesFound + { + get => GetString("NamedRoutes_AmbiguousRoutesFound"); + } + + /// + /// The supplied route name '{0}' is ambiguous and matched more than one route. + /// + internal static string FormatNamedRoutes_AmbiguousRoutesFound(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("NamedRoutes_AmbiguousRoutesFound"), p0); + + /// + /// A default handler must be set on the {0}. + /// + internal static string DefaultHandler_MustBeSet + { + get => GetString("DefaultHandler_MustBeSet"); + } + + /// + /// A default handler must be set on the {0}. + /// + internal static string FormatDefaultHandler_MustBeSet(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("DefaultHandler_MustBeSet"), p0); + + /// + /// The constructor to use for activating the constraint type '{0}' is ambiguous. Multiple constructors were found with the following number of parameters: {1}. + /// + internal static string DefaultInlineConstraintResolver_AmbiguousCtors + { + get => GetString("DefaultInlineConstraintResolver_AmbiguousCtors"); + } + + /// + /// The constructor to use for activating the constraint type '{0}' is ambiguous. Multiple constructors were found with the following number of parameters: {1}. + /// + internal static string FormatDefaultInlineConstraintResolver_AmbiguousCtors(object p0, object p1) + => string.Format(CultureInfo.CurrentCulture, GetString("DefaultInlineConstraintResolver_AmbiguousCtors"), p0, p1); + + /// + /// Could not find a constructor for constraint type '{0}' with the following number of parameters: {1}. + /// + internal static string DefaultInlineConstraintResolver_CouldNotFindCtor + { + get => GetString("DefaultInlineConstraintResolver_CouldNotFindCtor"); + } + + /// + /// Could not find a constructor for constraint type '{0}' with the following number of parameters: {1}. + /// + internal static string FormatDefaultInlineConstraintResolver_CouldNotFindCtor(object p0, object p1) + => string.Format(CultureInfo.CurrentCulture, GetString("DefaultInlineConstraintResolver_CouldNotFindCtor"), p0, p1); + + /// + /// The constraint type '{0}' which is mapped to constraint key '{1}' must implement the '{2}' interface. + /// + internal static string DefaultInlineConstraintResolver_TypeNotConstraint + { + get => GetString("DefaultInlineConstraintResolver_TypeNotConstraint"); + } + + /// + /// The constraint type '{0}' which is mapped to constraint key '{1}' must implement the '{2}' interface. + /// + internal static string FormatDefaultInlineConstraintResolver_TypeNotConstraint(object p0, object p1, object p2) + => string.Format(CultureInfo.CurrentCulture, GetString("DefaultInlineConstraintResolver_TypeNotConstraint"), p0, p1, p2); + + /// + /// A path segment that contains more than one section, such as a literal section or a parameter, cannot contain a catch-all parameter. + /// + internal static string TemplateRoute_CannotHaveCatchAllInMultiSegment + { + get => GetString("TemplateRoute_CannotHaveCatchAllInMultiSegment"); + } + + /// + /// A path segment that contains more than one section, such as a literal section or a parameter, cannot contain a catch-all parameter. + /// + internal static string FormatTemplateRoute_CannotHaveCatchAllInMultiSegment() + => GetString("TemplateRoute_CannotHaveCatchAllInMultiSegment"); + + /// + /// The route parameter '{0}' has both an inline default value and an explicit default value specified. A route parameter cannot contain an inline default value when a default value is specified explicitly. Consider removing one of them. + /// + internal static string TemplateRoute_CannotHaveDefaultValueSpecifiedInlineAndExplicitly + { + get => GetString("TemplateRoute_CannotHaveDefaultValueSpecifiedInlineAndExplicitly"); + } + + /// + /// The route parameter '{0}' has both an inline default value and an explicit default value specified. A route parameter cannot contain an inline default value when a default value is specified explicitly. Consider removing one of them. + /// + internal static string FormatTemplateRoute_CannotHaveDefaultValueSpecifiedInlineAndExplicitly(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("TemplateRoute_CannotHaveDefaultValueSpecifiedInlineAndExplicitly"), p0); + + /// + /// A path segment cannot contain two consecutive parameters. They must be separated by a '/' or by a literal string. + /// + internal static string TemplateRoute_CannotHaveConsecutiveParameters + { + get => GetString("TemplateRoute_CannotHaveConsecutiveParameters"); + } + + /// + /// A path segment cannot contain two consecutive parameters. They must be separated by a '/' or by a literal string. + /// + internal static string FormatTemplateRoute_CannotHaveConsecutiveParameters() + => GetString("TemplateRoute_CannotHaveConsecutiveParameters"); + + /// + /// The route template separator character '/' cannot appear consecutively. It must be separated by either a parameter or a literal value. + /// + internal static string TemplateRoute_CannotHaveConsecutiveSeparators + { + get => GetString("TemplateRoute_CannotHaveConsecutiveSeparators"); + } + + /// + /// The route template separator character '/' cannot appear consecutively. It must be separated by either a parameter or a literal value. + /// + internal static string FormatTemplateRoute_CannotHaveConsecutiveSeparators() + => GetString("TemplateRoute_CannotHaveConsecutiveSeparators"); + + /// + /// A catch-all parameter cannot be marked optional. + /// + internal static string TemplateRoute_CatchAllCannotBeOptional + { + get => GetString("TemplateRoute_CatchAllCannotBeOptional"); + } + + /// + /// A catch-all parameter cannot be marked optional. + /// + internal static string FormatTemplateRoute_CatchAllCannotBeOptional() + => GetString("TemplateRoute_CatchAllCannotBeOptional"); + + /// + /// An optional parameter cannot have default value. + /// + internal static string TemplateRoute_OptionalCannotHaveDefaultValue + { + get => GetString("TemplateRoute_OptionalCannotHaveDefaultValue"); + } + + /// + /// An optional parameter cannot have default value. + /// + internal static string FormatTemplateRoute_OptionalCannotHaveDefaultValue() + => GetString("TemplateRoute_OptionalCannotHaveDefaultValue"); + + /// + /// A catch-all parameter can only appear as the last segment of the route template. + /// + internal static string TemplateRoute_CatchAllMustBeLast + { + get => GetString("TemplateRoute_CatchAllMustBeLast"); + } + + /// + /// A catch-all parameter can only appear as the last segment of the route template. + /// + internal static string FormatTemplateRoute_CatchAllMustBeLast() + => GetString("TemplateRoute_CatchAllMustBeLast"); + + /// + /// The literal section '{0}' is invalid. Literal sections cannot contain the '?' character. + /// + internal static string TemplateRoute_InvalidLiteral + { + get => GetString("TemplateRoute_InvalidLiteral"); + } + + /// + /// The literal section '{0}' is invalid. Literal sections cannot contain the '?' character. + /// + internal static string FormatTemplateRoute_InvalidLiteral(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("TemplateRoute_InvalidLiteral"), p0); + + /// + /// The route parameter name '{0}' is invalid. Route parameter names must be non-empty and cannot contain these characters: '{{', '}}', '/'. The '?' character marks a parameter as optional, and can occur only at the end of the parameter. The '*' character marks a parameter as catch-all, and can occur only at the start of the parameter. + /// + internal static string TemplateRoute_InvalidParameterName + { + get => GetString("TemplateRoute_InvalidParameterName"); + } + + /// + /// The route parameter name '{0}' is invalid. Route parameter names must be non-empty and cannot contain these characters: '{{', '}}', '/'. The '?' character marks a parameter as optional, and can occur only at the end of the parameter. The '*' character marks a parameter as catch-all, and can occur only at the start of the parameter. + /// + internal static string FormatTemplateRoute_InvalidParameterName(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("TemplateRoute_InvalidParameterName"), p0); + + /// + /// The route template cannot start with a '~' character unless followed by a '/'. + /// + internal static string TemplateRoute_InvalidRouteTemplate + { + get => GetString("TemplateRoute_InvalidRouteTemplate"); + } + + /// + /// The route template cannot start with a '~' character unless followed by a '/'. + /// + internal static string FormatTemplateRoute_InvalidRouteTemplate() + => GetString("TemplateRoute_InvalidRouteTemplate"); + + /// + /// There is an incomplete parameter in the route template. Check that each '{' character has a matching '}' character. + /// + internal static string TemplateRoute_MismatchedParameter + { + get => GetString("TemplateRoute_MismatchedParameter"); + } + + /// + /// There is an incomplete parameter in the route template. Check that each '{' character has a matching '}' character. + /// + internal static string FormatTemplateRoute_MismatchedParameter() + => GetString("TemplateRoute_MismatchedParameter"); + + /// + /// The route parameter name '{0}' appears more than one time in the route template. + /// + internal static string TemplateRoute_RepeatedParameter + { + get => GetString("TemplateRoute_RepeatedParameter"); + } + + /// + /// The route parameter name '{0}' appears more than one time in the route template. + /// + internal static string FormatTemplateRoute_RepeatedParameter(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("TemplateRoute_RepeatedParameter"), p0); + + /// + /// The constraint entry '{0}' - '{1}' on the route '{2}' must have a string value or be of a type which implements '{3}'. + /// + internal static string RouteConstraintBuilder_ValidationMustBeStringOrCustomConstraint + { + get => GetString("RouteConstraintBuilder_ValidationMustBeStringOrCustomConstraint"); + } + + /// + /// The constraint entry '{0}' - '{1}' on the route '{2}' must have a string value or be of a type which implements '{3}'. + /// + internal static string FormatRouteConstraintBuilder_ValidationMustBeStringOrCustomConstraint(object p0, object p1, object p2, object p3) + => string.Format(CultureInfo.CurrentCulture, GetString("RouteConstraintBuilder_ValidationMustBeStringOrCustomConstraint"), p0, p1, p2, p3); + + /// + /// The constraint entry '{0}' - '{1}' on the route '{2}' could not be resolved by the constraint resolver of type '{3}'. + /// + internal static string RouteConstraintBuilder_CouldNotResolveConstraint + { + get => GetString("RouteConstraintBuilder_CouldNotResolveConstraint"); + } + + /// + /// The constraint entry '{0}' - '{1}' on the route '{2}' could not be resolved by the constraint resolver of type '{3}'. + /// + internal static string FormatRouteConstraintBuilder_CouldNotResolveConstraint(object p0, object p1, object p2, object p3) + => string.Format(CultureInfo.CurrentCulture, GetString("RouteConstraintBuilder_CouldNotResolveConstraint"), p0, p1, p2, p3); + + /// + /// In a route parameter, '{' and '}' must be escaped with '{{' and '}}'. + /// + internal static string TemplateRoute_UnescapedBrace + { + get => GetString("TemplateRoute_UnescapedBrace"); + } + + /// + /// In a route parameter, '{' and '}' must be escaped with '{{' and '}}'. + /// + internal static string FormatTemplateRoute_UnescapedBrace() + => GetString("TemplateRoute_UnescapedBrace"); + + /// + /// In the segment '{0}', the optional parameter '{1}' is preceded by an invalid segment '{2}'. Only a period (.) can precede an optional parameter. + /// + internal static string TemplateRoute_OptionalParameterCanbBePrecededByPeriod + { + get => GetString("TemplateRoute_OptionalParameterCanbBePrecededByPeriod"); + } + + /// + /// In the segment '{0}', the optional parameter '{1}' is preceded by an invalid segment '{2}'. Only a period (.) can precede an optional parameter. + /// + internal static string FormatTemplateRoute_OptionalParameterCanbBePrecededByPeriod(object p0, object p1, object p2) + => string.Format(CultureInfo.CurrentCulture, GetString("TemplateRoute_OptionalParameterCanbBePrecededByPeriod"), p0, p1, p2); + + /// + /// An optional parameter must be at the end of the segment. In the segment '{0}', optional parameter '{1}' is followed by '{2}'. + /// + internal static string TemplateRoute_OptionalParameterHasTobeTheLast + { + get => GetString("TemplateRoute_OptionalParameterHasTobeTheLast"); + } + + /// + /// An optional parameter must be at the end of the segment. In the segment '{0}', optional parameter '{1}' is followed by '{2}'. + /// + internal static string FormatTemplateRoute_OptionalParameterHasTobeTheLast(object p0, object p1, object p2) + => string.Format(CultureInfo.CurrentCulture, GetString("TemplateRoute_OptionalParameterHasTobeTheLast"), p0, p1, p2); + + /// + /// Two or more routes named '{0}' have different templates. + /// + internal static string AttributeRoute_DifferentLinkGenerationEntries_SameName + { + get => GetString("AttributeRoute_DifferentLinkGenerationEntries_SameName"); + } + + /// + /// Two or more routes named '{0}' have different templates. + /// + internal static string FormatAttributeRoute_DifferentLinkGenerationEntries_SameName(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("AttributeRoute_DifferentLinkGenerationEntries_SameName"), p0); + + /// + /// Unable to find the required services. Please add all the required services by calling '{0}.{1}' inside the call to '{2}' in the application startup code. + /// + internal static string UnableToFindServices + { + get => GetString("UnableToFindServices"); + } + + /// + /// Unable to find the required services. Please add all the required services by calling '{0}.{1}' inside the call to '{2}' in the application startup code. + /// + internal static string FormatUnableToFindServices(object p0, object p1, object p2) + => string.Format(CultureInfo.CurrentCulture, GetString("UnableToFindServices"), p0, p1, p2); + + /// + /// An error occurred while creating the route with name '{0}' and template '{1}'. + /// + internal static string TemplateRoute_Exception + { + get => GetString("TemplateRoute_Exception"); + } + + /// + /// An error occurred while creating the route with name '{0}' and template '{1}'. + /// + internal static string FormatTemplateRoute_Exception(object p0, object p1) + => string.Format(CultureInfo.CurrentCulture, GetString("TemplateRoute_Exception"), p0, p1); + + private static string GetString(string name, params string[] formatterNames) + { + var value = _resourceManager.GetString(name); + + System.Diagnostics.Debug.Assert(value != null); + + if (formatterNames != null) + { + for (var i = 0; i < formatterNames.Length; i++) + { + value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}"); + } + } + + return value; + } + } +} diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/RequestDelegateRouteBuilderExtensions.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/RequestDelegateRouteBuilderExtensions.cs new file mode 100644 index 0000000000..f376683c4d --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/RequestDelegateRouteBuilderExtensions.cs @@ -0,0 +1,295 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing.Constraints; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Routing +{ + public static class RequestDelegateRouteBuilderExtensions + { + /// + /// Adds a route to the for the given , and + /// . + /// + /// The . + /// The route template. + /// The route handler. + /// A reference to the after this operation has completed. + public static IRouteBuilder MapRoute(this IRouteBuilder builder, string template, RequestDelegate handler) + { + var route = new Route( + new RouteHandler(handler), + template, + defaults: null, + constraints: null, + dataTokens: null, + inlineConstraintResolver: GetConstraintResolver(builder)); + + builder.Routes.Add(route); + return builder; + } + + /// + /// Adds a route to the for the given , and + /// . + /// + /// The . + /// The route template. + /// The action to apply to the . + /// A reference to the after this operation has completed. + public static IRouteBuilder MapMiddlewareRoute(this IRouteBuilder builder, string template, Action action) + { + var nested = builder.ApplicationBuilder.New(); + action(nested); + return builder.MapRoute(template, nested.Build()); + } + + /// + /// Adds a route to the that only matches HTTP DELETE requests for the given + /// , and . + /// + /// The . + /// The route template. + /// The route handler. + /// A reference to the after this operation has completed. + public static IRouteBuilder MapDelete(this IRouteBuilder builder, string template, RequestDelegate handler) + { + return builder.MapVerb("DELETE", template, handler); + } + + /// + /// Adds a route to the that only matches HTTP DELETE requests for the given + /// , and . + /// + /// The . + /// The route template. + /// The action to apply to the . + /// A reference to the after this operation has completed. + public static IRouteBuilder MapMiddlewareDelete(this IRouteBuilder builder, string template, Action action) + { + return builder.MapMiddlewareVerb("DELETE", template, action); + } + + /// + /// Adds a route to the that only matches HTTP DELETE requests for the given + /// , and . + /// + /// The . + /// The route template. + /// The route handler. + /// A reference to the after this operation has completed. + public static IRouteBuilder MapDelete( + this IRouteBuilder builder, + string template, + Func handler) + { + return builder.MapVerb("DELETE", template, handler); + } + + /// + /// Adds a route to the that only matches HTTP GET requests for the given + /// , and . + /// + /// The . + /// The route template. + /// The route handler. + /// A reference to the after this operation has completed. + public static IRouteBuilder MapGet(this IRouteBuilder builder, string template, RequestDelegate handler) + { + return builder.MapVerb("GET", template, handler); + } + + /// + /// Adds a route to the that only matches HTTP GET requests for the given + /// , and . + /// + /// The . + /// The route template. + /// The action to apply to the . + /// A reference to the after this operation has completed. + public static IRouteBuilder MapMiddlewareGet(this IRouteBuilder builder, string template, Action action) + { + return builder.MapMiddlewareVerb("GET", template, action); + } + + /// + /// Adds a route to the that only matches HTTP GET requests for the given + /// , and . + /// + /// The . + /// The route template. + /// The route handler. + /// A reference to the after this operation has completed. + public static IRouteBuilder MapGet( + this IRouteBuilder builder, + string template, + Func handler) + { + return builder.MapVerb("GET", template, handler); + } + + /// + /// Adds a route to the that only matches HTTP POST requests for the given + /// , and . + /// + /// The . + /// The route template. + /// The route handler. + /// A reference to the after this operation has completed. + public static IRouteBuilder MapPost(this IRouteBuilder builder, string template, RequestDelegate handler) + { + return builder.MapVerb("POST", template, handler); + } + + /// + /// Adds a route to the that only matches HTTP POST requests for the given + /// , and . + /// + /// The . + /// The route template. + /// The action to apply to the . + /// A reference to the after this operation has completed. + public static IRouteBuilder MapMiddlewarePost(this IRouteBuilder builder, string template, Action action) + { + return builder.MapMiddlewareVerb("POST", template, action); + } + + /// + /// Adds a route to the that only matches HTTP POST requests for the given + /// , and . + /// + /// The . + /// The route template. + /// The route handler. + /// A reference to the after this operation has completed. + public static IRouteBuilder MapPost( + this IRouteBuilder builder, + string template, + Func handler) + { + return builder.MapVerb("POST", template, handler); + } + + /// + /// Adds a route to the that only matches HTTP PUT requests for the given + /// , and . + /// + /// The . + /// The route template. + /// The route handler. + /// A reference to the after this operation has completed. + public static IRouteBuilder MapPut(this IRouteBuilder builder, string template, RequestDelegate handler) + { + return builder.MapVerb("PUT", template, handler); + } + + /// + /// Adds a route to the that only matches HTTP PUT requests for the given + /// , and . + /// + /// The . + /// The route template. + /// The action to apply to the . + /// A reference to the after this operation has completed. + public static IRouteBuilder MapMiddlewarePut(this IRouteBuilder builder, string template, Action action) + { + return builder.MapMiddlewareVerb("PUT", template, action); + } + + /// + /// Adds a route to the that only matches HTTP PUT requests for the given + /// , and . + /// + /// The . + /// The route template. + /// The route handler. + /// A reference to the after this operation has completed. + public static IRouteBuilder MapPut( + this IRouteBuilder builder, + string template, + Func handler) + { + return builder.MapVerb("PUT", template, handler); + } + + /// + /// Adds a route to the that only matches HTTP requests for the given + /// , , and . + /// + /// The . + /// The HTTP verb allowed by the route. + /// The route template. + /// The route handler. + /// A reference to the after this operation has completed. + public static IRouteBuilder MapVerb( + this IRouteBuilder builder, + string verb, + string template, + Func handler) + { + RequestDelegate requestDelegate = (httpContext) => + { + return handler(httpContext.Request, httpContext.Response, httpContext.GetRouteData()); + }; + + return builder.MapVerb(verb, template, requestDelegate); + } + + /// + /// Adds a route to the that only matches HTTP requests for the given + /// , , and . + /// + /// The . + /// The HTTP verb allowed by the route. + /// The route template. + /// The route handler. + /// A reference to the after this operation has completed. + public static IRouteBuilder MapVerb( + this IRouteBuilder builder, + string verb, + string template, + RequestDelegate handler) + { + var route = new Route( + new RouteHandler(handler), + template, + defaults: null, + constraints: new RouteValueDictionary(new { httpMethod = new HttpMethodRouteConstraint(verb) }), + dataTokens: null, + inlineConstraintResolver: GetConstraintResolver(builder)); + + builder.Routes.Add(route); + return builder; + } + + /// + /// Adds a route to the that only matches HTTP requests for the given + /// , , and . + /// + /// The . + /// The HTTP verb allowed by the route. + /// The route template. + /// The action to apply to the . + /// A reference to the after this operation has completed. + public static IRouteBuilder MapMiddlewareVerb( + this IRouteBuilder builder, + string verb, + string template, + Action action) + { + var nested = builder.ApplicationBuilder.New(); + action(nested); + return builder.MapVerb(verb, template, nested.Build()); + } + + private static IInlineConstraintResolver GetConstraintResolver(IRouteBuilder builder) + { + return builder.ServiceProvider.GetRequiredService(); + } + } +} diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Resources.resx b/src/Routing/src/Microsoft.AspNetCore.Routing/Resources.resx new file mode 100644 index 0000000000..d883906f7c --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Resources.resx @@ -0,0 +1,204 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Value must be greater than or equal to {0}. + + + The value for argument '{0}' should be less than or equal to the value for the argument '{1}'. + + + The '{0}' property of '{1}' must not be null. + + + The supplied route name '{0}' is ambiguous and matched more than one route. + + + A default handler must be set on the {0}. + + + The constructor to use for activating the constraint type '{0}' is ambiguous. Multiple constructors were found with the following number of parameters: {1}. + + + Could not find a constructor for constraint type '{0}' with the following number of parameters: {1}. + + + The constraint type '{0}' which is mapped to constraint key '{1}' must implement the '{2}' interface. + + + A path segment that contains more than one section, such as a literal section or a parameter, cannot contain a catch-all parameter. + + + The route parameter '{0}' has both an inline default value and an explicit default value specified. A route parameter cannot contain an inline default value when a default value is specified explicitly. Consider removing one of them. + + + A path segment cannot contain two consecutive parameters. They must be separated by a '/' or by a literal string. + + + The route template separator character '/' cannot appear consecutively. It must be separated by either a parameter or a literal value. + + + A catch-all parameter cannot be marked optional. + + + An optional parameter cannot have default value. + + + A catch-all parameter can only appear as the last segment of the route template. + + + The literal section '{0}' is invalid. Literal sections cannot contain the '?' character. + + + The route parameter name '{0}' is invalid. Route parameter names must be non-empty and cannot contain these characters: '{{', '}}', '/'. The '?' character marks a parameter as optional, and can occur only at the end of the parameter. The '*' character marks a parameter as catch-all, and can occur only at the start of the parameter. + + + The route template cannot start with a '~' character unless followed by a '/'. + + + There is an incomplete parameter in the route template. Check that each '{' character has a matching '}' character. + + + The route parameter name '{0}' appears more than one time in the route template. + + + The constraint entry '{0}' - '{1}' on the route '{2}' must have a string value or be of a type which implements '{3}'. + + + The constraint entry '{0}' - '{1}' on the route '{2}' could not be resolved by the constraint resolver of type '{3}'. + + + In a route parameter, '{' and '}' must be escaped with '{{' and '}}'. + + + In the segment '{0}', the optional parameter '{1}' is preceded by an invalid segment '{2}'. Only a period (.) can precede an optional parameter. + + + An optional parameter must be at the end of the segment. In the segment '{0}', optional parameter '{1}' is followed by '{2}'. + + + Two or more routes named '{0}' have different templates. + + + Unable to find the required services. Please add all the required services by calling '{0}.{1}' inside the call to '{2}' in the application startup code. + + + An error occurred while creating the route with name '{0}' and template '{1}'. + + \ No newline at end of file diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Route.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Route.cs new file mode 100644 index 0000000000..3d38f922b3 --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Route.cs @@ -0,0 +1,76 @@ +// 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; + +namespace Microsoft.AspNetCore.Routing +{ + public class Route : RouteBase + { + private readonly IRouter _target; + + public Route( + IRouter target, + string routeTemplate, + IInlineConstraintResolver inlineConstraintResolver) + : this( + target, + routeTemplate, + defaults: null, + constraints: null, + dataTokens: null, + inlineConstraintResolver: inlineConstraintResolver) + { + } + + public Route( + IRouter target, + string routeTemplate, + RouteValueDictionary defaults, + IDictionary constraints, + RouteValueDictionary dataTokens, + IInlineConstraintResolver inlineConstraintResolver) + : this(target, null, routeTemplate, defaults, constraints, dataTokens, inlineConstraintResolver) + { + } + + public Route( + IRouter target, + string routeName, + string routeTemplate, + RouteValueDictionary defaults, + IDictionary constraints, + RouteValueDictionary dataTokens, + IInlineConstraintResolver inlineConstraintResolver) + : base( + routeTemplate, + routeName, + inlineConstraintResolver, + defaults, + constraints, + dataTokens) + { + if (target == null) + { + throw new ArgumentNullException(nameof(target)); + } + + _target = target; + } + + public string RouteTemplate => ParsedTemplate.TemplateText; + + protected override Task OnRouteMatched(RouteContext context) + { + context.RouteData.Routers.Add(_target); + return _target.RouteAsync(context); + } + + protected override VirtualPathData OnVirtualPathGenerated(VirtualPathContext context) + { + return _target.GetVirtualPath(context); + } + } +} diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/RouteBase.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/RouteBase.cs new file mode 100644 index 0000000000..6d1eb7bd27 --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/RouteBase.cs @@ -0,0 +1,272 @@ +// 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.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing.Internal; +using Microsoft.AspNetCore.Routing.Logging; +using Microsoft.AspNetCore.Routing.Template; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.ObjectPool; + +namespace Microsoft.AspNetCore.Routing +{ + public abstract class RouteBase : IRouter, INamedRouter + { + private TemplateMatcher _matcher; + private TemplateBinder _binder; + private ILogger _logger; + private ILogger _constraintLogger; + + public RouteBase( + string template, + string name, + IInlineConstraintResolver constraintResolver, + RouteValueDictionary defaults, + IDictionary constraints, + RouteValueDictionary dataTokens) + { + if (constraintResolver == null) + { + throw new ArgumentNullException(nameof(constraintResolver)); + } + + template = template ?? string.Empty; + Name = name; + ConstraintResolver = constraintResolver; + DataTokens = dataTokens ?? new RouteValueDictionary(); + + try + { + // Data we parse from the template will be used to fill in the rest of the constraints or + // defaults. The parser will throw for invalid routes. + ParsedTemplate = TemplateParser.Parse(template); + + Constraints = GetConstraints(constraintResolver, ParsedTemplate, constraints); + Defaults = GetDefaults(ParsedTemplate, defaults); + } + catch (Exception exception) + { + throw new RouteCreationException(Resources.FormatTemplateRoute_Exception(name, template), exception); + } + } + + public virtual IDictionary Constraints { get; protected set; } + + protected virtual IInlineConstraintResolver ConstraintResolver { get; set; } + + public virtual RouteValueDictionary DataTokens { get; protected set; } + + public virtual RouteValueDictionary Defaults { get; protected set; } + + public virtual string Name { get; protected set; } + + public virtual RouteTemplate ParsedTemplate { get; protected set; } + + protected abstract Task OnRouteMatched(RouteContext context); + + protected abstract VirtualPathData OnVirtualPathGenerated(VirtualPathContext context); + + /// + public virtual Task RouteAsync(RouteContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + EnsureMatcher(); + EnsureLoggers(context.HttpContext); + + var requestPath = context.HttpContext.Request.Path; + + if (!_matcher.TryMatch(requestPath, context.RouteData.Values)) + { + // If we got back a null value set, that means the URI did not match + return Task.CompletedTask; + } + + // Perf: Avoid accessing dictionaries if you don't need to write to them, these dictionaries are all + // created lazily. + if (DataTokens.Count > 0) + { + MergeValues(context.RouteData.DataTokens, DataTokens); + } + + if (!RouteConstraintMatcher.Match( + Constraints, + context.RouteData.Values, + context.HttpContext, + this, + RouteDirection.IncomingRequest, + _constraintLogger)) + { + return Task.CompletedTask; + } + _logger.MatchedRoute(Name, ParsedTemplate.TemplateText); + + return OnRouteMatched(context); + } + + /// + public virtual VirtualPathData GetVirtualPath(VirtualPathContext context) + { + EnsureBinder(context.HttpContext); + EnsureLoggers(context.HttpContext); + + var values = _binder.GetValues(context.AmbientValues, context.Values); + if (values == null) + { + // We're missing one of the required values for this route. + return null; + } + + if (!RouteConstraintMatcher.Match( + Constraints, + values.CombinedValues, + context.HttpContext, + this, + RouteDirection.UrlGeneration, + _constraintLogger)) + { + return null; + } + + context.Values = values.CombinedValues; + + var pathData = OnVirtualPathGenerated(context); + if (pathData != null) + { + // If the target generates a value then that can short circuit. + return pathData; + } + + // If we can produce a value go ahead and do it, the caller can check context.IsBound + // to see if the values were validated. + + // When we still cannot produce a value, this should return null. + var virtualPath = _binder.BindValues(values.AcceptedValues); + if (virtualPath == null) + { + return null; + } + + pathData = new VirtualPathData(this, virtualPath); + if (DataTokens != null) + { + foreach (var dataToken in DataTokens) + { + pathData.DataTokens.Add(dataToken.Key, dataToken.Value); + } + } + + return pathData; + } + + protected static IDictionary GetConstraints( + IInlineConstraintResolver inlineConstraintResolver, + RouteTemplate parsedTemplate, + IDictionary constraints) + { + var constraintBuilder = new RouteConstraintBuilder(inlineConstraintResolver, parsedTemplate.TemplateText); + + if (constraints != null) + { + foreach (var kvp in constraints) + { + constraintBuilder.AddConstraint(kvp.Key, kvp.Value); + } + } + + foreach (var parameter in parsedTemplate.Parameters) + { + if (parameter.IsOptional) + { + constraintBuilder.SetOptional(parameter.Name); + } + + foreach (var inlineConstraint in parameter.InlineConstraints) + { + constraintBuilder.AddResolvedConstraint(parameter.Name, inlineConstraint.Constraint); + } + } + + return constraintBuilder.Build(); + } + + protected static RouteValueDictionary GetDefaults( + RouteTemplate parsedTemplate, + RouteValueDictionary defaults) + { + var result = defaults == null ? new RouteValueDictionary() : new RouteValueDictionary(defaults); + + foreach (var parameter in parsedTemplate.Parameters) + { + if (parameter.DefaultValue != null) + { + if (result.ContainsKey(parameter.Name)) + { + throw new InvalidOperationException( + Resources.FormatTemplateRoute_CannotHaveDefaultValueSpecifiedInlineAndExplicitly( + parameter.Name)); + } + else + { + result.Add(parameter.Name, parameter.DefaultValue); + } + } + } + + return result; + } + + private static void MergeValues( + RouteValueDictionary destination, + RouteValueDictionary values) + { + foreach (var kvp in values) + { + // This will replace the original value for the specified key. + // Values from the matched route will take preference over previous + // data in the route context. + destination[kvp.Key] = kvp.Value; + } + } + + private void EnsureBinder(HttpContext context) + { + if (_binder == null) + { + var pool = context.RequestServices.GetRequiredService>(); + _binder = new TemplateBinder(UrlEncoder.Default, pool, ParsedTemplate, Defaults); + } + } + + private void EnsureLoggers(HttpContext context) + { + if (_logger == null) + { + var factory = context.RequestServices.GetRequiredService(); + _logger = factory.CreateLogger(typeof(RouteBase).FullName); + _constraintLogger = factory.CreateLogger(typeof(RouteConstraintMatcher).FullName); + } + } + + private void EnsureMatcher() + { + if (_matcher == null) + { + _matcher = new TemplateMatcher(ParsedTemplate, Defaults); + } + } + + public override string ToString() + { + return ParsedTemplate.TemplateText; + } + } +} diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/RouteBuilder.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/RouteBuilder.cs new file mode 100644 index 0000000000..b492bfb59e --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/RouteBuilder.cs @@ -0,0 +1,61 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Routing.Internal; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Routing +{ + public class RouteBuilder : IRouteBuilder + { + public RouteBuilder(IApplicationBuilder applicationBuilder) + : this(applicationBuilder, defaultHandler: null) + { + } + + public RouteBuilder(IApplicationBuilder applicationBuilder, IRouter defaultHandler) + { + if (applicationBuilder == null) + { + throw new ArgumentNullException(nameof(applicationBuilder)); + } + + if (applicationBuilder.ApplicationServices.GetService(typeof(RoutingMarkerService)) == null) + { + throw new InvalidOperationException(Resources.FormatUnableToFindServices( + nameof(IServiceCollection), + nameof(RoutingServiceCollectionExtensions.AddRouting), + "ConfigureServices(...)")); + } + + ApplicationBuilder = applicationBuilder; + DefaultHandler = defaultHandler; + ServiceProvider = applicationBuilder.ApplicationServices; + + Routes = new List(); + } + + public IApplicationBuilder ApplicationBuilder { get; } + + public IRouter DefaultHandler { get; set; } + + public IServiceProvider ServiceProvider { get; } + + public IList Routes { get; } + + public IRouter Build() + { + var routeCollection = new RouteCollection(); + + foreach (var route in Routes) + { + routeCollection.Add(route); + } + + return routeCollection; + } + } +} \ No newline at end of file diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/RouteCollection.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/RouteCollection.cs new file mode 100644 index 0000000000..a2dae64291 --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/RouteCollection.cs @@ -0,0 +1,181 @@ +// 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.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Routing +{ + public class RouteCollection : IRouteCollection + { + private readonly static char[] UrlQueryDelimiters = new char[] { '?', '#' }; + private readonly List _routes = new List(); + private readonly List _unnamedRoutes = new List(); + private readonly Dictionary _namedRoutes = + new Dictionary(StringComparer.OrdinalIgnoreCase); + + private RouteOptions _options; + + public IRouter this[int index] + { + get { return _routes[index]; } + } + + public int Count + { + get { return _routes.Count; } + } + + public void Add(IRouter router) + { + if (router == null) + { + throw new ArgumentNullException(nameof(router)); + } + + var namedRouter = router as INamedRouter; + if (namedRouter != null) + { + if (!string.IsNullOrEmpty(namedRouter.Name)) + { + _namedRoutes.Add(namedRouter.Name, namedRouter); + } + } + else + { + _unnamedRoutes.Add(router); + } + + _routes.Add(router); + } + + public async virtual Task RouteAsync(RouteContext context) + { + // Perf: We want to avoid allocating a new RouteData for each route we need to process. + // We can do this by snapshotting the state at the beginning and then restoring it + // for each router we execute. + var snapshot = context.RouteData.PushState(null, values: null, dataTokens: null); + + for (var i = 0; i < Count; i++) + { + var route = this[i]; + context.RouteData.Routers.Add(route); + + try + { + await route.RouteAsync(context); + + if (context.Handler != null) + { + break; + } + } + finally + { + if (context.Handler == null) + { + snapshot.Restore(); + } + } + } + } + + public virtual VirtualPathData GetVirtualPath(VirtualPathContext context) + { + EnsureOptions(context.HttpContext); + + if (!string.IsNullOrEmpty(context.RouteName)) + { + VirtualPathData namedRoutePathData = null; + INamedRouter matchedNamedRoute; + if (_namedRoutes.TryGetValue(context.RouteName, out matchedNamedRoute)) + { + namedRoutePathData = matchedNamedRoute.GetVirtualPath(context); + } + + var pathData = GetVirtualPath(context, _unnamedRoutes); + + // If the named route and one of the unnamed routes also matches, then we have an ambiguity. + if (namedRoutePathData != null && pathData != null) + { + var message = Resources.FormatNamedRoutes_AmbiguousRoutesFound(context.RouteName); + throw new InvalidOperationException(message); + } + + return NormalizeVirtualPath(namedRoutePathData ?? pathData); + } + else + { + return NormalizeVirtualPath(GetVirtualPath(context, _routes)); + } + } + + private VirtualPathData GetVirtualPath(VirtualPathContext context, List routes) + { + for (var i = 0; i < routes.Count; i++) + { + var route = routes[i]; + + var pathData = route.GetVirtualPath(context); + if (pathData != null) + { + return pathData; + } + } + + return null; + } + + private VirtualPathData NormalizeVirtualPath(VirtualPathData pathData) + { + if (pathData == null) + { + return pathData; + } + + var url = pathData.VirtualPath; + + if (!string.IsNullOrEmpty(url) && (_options.LowercaseUrls || _options.AppendTrailingSlash)) + { + var indexOfSeparator = url.IndexOfAny(UrlQueryDelimiters); + var urlWithoutQueryString = url; + var queryString = string.Empty; + + if (indexOfSeparator != -1) + { + urlWithoutQueryString = url.Substring(0, indexOfSeparator); + queryString = url.Substring(indexOfSeparator); + } + + if (_options.LowercaseUrls) + { + urlWithoutQueryString = urlWithoutQueryString.ToLowerInvariant(); + } + + if (_options.AppendTrailingSlash && !urlWithoutQueryString.EndsWith("/")) + { + urlWithoutQueryString += "/"; + } + + // queryString will contain the delimiter ? or # as the first character, so it's safe to append. + url = urlWithoutQueryString + queryString; + + return new VirtualPathData(pathData.Router, url, pathData.DataTokens); + } + + return pathData; + } + + private void EnsureOptions(HttpContext context) + { + if (_options == null) + { + _options = context.RequestServices.GetRequiredService>().Value; + } + } + } +} diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/RouteConstraintBuilder.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/RouteConstraintBuilder.cs new file mode 100644 index 0000000000..c230ed0e96 --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/RouteConstraintBuilder.cs @@ -0,0 +1,191 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Routing.Constraints; + +namespace Microsoft.AspNetCore.Routing +{ + /// + /// A builder for produding a mapping of keys to see . + /// + /// + /// allows iterative building a set of route constraints, and will + /// merge multiple entries for the same key. + /// + public class RouteConstraintBuilder + { + private readonly IInlineConstraintResolver _inlineConstraintResolver; + private readonly string _displayName; + + private readonly Dictionary> _constraints; + private readonly HashSet _optionalParameters; + /// + /// Creates a new instance. + /// + /// The . + /// The display name (for use in error messages). + public RouteConstraintBuilder( + IInlineConstraintResolver inlineConstraintResolver, + string displayName) + { + if (inlineConstraintResolver == null) + { + throw new ArgumentNullException(nameof(inlineConstraintResolver)); + } + + if (displayName == null) + { + throw new ArgumentNullException(nameof(displayName)); + } + + _inlineConstraintResolver = inlineConstraintResolver; + _displayName = displayName; + + _constraints = new Dictionary>(StringComparer.OrdinalIgnoreCase); + _optionalParameters = new HashSet(StringComparer.OrdinalIgnoreCase); + } + + /// + /// Builds a mapping of constraints. + /// + /// An of the constraints. + public IDictionary Build() + { + var constraints = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var kvp in _constraints) + { + IRouteConstraint constraint; + if (kvp.Value.Count == 1) + { + constraint = kvp.Value[0]; + } + else + { + constraint = new CompositeRouteConstraint(kvp.Value.ToArray()); + } + + if (_optionalParameters.Contains(kvp.Key)) + { + var optionalConstraint = new OptionalRouteConstraint(constraint); + constraints.Add(kvp.Key, optionalConstraint); + } + else + { + constraints.Add(kvp.Key, constraint); + } + } + + return constraints; + } + + /// + /// Adds a constraint instance for the given key. + /// + /// The key. + /// + /// The constraint instance. Must either be a string or an instance of . + /// + /// + /// If the is a string, it will be converted to a . + /// + /// For example, the string Product[0-9]+ will be converted to the regular expression + /// ^(Product[0-9]+). See for more details. + /// + public void AddConstraint(string key, object value) + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + var constraint = value as IRouteConstraint; + if (constraint == null) + { + var regexPattern = value as string; + if (regexPattern == null) + { + throw new RouteCreationException( + Resources.FormatRouteConstraintBuilder_ValidationMustBeStringOrCustomConstraint( + key, + value, + _displayName, + typeof(IRouteConstraint))); + } + + var constraintsRegEx = "^(" + regexPattern + ")$"; + constraint = new RegexRouteConstraint(constraintsRegEx); + } + + Add(key, constraint); + } + + /// + /// Adds a constraint for the given key, resolved by the . + /// + /// The key. + /// The text to be resolved by . + /// + /// The can create instances + /// based on . See to register + /// custom constraint types. + /// + public void AddResolvedConstraint(string key, string constraintText) + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + if (constraintText == null) + { + throw new ArgumentNullException(nameof(constraintText)); + } + + var constraint = _inlineConstraintResolver.ResolveConstraint(constraintText); + if (constraint == null) + { + throw new InvalidOperationException( + Resources.FormatRouteConstraintBuilder_CouldNotResolveConstraint( + key, + constraintText, + _displayName, + _inlineConstraintResolver.GetType().Name)); + } + + Add(key, constraint); + } + + /// + /// Sets the given key as optional. + /// + /// The key. + public void SetOptional(string key) + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + _optionalParameters.Add(key); + } + + private void Add(string key, IRouteConstraint constraint) + { + List list; + if (!_constraints.TryGetValue(key, out list)) + { + list = new List(); + _constraints.Add(key, list); + } + + list.Add(constraint); + } + } +} diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/RouteConstraintMatcher.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/RouteConstraintMatcher.cs new file mode 100644 index 0000000000..8601d85581 --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/RouteConstraintMatcher.cs @@ -0,0 +1,67 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing.Logging; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Routing +{ + public static class RouteConstraintMatcher + { + public static bool Match( + IDictionary constraints, + RouteValueDictionary routeValues, + HttpContext httpContext, + IRouter route, + RouteDirection routeDirection, + ILogger logger) + { + if (routeValues == null) + { + throw new ArgumentNullException(nameof(routeValues)); + } + + if (httpContext == null) + { + throw new ArgumentNullException(nameof(httpContext)); + } + + if (route == null) + { + throw new ArgumentNullException(nameof(route)); + } + + if (logger == null) + { + throw new ArgumentNullException(nameof(logger)); + } + + if (constraints == null || constraints.Count == 0) + { + return true; + } + + foreach (var kvp in constraints) + { + var constraint = kvp.Value; + if (!constraint.Match(httpContext, route, kvp.Key, routeValues, routeDirection)) + { + if (routeDirection.Equals(RouteDirection.IncomingRequest)) + { + object routeValue; + routeValues.TryGetValue(kvp.Key, out routeValue); + + logger.RouteValueDoesNotMatchConstraint(routeValue, kvp.Key, kvp.Value); + } + + return false; + } + } + + return true; + } + } +} diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/RouteCreationException.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/RouteCreationException.cs new file mode 100644 index 0000000000..0c47e7e412 --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/RouteCreationException.cs @@ -0,0 +1,33 @@ +// 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; + +namespace Microsoft.AspNetCore.Routing +{ + /// + /// The exception that is thrown for invalid routes or constraints. + /// + public class RouteCreationException : Exception + { + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The message that describes the error. + public RouteCreationException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class with a specified error message + /// and a reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception. + public RouteCreationException(string message, Exception innerException) + : base(message, innerException) + { + } + } +} diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/RouteHandler.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/RouteHandler.cs new file mode 100644 index 0000000000..a2fcce601f --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/RouteHandler.cs @@ -0,0 +1,35 @@ +// 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 +{ + public class RouteHandler : IRouteHandler, IRouter + { + private readonly RequestDelegate _requestDelegate; + + public RouteHandler(RequestDelegate requestDelegate) + { + _requestDelegate = requestDelegate; + } + + public RequestDelegate GetRequestHandler(HttpContext httpContext, RouteData routeData) + { + return _requestDelegate; + } + + public VirtualPathData GetVirtualPath(VirtualPathContext context) + { + // Nothing to do. + return null; + } + + public Task RouteAsync(RouteContext context) + { + context.Handler = _requestDelegate; + return Task.CompletedTask; + } + } +} diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/RouteOptions.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/RouteOptions.cs new file mode 100644 index 0000000000..1ae47ed13f --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/RouteOptions.cs @@ -0,0 +1,73 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Routing.Constraints; + +namespace Microsoft.AspNetCore.Routing +{ + public class RouteOptions + { + /// + /// Gets or sets a value indicating whether all generated URLs are lower-case. + /// + public bool LowercaseUrls { get; set; } + + /// + /// Gets or sets a value indicating whether a trailing slash should be appended to the generated URLs. + /// + public bool AppendTrailingSlash { get; set; } + + private IDictionary _constraintTypeMap = GetDefaultConstraintMap(); + + public IDictionary ConstraintMap + { + get + { + return _constraintTypeMap; + } + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(ConstraintMap)); + } + + _constraintTypeMap = value; + } + } + + private static IDictionary GetDefaultConstraintMap() + { + return new Dictionary(StringComparer.OrdinalIgnoreCase) + { + // Type-specific constraints + { "int", typeof(IntRouteConstraint) }, + { "bool", typeof(BoolRouteConstraint) }, + { "datetime", typeof(DateTimeRouteConstraint) }, + { "decimal", typeof(DecimalRouteConstraint) }, + { "double", typeof(DoubleRouteConstraint) }, + { "float", typeof(FloatRouteConstraint) }, + { "guid", typeof(GuidRouteConstraint) }, + { "long", typeof(LongRouteConstraint) }, + + // Length constraints + { "minlength", typeof(MinLengthRouteConstraint) }, + { "maxlength", typeof(MaxLengthRouteConstraint) }, + { "length", typeof(LengthRouteConstraint) }, + + // Min/Max value constraints + { "min", typeof(MinRouteConstraint) }, + { "max", typeof(MaxRouteConstraint) }, + { "range", typeof(RangeRouteConstraint) }, + + // Regex-based constraints + { "alpha", typeof(AlphaRouteConstraint) }, + { "regex", typeof(RegexInlineRouteConstraint) }, + + {"required", typeof(RequiredRouteConstraint) }, + }; + } + } +} diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/RouteValueEqualityComparer.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/RouteValueEqualityComparer.cs new file mode 100644 index 0000000000..6f2a1eab45 --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/RouteValueEqualityComparer.cs @@ -0,0 +1,53 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Globalization; + +namespace Microsoft.AspNetCore.Routing +{ + /// + /// An implementation that compares objects as-if + /// they were route value strings. + /// + /// + /// Values that are are not strings are converted to strings using + /// Convert.ToString(x, CultureInfo.InvariantCulture). null values are converted + /// to the empty string. + /// + /// strings are compared using . + /// + public class RouteValueEqualityComparer : IEqualityComparer + { + /// + public new bool Equals(object x, object y) + { + var stringX = x as string ?? Convert.ToString(x, CultureInfo.InvariantCulture); + var stringY = y as string ?? Convert.ToString(y, CultureInfo.InvariantCulture); + + if (string.IsNullOrEmpty(stringX) && string.IsNullOrEmpty(stringY)) + { + return true; + } + else + { + return string.Equals(stringX, stringY, StringComparison.OrdinalIgnoreCase); + } + } + + /// + public int GetHashCode(object obj) + { + var stringObj = obj as string ?? Convert.ToString(obj, CultureInfo.InvariantCulture); + if (string.IsNullOrEmpty(stringObj)) + { + return StringComparer.OrdinalIgnoreCase.GetHashCode(string.Empty); + } + else + { + return StringComparer.OrdinalIgnoreCase.GetHashCode(stringObj); + } + } + } +} \ No newline at end of file diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/RouterMiddleware.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/RouterMiddleware.cs new file mode 100644 index 0000000000..a8256dc5fa --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/RouterMiddleware.cs @@ -0,0 +1,52 @@ +// 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; +using Microsoft.AspNetCore.Routing.Logging; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Builder +{ + public class RouterMiddleware + { + private readonly ILogger _logger; + private readonly RequestDelegate _next; + private readonly IRouter _router; + + public RouterMiddleware( + RequestDelegate next, + ILoggerFactory loggerFactory, + IRouter router) + { + _next = next; + _router = router; + + _logger = loggerFactory.CreateLogger(); + } + + public async Task Invoke(HttpContext httpContext) + { + var context = new RouteContext(httpContext); + context.RouteData.Routers.Add(_router); + + await _router.RouteAsync(context); + + if (context.Handler == null) + { + _logger.RequestDidNotMatchRoutes(); + await _next.Invoke(httpContext); + } + else + { + httpContext.Features[typeof(IRoutingFeature)] = new RoutingFeature() + { + RouteData = context.RouteData, + }; + + await context.Handler(context.HttpContext); + } + } + } +} diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/RoutingBuilderExtensions.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/RoutingBuilderExtensions.cs new file mode 100644 index 0000000000..cc2e2457e8 --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/RoutingBuilderExtensions.cs @@ -0,0 +1,78 @@ +// 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; +using Microsoft.AspNetCore.Routing.Internal; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Builder +{ + /// + /// Extension methods for adding the middleware to an . + /// + public static class RoutingBuilderExtensions + { + /// + /// Adds a middleware to the specified with the specified . + /// + /// The to add the middleware to. + /// The to use for routing requests. + /// A reference to this instance after the operation has completed. + public static IApplicationBuilder UseRouter(this IApplicationBuilder builder, IRouter router) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (router == null) + { + throw new ArgumentNullException(nameof(router)); + } + + if (builder.ApplicationServices.GetService(typeof(RoutingMarkerService)) == null) + { + throw new InvalidOperationException(Resources.FormatUnableToFindServices( + nameof(IServiceCollection), + nameof(RoutingServiceCollectionExtensions.AddRouting), + "ConfigureServices(...)")); + } + + return builder.UseMiddleware(router); + } + + /// + /// Adds a middleware to the specified + /// with the built from configured . + /// + /// The to add the middleware to. + /// An to configure the provided . + /// A reference to this instance after the operation has completed. + public static IApplicationBuilder UseRouter(this IApplicationBuilder builder, Action action) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (action == null) + { + throw new ArgumentNullException(nameof(action)); + } + + if (builder.ApplicationServices.GetService(typeof(RoutingMarkerService)) == null) + { + throw new InvalidOperationException(Resources.FormatUnableToFindServices( + nameof(IServiceCollection), + nameof(RoutingServiceCollectionExtensions.AddRouting), + "ConfigureServices(...)")); + } + + var routeBuilder = new RouteBuilder(builder); + action(routeBuilder); + + return builder.UseRouter(routeBuilder.Build()); + } + } +} \ No newline at end of file diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/RoutingFeature.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/RoutingFeature.cs new file mode 100644 index 0000000000..30b88ac1b9 --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/RoutingFeature.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 class RoutingFeature : IRoutingFeature + { + public RouteData RouteData { get; set; } + } +} diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Template/InlineConstraint.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Template/InlineConstraint.cs new file mode 100644 index 0000000000..5c50eefb0e --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Template/InlineConstraint.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; + +namespace Microsoft.AspNetCore.Routing.Template +{ + /// + /// The parsed representation of an inline constraint in a route parameter. + /// + public class InlineConstraint + { + /// + /// Creates a new . + /// + /// The constraint text. + public InlineConstraint(string constraint) + { + if (constraint == null) + { + throw new ArgumentNullException(nameof(constraint)); + } + + Constraint = constraint; + } + + /// + /// Gets the constraint text. + /// + public string Constraint { get; } + } +} \ No newline at end of file diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Template/RoutePrecedence.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Template/RoutePrecedence.cs new file mode 100644 index 0000000000..663f455923 --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Template/RoutePrecedence.cs @@ -0,0 +1,131 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; +using System.Linq; + +namespace Microsoft.AspNetCore.Routing.Template +{ + /// + /// Computes precedence for a route template. + /// + public static class RoutePrecedence + { + // Compute the precedence for matching a provided url + // e.g.: /api/template == 1.1 + // /api/template/{id} == 1.13 + // /api/{id:int} == 1.2 + // /api/template/{id:int} == 1.12 + public static decimal ComputeInbound(RouteTemplate template) + { + // Each precedence digit corresponds to one decimal place. For example, 3 segments with precedences 2, 1, + // and 4 results in a combined precedence of 2.14 (decimal). + var precedence = 0m; + + for (var i = 0; i < template.Segments.Count; i++) + { + var segment = template.Segments[i]; + + var digit = ComputeInboundPrecedenceDigit(segment); + Debug.Assert(digit >= 0 && digit < 10); + + precedence += decimal.Divide(digit, (decimal)Math.Pow(10, i)); + } + + return precedence; + } + + // Compute the precedence for generating a url + // e.g.: /api/template == 5.5 + // /api/template/{id} == 5.53 + // /api/{id:int} == 5.4 + // /api/template/{id:int} == 5.54 + public static decimal ComputeOutbound(RouteTemplate template) + { + // Each precedence digit corresponds to one decimal place. For example, 3 segments with precedences 2, 1, + // and 4 results in a combined precedence of 2.14 (decimal). + var precedence = 0m; + + for (var i = 0; i < template.Segments.Count; i++) + { + var segment = template.Segments[i]; + + var digit = ComputeOutboundPrecedenceDigit(segment); + Debug.Assert(digit >= 0 && digit < 10); + + precedence += decimal.Divide(digit, (decimal)Math.Pow(10, i)); + } + + return precedence; + } + + // Segments have the following order: + // 5 - Literal segments + // 4 - Multi-part segments && Constrained parameter segments + // 3 - Unconstrained parameter segements + // 2 - Constrained wildcard parameter segments + // 1 - Unconstrained wildcard parameter segments + private static int ComputeOutboundPrecedenceDigit(TemplateSegment segment) + { + if(segment.Parts.Count > 1) + { + return 4; + } + + var part = segment.Parts[0]; + if(part.IsLiteral) + { + return 5; + } + else + { + Debug.Assert(part.IsParameter); + var digit = part.IsCatchAll ? 1 : 3; + + if (part.InlineConstraints != null && part.InlineConstraints.Any()) + { + digit++; + } + + return digit; + } + } + + // Segments have the following order: + // 1 - Literal segments + // 2 - Constrained parameter segments / Multi-part segments + // 3 - Unconstrained parameter segments + // 4 - Constrained wildcard parameter segments + // 5 - Unconstrained wildcard parameter segments + private static int ComputeInboundPrecedenceDigit(TemplateSegment segment) + { + if (segment.Parts.Count > 1) + { + // Multi-part segments should appear after literal segments and along with parameter segments + return 2; + } + + var part = segment.Parts[0]; + // Literal segments always go first + if (part.IsLiteral) + { + return 1; + } + else + { + Debug.Assert(part.IsParameter); + var digit = part.IsCatchAll ? 5 : 3; + + // If there is a route constraint for the parameter, reduce order by 1 + // Constrained parameters end up with order 2, Constrained catch alls end up with order 4 + if (part.InlineConstraints != null && part.InlineConstraints.Any()) + { + digit--; + } + + return digit; + } + } + } +} \ No newline at end of file diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Template/RouteTemplate.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Template/RouteTemplate.cs new file mode 100644 index 0000000000..d482f3a1b4 --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Template/RouteTemplate.cs @@ -0,0 +1,82 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +namespace Microsoft.AspNetCore.Routing.Template +{ + [DebuggerDisplay("{DebuggerToString()}")] + public class RouteTemplate + { + private const string SeparatorString = "/"; + + public RouteTemplate(string template, List segments) + { + if (segments == null) + { + throw new ArgumentNullException(nameof(segments)); + } + + TemplateText = template; + + Segments = segments; + + Parameters = new List(); + for (var i = 0; i < segments.Count; i++) + { + var segment = Segments[i]; + for (var j = 0; j < segment.Parts.Count; j++) + { + var part = segment.Parts[j]; + if (part.IsParameter) + { + Parameters.Add(part); + } + } + } + } + + public string TemplateText { get; } + + public IList Parameters { get; } + + public IList Segments { get; } + + public TemplateSegment GetSegment(int index) + { + if (index < 0) + { + throw new IndexOutOfRangeException(); + } + + return index >= Segments.Count ? null : Segments[index]; + } + + private string DebuggerToString() + { + return string.Join(SeparatorString, Segments.Select(s => s.DebuggerToString())); + } + + /// + /// Gets the parameter matching the given name. + /// + /// The name of the parameter to match. + /// The matching parameter or null if no parameter matches the given name. + public TemplatePart GetParameter(string name) + { + for (var i = 0; i < Parameters.Count; i++) + { + var parameter = Parameters[i]; + if (string.Equals(parameter.Name, name, StringComparison.OrdinalIgnoreCase)) + { + return parameter; + } + } + + return null; + } + } +} diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Template/TemplateBinder.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Template/TemplateBinder.cs new file mode 100644 index 0000000000..802352e935 --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Template/TemplateBinder.cs @@ -0,0 +1,459 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections; +using System.Diagnostics; +using System.Globalization; +using System.Text.Encodings.Web; +using Microsoft.AspNetCore.Routing.Internal; +using Microsoft.Extensions.ObjectPool; + +namespace Microsoft.AspNetCore.Routing.Template +{ + public class TemplateBinder + { + private readonly UrlEncoder _urlEncoder; + private readonly ObjectPool _pool; + + private readonly RouteValueDictionary _defaults; + private readonly RouteValueDictionary _filters; + private readonly RouteTemplate _template; + + /// + /// Creates a new instance of . + /// + /// The . + /// The . + /// The to bind values to. + /// The default values for . + public TemplateBinder( + UrlEncoder urlEncoder, + ObjectPool pool, + RouteTemplate template, + RouteValueDictionary defaults) + { + if (urlEncoder == null) + { + throw new ArgumentNullException(nameof(urlEncoder)); + } + + if (pool == null) + { + throw new ArgumentNullException(nameof(pool)); + } + + if (template == null) + { + throw new ArgumentNullException(nameof(template)); + } + + _urlEncoder = urlEncoder; + _pool = pool; + _template = template; + _defaults = defaults; + + // Any default that doesn't have a corresponding parameter is a 'filter' and if a value + // is provided for that 'filter' it must match the value in defaults. + _filters = new RouteValueDictionary(_defaults); + foreach (var parameter in _template.Parameters) + { + _filters.Remove(parameter.Name); + } + } + + // Step 1: Get the list of values we're going to try to use to match and generate this URI + public TemplateValuesResult GetValues(RouteValueDictionary ambientValues, RouteValueDictionary values) + { + var context = new TemplateBindingContext(_defaults); + + // Find out which entries in the URI are valid for the URI we want to generate. + // If the URI had ordered parameters a="1", b="2", c="3" and the new values + // specified that b="9", then we need to invalidate everything after it. The new + // values should then be a="1", b="9", c=. + // + // We also handle the case where a parameter is optional but has no value - we shouldn't + // accept additional parameters that appear *after* that parameter. + for (var i = 0; i < _template.Parameters.Count; i++) + { + var parameter = _template.Parameters[i]; + + // If it's a parameter subsegment, examine the current value to see if it matches the new value + var parameterName = parameter.Name; + + object newParameterValue; + var hasNewParameterValue = values.TryGetValue(parameterName, out newParameterValue); + + object currentParameterValue = null; + var hasCurrentParameterValue = ambientValues != null && + ambientValues.TryGetValue(parameterName, out currentParameterValue); + + if (hasNewParameterValue && hasCurrentParameterValue) + { + if (!RoutePartsEqual(currentParameterValue, newParameterValue)) + { + // Stop copying current values when we find one that doesn't match + break; + } + } + + if (!hasNewParameterValue && + !hasCurrentParameterValue && + _defaults?.ContainsKey(parameter.Name) != true) + { + // This is an unsatisfied parameter value and there are no defaults. We might still + // be able to generate a URL but we should stop 'accepting' ambient values. + // + // This might be a case like: + // template: a/{b?}/{c?} + // ambient: { c = 17 } + // values: { } + // + // We can still generate a URL from this ("/a") but we shouldn't accept 'c' because + // we can't use it. + // + // In the example above we should fall into this block for 'b'. + break; + } + + // If the parameter is a match, add it to the list of values we will use for URI generation + if (hasNewParameterValue) + { + if (IsRoutePartNonEmpty(newParameterValue)) + { + context.Accept(parameterName, newParameterValue); + } + } + else + { + if (hasCurrentParameterValue) + { + context.Accept(parameterName, currentParameterValue); + } + } + } + + // Add all remaining new values to the list of values we will use for URI generation + foreach (var kvp in values) + { + if (IsRoutePartNonEmpty(kvp.Value)) + { + context.Accept(kvp.Key, kvp.Value); + } + } + + // Accept all remaining default values if they match a required parameter + for (var i = 0; i < _template.Parameters.Count; i++) + { + var parameter = _template.Parameters[i]; + if (parameter.IsOptional || parameter.IsCatchAll) + { + continue; + } + + if (context.NeedsValue(parameter.Name)) + { + // Add the default value only if there isn't already a new value for it and + // only if it actually has a default value, which we determine based on whether + // the parameter value is required. + context.AcceptDefault(parameter.Name); + } + } + + // Validate that all required parameters have a value. + for (var i = 0; i < _template.Parameters.Count; i++) + { + var parameter = _template.Parameters[i]; + if (parameter.IsOptional || parameter.IsCatchAll) + { + continue; + } + + if (!context.AcceptedValues.ContainsKey(parameter.Name)) + { + // We don't have a value for this parameter, so we can't generate a url. + return null; + } + } + + // Any default values that don't appear as parameters are treated like filters. Any new values + // provided must match these defaults. + foreach (var filter in _filters) + { + var parameter = GetParameter(filter.Key); + if (parameter != null) + { + continue; + } + + object value; + if (values.TryGetValue(filter.Key, out value)) + { + if (!RoutePartsEqual(value, filter.Value)) + { + // If there is a non-parameterized value in the route and there is a + // new value for it and it doesn't match, this route won't match. + return null; + } + } + } + + // Add any ambient values that don't match parameters - they need to be visible to constraints + // but they will ignored by link generation. + var combinedValues = new RouteValueDictionary(context.AcceptedValues); + if (ambientValues != null) + { + foreach (var kvp in ambientValues) + { + if (IsRoutePartNonEmpty(kvp.Value)) + { + var parameter = GetParameter(kvp.Key); + if (parameter == null && !context.AcceptedValues.ContainsKey(kvp.Key)) + { + combinedValues.Add(kvp.Key, kvp.Value); + } + } + } + } + + return new TemplateValuesResult() + { + AcceptedValues = context.AcceptedValues, + CombinedValues = combinedValues, + }; + } + + // Step 2: If the route is a match generate the appropriate URI + public string BindValues(RouteValueDictionary acceptedValues) + { + var context = _pool.Get(); + var result = BindValues(context, acceptedValues); + _pool.Return(context); + return result; + } + + private string BindValues(UriBuildingContext context, RouteValueDictionary acceptedValues) + { + for (var i = 0; i < _template.Segments.Count; i++) + { + Debug.Assert(context.BufferState == SegmentState.Beginning); + Debug.Assert(context.UriState == SegmentState.Beginning); + + var segment = _template.Segments[i]; + + for (var j = 0; j < segment.Parts.Count; j++) + { + var part = segment.Parts[j]; + + if (part.IsLiteral) + { + if (!context.Accept(part.Text)) + { + return null; + } + } + else if (part.IsParameter) + { + // If it's a parameter, get its value + object value; + var hasValue = acceptedValues.TryGetValue(part.Name, out value); + if (hasValue) + { + acceptedValues.Remove(part.Name); + } + + var isSameAsDefault = false; + object defaultValue; + if (_defaults != null && _defaults.TryGetValue(part.Name, out defaultValue)) + { + if (RoutePartsEqual(value, defaultValue)) + { + isSameAsDefault = true; + } + } + + var converted = Convert.ToString(value, CultureInfo.InvariantCulture); + if (isSameAsDefault) + { + // If the accepted value is the same as the default value buffer it since + // we won't necessarily add it to the URI we generate. + if (!context.Buffer(converted)) + { + return null; + } + } + else + { + // If the value is not accepted, it is null or empty value in the + // middle of the segment. We accept this if the parameter is an + // optional parameter and it is preceded by an optional seperator. + // I this case, we need to remove the optional seperator that we + // have added to the URI + // Example: template = {id}.{format?}. parameters: id=5 + // In this case after we have generated "5.", we wont find any value + // for format, so we remove '.' and generate 5. + if (!context.Accept(converted)) + { + if (j != 0 && part.IsOptional && segment.Parts[j - 1].IsOptionalSeperator) + { + context.Remove(segment.Parts[j - 1].Text); + } + else + { + return null; + } + } + } + } + } + + context.EndSegment(); + } + + // Generate the query string from the remaining values + var wroteFirst = false; + foreach (var kvp in acceptedValues) + { + if (_defaults != null && _defaults.ContainsKey(kvp.Key)) + { + // This value is a 'filter' we don't need to put it in the query string. + continue; + } + + var values = kvp.Value as IEnumerable; + if (values != null && !(values is string)) + { + foreach (var value in values) + { + wroteFirst |= AddParameterToContext(context, kvp.Key, value, wroteFirst); + } + } + else + { + wroteFirst |= AddParameterToContext(context, kvp.Key, kvp.Value, wroteFirst); + } + } + return context.ToString(); + } + + private bool AddParameterToContext(UriBuildingContext context, string key, object value, bool wroteFirst) + { + var converted = Convert.ToString(value, CultureInfo.InvariantCulture); + if (!string.IsNullOrEmpty(converted)) + { + context.Writer.Write(wroteFirst ? '&' : '?'); + _urlEncoder.Encode(context.Writer, key); + context.Writer.Write('='); + _urlEncoder.Encode(context.Writer, converted); + return true; + } + return false; + } + + private TemplatePart GetParameter(string name) + { + for (var i = 0; i < _template.Parameters.Count; i++) + { + var parameter = _template.Parameters[i]; + if (string.Equals(parameter.Name, name, StringComparison.OrdinalIgnoreCase)) + { + return parameter; + } + } + + return null; + } + + /// + /// Compares two objects for equality as parts of a case-insensitive path. + /// + /// An object to compare. + /// An object to compare. + /// True if the object are equal, otherwise false. + public static bool RoutePartsEqual(object a, object b) + { + var sa = a as string; + var sb = b as string; + + if (sa != null && sb != null) + { + // For strings do a case-insensitive comparison + return string.Equals(sa, sb, StringComparison.OrdinalIgnoreCase); + } + else + { + if (a != null && b != null) + { + // Explicitly call .Equals() in case it is overridden in the type + return a.Equals(b); + } + else + { + // At least one of them is null. Return true if they both are + return a == b; + } + } + } + + private static bool IsRoutePartNonEmpty(object routePart) + { + var routePartString = routePart as string; + if (routePartString == null) + { + return routePart != null; + } + else + { + return routePartString.Length > 0; + } + } + + [DebuggerDisplay("{DebuggerToString(),nq}")] + private struct TemplateBindingContext + { + private readonly RouteValueDictionary _defaults; + private readonly RouteValueDictionary _acceptedValues; + + public TemplateBindingContext(RouteValueDictionary defaults) + { + _defaults = defaults; + + _acceptedValues = new RouteValueDictionary(); + } + + public RouteValueDictionary AcceptedValues + { + get { return _acceptedValues; } + } + + public void Accept(string key, object value) + { + if (!_acceptedValues.ContainsKey(key)) + { + _acceptedValues.Add(key, value); + } + } + + public void AcceptDefault(string key) + { + Debug.Assert(!_acceptedValues.ContainsKey(key)); + + object value; + if (_defaults != null && _defaults.TryGetValue(key, out value)) + { + _acceptedValues.Add(key, value); + } + } + + public bool NeedsValue(string key) + { + return !_acceptedValues.ContainsKey(key); + } + + private string DebuggerToString() + { + return string.Format("{{Accepted: '{0}'}}", string.Join(", ", _acceptedValues.Keys)); + } + } + } +} diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Template/TemplateMatcher.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Template/TemplateMatcher.cs new file mode 100644 index 0000000000..e7cc2ab9f5 --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Template/TemplateMatcher.cs @@ -0,0 +1,456 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing.Internal; + +namespace Microsoft.AspNetCore.Routing.Template +{ + public class TemplateMatcher + { + private const string SeparatorString = "/"; + private const char SeparatorChar = '/'; + + // Perf: This is a cache to avoid looking things up in 'Defaults' each request. + private readonly bool[] _hasDefaultValue; + private readonly object[] _defaultValues; + + private static readonly char[] Delimiters = new char[] { SeparatorChar }; + + public TemplateMatcher( + RouteTemplate template, + RouteValueDictionary defaults) + { + if (template == null) + { + throw new ArgumentNullException(nameof(template)); + } + + Template = template; + Defaults = defaults ?? new RouteValueDictionary(); + + // Perf: cache the default value for each parameter (other than complex segments). + _hasDefaultValue = new bool[Template.Segments.Count]; + _defaultValues = new object[Template.Segments.Count]; + + for (var i = 0; i < Template.Segments.Count; i++) + { + var segment = Template.Segments[i]; + if (!segment.IsSimple) + { + continue; + } + + var part = segment.Parts[0]; + if (!part.IsParameter) + { + continue; + } + + object value; + if (Defaults.TryGetValue(part.Name, out value)) + { + _hasDefaultValue[i] = true; + _defaultValues[i] = value; + } + } + } + + public RouteValueDictionary Defaults { get; } + + public RouteTemplate Template { get; } + + public bool TryMatch(PathString path, RouteValueDictionary values) + { + if (values == null) + { + throw new ArgumentNullException(nameof(values)); + } + + var i = 0; + var pathTokenizer = new PathTokenizer(path); + + // Perf: We do a traversal of the request-segments + route-segments twice. + // + // For most segment-types, we only really need to any work on one of the two passes. + // + // On the first pass, we're just looking to see if there's anything that would disqualify us from matching. + // The most common case would be a literal segment that doesn't match. + // + // On the second pass, we're almost certainly going to match the URL, so go ahead and allocate the 'values' + // and start capturing strings. + foreach (var pathSegment in pathTokenizer) + { + if (pathSegment.Length == 0) + { + return false; + } + + var routeSegment = Template.GetSegment(i++); + if (routeSegment == null && pathSegment.Length > 0) + { + // If routeSegment is null, then we're out of route segments. All we can match is the empty + // string. + return false; + } + else if (routeSegment.IsSimple && routeSegment.Parts[0].IsLiteral) + { + // This is a literal segment, so we need to match the text, or the route isn't a match. + var part = routeSegment.Parts[0]; + if (!pathSegment.Equals(part.Text, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + } + else if (routeSegment.IsSimple && routeSegment.Parts[0].IsCatchAll) + { + // Nothing to validate for a catch-all - it can match any string, including the empty string. + // + // Also, a catch-all has to be the last part, so we're done. + break; + } + else if (routeSegment.IsSimple && routeSegment.Parts[0].IsParameter) + { + // For a parameter, validate that it's a has some length, or we have a default, or it's optional. + var part = routeSegment.Parts[0]; + if (pathSegment.Length == 0 && + !_hasDefaultValue[i] && + !part.IsOptional) + { + // There's no value for this parameter, the route can't match. + return false; + } + } + else + { + Debug.Assert(!routeSegment.IsSimple); + + // Don't attempt to validate a complex segment at this point other than being non-emtpy, + // do it in the second pass. + } + } + + for (; i < Template.Segments.Count; i++) + { + // We've matched the request path so far, but still have remaining route segments. These need + // to be all single-part parameter segments with default values or else they won't match. + var routeSegment = Template.GetSegment(i); + Debug.Assert(routeSegment != null); + + if (!routeSegment.IsSimple) + { + // If the segment is a complex segment, it MUST contain literals, and we've parsed the full + // path so far, so it can't match. + return false; + } + + var part = routeSegment.Parts[0]; + if (part.IsLiteral) + { + // If the segment is a simple literal - which need the URL to provide a value, so we don't match. + return false; + } + + if (part.IsCatchAll) + { + // Nothing to validate for a catch-all - it can match any string, including the empty string. + // + // Also, a catch-all has to be the last part, so we're done. + break; + } + + // If we get here, this is a simple segment with a parameter. We need it to be optional, or for the + // defaults to have a value. + Debug.Assert(routeSegment.IsSimple && part.IsParameter); + if (!_hasDefaultValue[i] && !part.IsOptional) + { + // There's no default for this (non-optional) parameter so it can't match. + return false; + } + } + + // At this point we've very likely got a match, so start capturing values for real. + + i = 0; + foreach (var requestSegment in pathTokenizer) + { + var routeSegment = Template.GetSegment(i++); + + if (routeSegment.IsSimple && routeSegment.Parts[0].IsCatchAll) + { + // A catch-all captures til the end of the string. + var part = routeSegment.Parts[0]; + var captured = requestSegment.Buffer.Substring(requestSegment.Offset); + if (captured.Length > 0) + { + values[part.Name] = captured; + } + else + { + // It's ok for a catch-all to produce a null value, so we don't check _hasDefaultValue. + values[part.Name] = _defaultValues[i]; + } + + // A catch-all has to be the last part, so we're done. + break; + } + else if (routeSegment.IsSimple && routeSegment.Parts[0].IsParameter) + { + // A simple parameter captures the whole segment, or a default value if nothing was + // provided. + var part = routeSegment.Parts[0]; + if (requestSegment.Length > 0) + { + values[part.Name] = requestSegment.ToString(); + } + else + { + if (_hasDefaultValue[i]) + { + values[part.Name] = _defaultValues[i]; + } + } + } + else if (!routeSegment.IsSimple) + { + if (!MatchComplexSegment(routeSegment, requestSegment.ToString(), Defaults, values)) + { + return false; + } + } + } + + for (; i < Template.Segments.Count; i++) + { + // We've matched the request path so far, but still have remaining route segments. We already know these + // are simple parameters that either have a default, or don't need to produce a value. + var routeSegment = Template.GetSegment(i); + Debug.Assert(routeSegment != null); + Debug.Assert(routeSegment.IsSimple); + + var part = routeSegment.Parts[0]; + Debug.Assert(part.IsParameter); + + // It's ok for a catch-all to produce a null value + if (_hasDefaultValue[i] || part.IsCatchAll) + { + // Don't replace an existing value with a null. + var defaultValue = _defaultValues[i]; + if (defaultValue != null || !values.ContainsKey(part.Name)) + { + values[part.Name] = defaultValue; + } + } + } + + // Copy all remaining default values to the route data + foreach (var kvp in Defaults) + { + if (!values.ContainsKey(kvp.Key)) + { + values.Add(kvp.Key, kvp.Value); + } + } + + return true; + } + + private bool MatchComplexSegment( + TemplateSegment routeSegment, + string requestSegment, + IReadOnlyDictionary defaults, + RouteValueDictionary values) + { + var indexOfLastSegment = routeSegment.Parts.Count - 1; + + // We match the request to the template starting at the rightmost parameter + // If the last segment of template is optional, then request can match the + // template with or without the last parameter. So we start with regular matching, + // but if it doesn't match, we start with next to last parameter. Example: + // Template: {p1}/{p2}.{p3?}. If the request is one/two.three it will match right away + // giving p3 value of three. But if the request is one/two, we start matching from the + // rightmost giving p3 the value of two, then we end up not matching the segment. + // In this case we start again from p2 to match the request and we succeed giving + // the value two to p2 + if (routeSegment.Parts[indexOfLastSegment].IsOptional && + routeSegment.Parts[indexOfLastSegment - 1].IsOptionalSeperator) + { + if (MatchComplexSegmentCore(routeSegment, requestSegment, Defaults, values, indexOfLastSegment)) + { + return true; + } + else + { + if (requestSegment.EndsWith( + routeSegment.Parts[indexOfLastSegment - 1].Text, + StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + return MatchComplexSegmentCore( + routeSegment, + requestSegment, + Defaults, + values, + indexOfLastSegment - 2); + } + } + else + { + return MatchComplexSegmentCore(routeSegment, requestSegment, Defaults, values, indexOfLastSegment); + } + } + + private bool MatchComplexSegmentCore( + TemplateSegment routeSegment, + string requestSegment, + IReadOnlyDictionary defaults, + RouteValueDictionary values, + int indexOfLastSegmentUsed) + { + Debug.Assert(routeSegment != null); + Debug.Assert(routeSegment.Parts.Count > 1); + + // Find last literal segment and get its last index in the string + var lastIndex = requestSegment.Length; + + TemplatePart parameterNeedsValue = null; // Keeps track of a parameter segment that is pending a value + TemplatePart lastLiteral = null; // Keeps track of the left-most literal we've encountered + + var outValues = new RouteValueDictionary(); + + while (indexOfLastSegmentUsed >= 0) + { + var newLastIndex = lastIndex; + + var part = routeSegment.Parts[indexOfLastSegmentUsed]; + if (part.IsParameter) + { + // Hold on to the parameter so that we can fill it in when we locate the next literal + parameterNeedsValue = part; + } + else + { + Debug.Assert(part.IsLiteral); + lastLiteral = part; + + var startIndex = lastIndex - 1; + // If we have a pending parameter subsegment, we must leave at least one character for that + if (parameterNeedsValue != null) + { + startIndex--; + } + + if (startIndex < 0) + { + return false; + } + + var indexOfLiteral = requestSegment.LastIndexOf( + part.Text, + startIndex, + StringComparison.OrdinalIgnoreCase); + if (indexOfLiteral == -1) + { + // If we couldn't find this literal index, this segment cannot match + return false; + } + + // If the first subsegment is a literal, it must match at the right-most extent of the request URI. + // Without this check if your route had "/Foo/" we'd match the request URI "/somethingFoo/". + // This check is related to the check we do at the very end of this function. + if (indexOfLastSegmentUsed == (routeSegment.Parts.Count - 1)) + { + if ((indexOfLiteral + part.Text.Length) != requestSegment.Length) + { + return false; + } + } + + newLastIndex = indexOfLiteral; + } + + if ((parameterNeedsValue != null) && + (((lastLiteral != null) && (part.IsLiteral)) || (indexOfLastSegmentUsed == 0))) + { + // If we have a pending parameter that needs a value, grab that value + + int parameterStartIndex; + int parameterTextLength; + + if (lastLiteral == null) + { + if (indexOfLastSegmentUsed == 0) + { + parameterStartIndex = 0; + } + else + { + parameterStartIndex = newLastIndex; + Debug.Assert(false, "indexOfLastSegementUsed should always be 0 from the check above"); + } + parameterTextLength = lastIndex; + } + else + { + // If we're getting a value for a parameter that is somewhere in the middle of the segment + if ((indexOfLastSegmentUsed == 0) && (part.IsParameter)) + { + parameterStartIndex = 0; + parameterTextLength = lastIndex; + } + else + { + parameterStartIndex = newLastIndex + lastLiteral.Text.Length; + parameterTextLength = lastIndex - parameterStartIndex; + } + } + + var parameterValueString = requestSegment.Substring(parameterStartIndex, parameterTextLength); + + if (string.IsNullOrEmpty(parameterValueString)) + { + // If we're here that means we have a segment that contains multiple sub-segments. + // For these segments all parameters must have non-empty values. If the parameter + // has an empty value it's not a match. + return false; + + } + else + { + // If there's a value in the segment for this parameter, use the subsegment value + outValues.Add(parameterNeedsValue.Name, parameterValueString); + } + + parameterNeedsValue = null; + lastLiteral = null; + } + + lastIndex = newLastIndex; + indexOfLastSegmentUsed--; + } + + // If the last subsegment is a parameter, it's OK that we didn't parse all the way to the left extent of + // the string since the parameter will have consumed all the remaining text anyway. If the last subsegment + // is a literal then we *must* have consumed the entire text in that literal. Otherwise we end up matching + // the route "Foo" to the request URI "somethingFoo". Thus we have to check that we parsed the *entire* + // request URI in order for it to be a match. + // This check is related to the check we do earlier in this function for LiteralSubsegments. + if (lastIndex == 0 || routeSegment.Parts[0].IsParameter) + { + foreach (var item in outValues) + { + values.Add(item.Key, item.Value); + } + + return true; + } + + return false; + } + } +} diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Template/TemplateParser.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Template/TemplateParser.cs new file mode 100644 index 0000000000..0168b22a4b --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Template/TemplateParser.cs @@ -0,0 +1,540 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; + +namespace Microsoft.AspNetCore.Routing.Template +{ + public static class TemplateParser + { + private const char Separator = '/'; + private const char OpenBrace = '{'; + private const char CloseBrace = '}'; + private const char EqualsSign = '='; + private const char QuestionMark = '?'; + private const char Asterisk = '*'; + private const string PeriodString = "."; + + public static RouteTemplate Parse(string routeTemplate) + { + if (routeTemplate == null) + { + routeTemplate = String.Empty; + } + + var trimmedRouteTemplate = TrimPrefix(routeTemplate); + + var context = new TemplateParserContext(trimmedRouteTemplate); + var segments = new List(); + + while (context.Next()) + { + if (context.Current == Separator) + { + // If we get here is means that there's a consecutive '/' character. + // Templates don't start with a '/' and parsing a segment consumes the separator. + throw new ArgumentException(Resources.TemplateRoute_CannotHaveConsecutiveSeparators, + nameof(routeTemplate)); + } + else + { + if (!ParseSegment(context, segments)) + { + throw new ArgumentException(context.Error, nameof(routeTemplate)); + } + } + } + + if (IsAllValid(context, segments)) + { + return new RouteTemplate(routeTemplate, segments); + } + else + { + throw new ArgumentException(context.Error, nameof(routeTemplate)); + } + } + + private static string TrimPrefix(string routeTemplate) + { + if (routeTemplate.StartsWith("~/", StringComparison.Ordinal)) + { + return routeTemplate.Substring(2); + } + else if (routeTemplate.StartsWith("/", StringComparison.Ordinal)) + { + return routeTemplate.Substring(1); + } + else if (routeTemplate.StartsWith("~", StringComparison.Ordinal)) + { + throw new ArgumentException(Resources.TemplateRoute_InvalidRouteTemplate, nameof(routeTemplate)); + } + return routeTemplate; + } + + private static bool ParseSegment(TemplateParserContext context, List segments) + { + Debug.Assert(context != null); + Debug.Assert(segments != null); + + var segment = new TemplateSegment(); + + while (true) + { + if (context.Current == OpenBrace) + { + if (!context.Next()) + { + // This is a dangling open-brace, which is not allowed + context.Error = Resources.TemplateRoute_MismatchedParameter; + return false; + } + + if (context.Current == OpenBrace) + { + // This is an 'escaped' brace in a literal, like "{{foo" + context.Back(); + if (!ParseLiteral(context, segment)) + { + return false; + } + } + else + { + // This is the inside of a parameter + if (!ParseParameter(context, segment)) + { + return false; + } + } + } + else if (context.Current == Separator) + { + // We've reached the end of the segment + break; + } + else + { + if (!ParseLiteral(context, segment)) + { + return false; + } + } + + if (!context.Next()) + { + // We've reached the end of the string + break; + } + } + + if (IsSegmentValid(context, segment)) + { + segments.Add(segment); + return true; + } + else + { + return false; + } + } + + private static bool ParseParameter(TemplateParserContext context, TemplateSegment segment) + { + context.Mark(); + + while (true) + { + if (context.Current == OpenBrace) + { + // This is an open brace inside of a parameter, it has to be escaped + if (context.Next()) + { + if (context.Current != OpenBrace) + { + // If we see something like "{p1:regex(^\d{3", we will come here. + context.Error = Resources.TemplateRoute_UnescapedBrace; + return false; + } + } + else + { + // This is a dangling open-brace, which is not allowed + // Example: "{p1:regex(^\d{" + context.Error = Resources.TemplateRoute_MismatchedParameter; + return false; + } + } + else if (context.Current == CloseBrace) + { + // When we encounter Closed brace here, it either means end of the parameter or it is a closed + // brace in the parameter, in that case it needs to be escaped. + // Example: {p1:regex(([}}])\w+}. First pair is escaped one and last marks end of the parameter + if (!context.Next()) + { + // This is the end of the string -and we have a valid parameter + context.Back(); + break; + } + + if (context.Current == CloseBrace) + { + // This is an 'escaped' brace in a parameter name + } + else + { + // This is the end of the parameter + context.Back(); + break; + } + } + + if (!context.Next()) + { + // This is a dangling open-brace, which is not allowed + context.Error = Resources.TemplateRoute_MismatchedParameter; + return false; + } + } + + var rawParameter = context.Capture(); + var decoded = rawParameter.Replace("}}", "}").Replace("{{", "{"); + + // At this point, we need to parse the raw name for inline constraint, + // default values and optional parameters. + var templatePart = InlineRouteParameterParser.ParseRouteParameter(decoded); + + if (templatePart.IsCatchAll && templatePart.IsOptional) + { + context.Error = Resources.TemplateRoute_CatchAllCannotBeOptional; + return false; + } + + if (templatePart.IsOptional && templatePart.DefaultValue != null) + { + // Cannot be optional and have a default value. + // The only way to declare an optional parameter is to have a ? at the end, + // hence we cannot have both default value and optional parameter within the template. + // A workaround is to add it as a separate entry in the defaults argument. + context.Error = Resources.TemplateRoute_OptionalCannotHaveDefaultValue; + return false; + } + + var parameterName = templatePart.Name; + if (IsValidParameterName(context, parameterName)) + { + segment.Parts.Add(templatePart); + return true; + } + else + { + return false; + } + } + + private static bool ParseLiteral(TemplateParserContext context, TemplateSegment segment) + { + context.Mark(); + + string encoded; + while (true) + { + if (context.Current == Separator) + { + encoded = context.Capture(); + context.Back(); + break; + } + else if (context.Current == OpenBrace) + { + if (!context.Next()) + { + // This is a dangling open-brace, which is not allowed + context.Error = Resources.TemplateRoute_MismatchedParameter; + return false; + } + + if (context.Current == OpenBrace) + { + // This is an 'escaped' brace in a literal, like "{{foo" - keep going. + } + else + { + // We've just seen the start of a parameter, so back up and return + context.Back(); + encoded = context.Capture(); + context.Back(); + break; + } + } + else if (context.Current == CloseBrace) + { + if (!context.Next()) + { + // This is a dangling close-brace, which is not allowed + context.Error = Resources.TemplateRoute_MismatchedParameter; + return false; + } + + if (context.Current == CloseBrace) + { + // This is an 'escaped' brace in a literal, like "{{foo" - keep going. + } + else + { + // This is an unbalanced close-brace, which is not allowed + context.Error = Resources.TemplateRoute_MismatchedParameter; + return false; + } + } + + if (!context.Next()) + { + encoded = context.Capture(); + break; + } + } + + var decoded = encoded.Replace("}}", "}").Replace("{{", "{"); + if (IsValidLiteral(context, decoded)) + { + segment.Parts.Add(TemplatePart.CreateLiteral(decoded)); + return true; + } + else + { + return false; + } + } + + private static bool IsAllValid(TemplateParserContext context, List segments) + { + // A catch-all parameter must be the last part of the last segment + for (var i = 0; i < segments.Count; i++) + { + var segment = segments[i]; + for (var j = 0; j < segment.Parts.Count; j++) + { + var part = segment.Parts[j]; + if (part.IsParameter && + part.IsCatchAll && + (i != segments.Count - 1 || j != segment.Parts.Count - 1)) + { + context.Error = Resources.TemplateRoute_CatchAllMustBeLast; + return false; + } + } + } + + return true; + } + + private static bool IsSegmentValid(TemplateParserContext context, TemplateSegment segment) + { + // If a segment has multiple parts, then it can't contain a catch all. + for (var i = 0; i < segment.Parts.Count; i++) + { + var part = segment.Parts[i]; + if (part.IsParameter && part.IsCatchAll && segment.Parts.Count > 1) + { + context.Error = Resources.TemplateRoute_CannotHaveCatchAllInMultiSegment; + return false; + } + } + + // if a segment has multiple parts, then only the last one parameter can be optional + // if it is following a optional seperator. + for (var i = 0; i < segment.Parts.Count; i++) + { + var part = segment.Parts[i]; + + if (part.IsParameter && part.IsOptional && segment.Parts.Count > 1) + { + // This optional parameter is the last part in the segment + if (i == segment.Parts.Count - 1) + { + if (!segment.Parts[i - 1].IsLiteral) + { + // The optional parameter is preceded by something that is not a literal. + // Example of error message: + // "In the segment '{RouteValue}{param?}', the optional parameter 'param' is preceded + // by an invalid segment '{RouteValue}'. Only a period (.) can precede an optional parameter. + context.Error = string.Format( + Resources.TemplateRoute_OptionalParameterCanbBePrecededByPeriod, + segment.DebuggerToString(), + part.Name, + segment.Parts[i - 1].DebuggerToString()); + + return false; + } + else if (segment.Parts[i - 1].Text != PeriodString) + { + // The optional parameter is preceded by a literal other than period. + // Example of error message: + // "In the segment '{RouteValue}-{param?}', the optional parameter 'param' is preceded + // by an invalid segment '-'. Only a period (.) can precede an optional parameter. + context.Error = string.Format( + Resources.TemplateRoute_OptionalParameterCanbBePrecededByPeriod, + segment.DebuggerToString(), + part.Name, + segment.Parts[i - 1].Text); + + return false; + } + + segment.Parts[i - 1].IsOptionalSeperator = true; + } + else + { + // This optional parameter is not the last one in the segment + // Example: + // An optional parameter must be at the end of the segment.In the segment '{RouteValue?})', + // optional parameter 'RouteValue' is followed by ')' + var nextPart = segment.Parts[i + 1]; + var invalidPartText = nextPart.IsParameter ? nextPart.Name : nextPart.Text; + + context.Error = string.Format( + Resources.TemplateRoute_OptionalParameterHasTobeTheLast, + segment.DebuggerToString(), + segment.Parts[i].Name, + invalidPartText); + + return false; + } + } + } + + // A segment cannot contain two consecutive parameters + var isLastSegmentParameter = false; + for (var i = 0; i < segment.Parts.Count; i++) + { + var part = segment.Parts[i]; + if (part.IsParameter && isLastSegmentParameter) + { + context.Error = Resources.TemplateRoute_CannotHaveConsecutiveParameters; + return false; + } + + isLastSegmentParameter = part.IsParameter; + } + + return true; + } + + private static bool IsValidParameterName(TemplateParserContext context, string parameterName) + { + if (parameterName.Length == 0) + { + context.Error = String.Format(CultureInfo.CurrentCulture, + Resources.TemplateRoute_InvalidParameterName, parameterName); + return false; + } + + for (var i = 0; i < parameterName.Length; i++) + { + var c = parameterName[i]; + if (c == Separator || c == OpenBrace || c == CloseBrace || c == QuestionMark || c == Asterisk) + { + context.Error = String.Format(CultureInfo.CurrentCulture, + Resources.TemplateRoute_InvalidParameterName, parameterName); + return false; + } + } + + if (!context.ParameterNames.Add(parameterName)) + { + context.Error = String.Format(CultureInfo.CurrentCulture, + Resources.TemplateRoute_RepeatedParameter, parameterName); + return false; + } + + return true; + } + + private static bool IsValidLiteral(TemplateParserContext context, string literal) + { + Debug.Assert(context != null); + Debug.Assert(literal != null); + + if (literal.IndexOf(QuestionMark) != -1) + { + context.Error = String.Format(CultureInfo.CurrentCulture, + Resources.TemplateRoute_InvalidLiteral, literal); + return false; + } + + return true; + } + + private static bool IsInvalidRouteTemplate(string routeTemplate) + { + return routeTemplate.StartsWith("~", StringComparison.Ordinal) || + routeTemplate.StartsWith("/", StringComparison.Ordinal); + } + + private class TemplateParserContext + { + private readonly string _template; + private int _index; + private int? _mark; + + private HashSet _parameterNames = new HashSet(StringComparer.OrdinalIgnoreCase); + + public TemplateParserContext(string template) + { + Debug.Assert(template != null); + _template = template; + + _index = -1; + } + + public char Current + { + get { return (_index < _template.Length && _index >= 0) ? _template[_index] : (char)0; } + } + + public string Error + { + get; + set; + } + + public HashSet ParameterNames + { + get { return _parameterNames; } + } + + public bool Back() + { + return --_index >= 0; + } + + public bool Next() + { + return ++_index < _template.Length; + } + + public void Mark() + { + _mark = _index; + } + + public string Capture() + { + if (_mark.HasValue) + { + var value = _template.Substring(_mark.Value, _index - _mark.Value); + _mark = null; + return value; + } + else + { + return null; + } + } + } + } +} diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Template/TemplatePart.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Template/TemplatePart.cs new file mode 100644 index 0000000000..70f588a41a --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Template/TemplatePart.cs @@ -0,0 +1,67 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +namespace Microsoft.AspNetCore.Routing.Template +{ + [DebuggerDisplay("{DebuggerToString()}")] + public class TemplatePart + { + public static TemplatePart CreateLiteral(string text) + { + return new TemplatePart() + { + IsLiteral = true, + Text = text, + }; + } + + public static TemplatePart CreateParameter(string name, + bool isCatchAll, + bool isOptional, + object defaultValue, + IEnumerable inlineConstraints) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + return new TemplatePart() + { + IsParameter = true, + Name = name, + IsCatchAll = isCatchAll, + IsOptional = isOptional, + DefaultValue = defaultValue, + InlineConstraints = inlineConstraints ?? Enumerable.Empty(), + }; + } + + public bool IsCatchAll { get; private set; } + public bool IsLiteral { get; private set; } + public bool IsParameter { get; private set; } + public bool IsOptional { get; private set; } + public bool IsOptionalSeperator { get; set; } + public string Name { get; private set; } + public string Text { get; private set; } + public object DefaultValue { get; private set; } + public IEnumerable InlineConstraints { get; private set; } + + internal string DebuggerToString() + { + if (IsParameter) + { + return "{" + (IsCatchAll ? "*" : string.Empty) + Name + (IsOptional ? "?" : string.Empty) + "}"; + } + else + { + return Text; + } + } + } +} diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Template/TemplateSegment.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Template/TemplateSegment.cs new file mode 100644 index 0000000000..4a86526509 --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Template/TemplateSegment.cs @@ -0,0 +1,22 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +namespace Microsoft.AspNetCore.Routing.Template +{ + [DebuggerDisplay("{DebuggerToString()}")] + public class TemplateSegment + { + public bool IsSimple => Parts.Count == 1; + + public List Parts { get; } = new List(); + + internal string DebuggerToString() + { + return string.Join(string.Empty, Parts.Select(p => p.DebuggerToString())); + } + } +} diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Template/TemplateValuesResult.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Template/TemplateValuesResult.cs new file mode 100644 index 0000000000..2a7c46398f --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Template/TemplateValuesResult.cs @@ -0,0 +1,29 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Routing.Template +{ + /// + /// The values used as inputs for constraints and link generation. + /// + public class TemplateValuesResult + { + /// + /// The set of values that will appear in the URL. + /// + public RouteValueDictionary AcceptedValues { get; set; } + + /// + /// The set of values that that were supplied for URL generation. + /// + /// + /// This combines implicit (ambient) values from the of the current request + /// (if applicable), explictly provided values, and default values for parameters that appear in + /// the route template. + /// + /// Implicit (ambient) values which are invalidated due to changes in values lexically earlier in the + /// route template are excluded from this set. + /// + public RouteValueDictionary CombinedValues { get; set; } + } +} \ No newline at end of file diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Tree/InboundMatch.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Tree/InboundMatch.cs new file mode 100644 index 0000000000..57f1b6db7b --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Tree/InboundMatch.cs @@ -0,0 +1,30 @@ +// 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.Diagnostics; +using Microsoft.AspNetCore.Routing.Template; + +namespace Microsoft.AspNetCore.Routing.Tree +{ + /// + /// A candidate route to match incoming URLs in a . + /// + [DebuggerDisplay("{DebuggerToString(),nq}")] + public class InboundMatch + { + /// + /// Gets or sets the . + /// + public InboundRouteEntry Entry { get; set; } + + /// + /// Gets or sets the . + /// + public TemplateMatcher TemplateMatcher { get; set; } + + private string DebuggerToString() + { + return TemplateMatcher?.Template?.TemplateText; + } + } +} diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Tree/InboundRouteEntry.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Tree/InboundRouteEntry.cs new file mode 100644 index 0000000000..7c4a5f0abc --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Tree/InboundRouteEntry.cs @@ -0,0 +1,56 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using Microsoft.AspNetCore.Routing.Template; + +namespace Microsoft.AspNetCore.Routing.Tree +{ + /// + /// Used to build an . Represents a URL template tha will be used to match incoming + /// request URLs. + /// + public class InboundRouteEntry + { + /// + /// Gets or sets the route constraints. + /// + public IDictionary Constraints { get; set; } + + /// + /// Gets or sets the route defaults. + /// + public RouteValueDictionary Defaults { get; set; } + + /// + /// Gets or sets the to invoke when this entry matches. + /// + public IRouter Handler { get; set; } + + /// + /// Gets or sets the order of the entry. + /// + /// + /// Entries are ordered first by (ascending) then by (descending). + /// + public int Order { get; set; } + + /// + /// Gets or sets the precedence of the entry. + /// + /// + /// Entries are ordered first by (ascending) then by (descending). + /// + public decimal Precedence { get; set; } + + /// + /// Gets or sets the name of the route. + /// + public string RouteName { get; set; } + + /// + /// Gets or sets the . + /// + public RouteTemplate RouteTemplate { get; set; } + } +} diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Tree/OutboundMatch.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Tree/OutboundMatch.cs new file mode 100644 index 0000000000..49980b9912 --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Tree/OutboundMatch.cs @@ -0,0 +1,23 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Routing.Template; + +namespace Microsoft.AspNetCore.Routing.Tree +{ + /// + /// A candidate match for link generation in a . + /// + public class OutboundMatch + { + /// + /// Gets or sets the . + /// + public OutboundRouteEntry Entry { get; set; } + + /// + /// Gets or sets the . + /// + public TemplateBinder TemplateBinder { get; set; } + } +} diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Tree/OutboundRouteEntry.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Tree/OutboundRouteEntry.cs new file mode 100644 index 0000000000..2364c3f350 --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Tree/OutboundRouteEntry.cs @@ -0,0 +1,62 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using Microsoft.AspNetCore.Routing.Template; + +namespace Microsoft.AspNetCore.Routing.Tree +{ + /// + /// Used to build a . Represents a URL template that will be used to generate + /// outgoing URLs. + /// + public class OutboundRouteEntry + { + /// + /// Gets or sets the route constraints. + /// + public IDictionary Constraints { get; set; } + + /// + /// Gets or sets the route defaults. + /// + public RouteValueDictionary Defaults { get; set; } + + /// + /// The to invoke when this entry matches. + /// + public IRouter Handler { get; set; } + + /// + /// Gets or sets the order of the entry. + /// + /// + /// Entries are ordered first by (ascending) then by (descending). + /// + public int Order { get; set; } + + /// + /// Gets or sets the precedence of the template for link generation. A greater value of + /// means that an entry is considered first. + /// + /// + /// Entries are ordered first by (ascending) then by (descending). + /// + public decimal Precedence { get; set; } + + /// + /// Gets or sets the name of the route. + /// + public string RouteName { get; set; } + + /// + /// Gets or sets the set of values that must be present for link genration. + /// + public RouteValueDictionary RequiredLinkValues { get; set; } + + /// + /// Gets or sets the . + /// + public RouteTemplate RouteTemplate { get; set; } + } +} \ No newline at end of file diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Tree/TreeRouteBuilder.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Tree/TreeRouteBuilder.cs new file mode 100644 index 0000000000..a746e7d170 --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Tree/TreeRouteBuilder.cs @@ -0,0 +1,454 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text.Encodings.Web; +using Microsoft.AspNetCore.Routing.Internal; +using Microsoft.AspNetCore.Routing.Template; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.ObjectPool; + +namespace Microsoft.AspNetCore.Routing.Tree +{ + /// + /// Builder for instances. + /// + public class TreeRouteBuilder + { + private readonly ILogger _logger; + private readonly ILogger _constraintLogger; + private readonly UrlEncoder _urlEncoder; + private readonly ObjectPool _objectPool; + private readonly IInlineConstraintResolver _constraintResolver; + + /// + /// + /// This constructor is obsolete and will be removed in a future version. The recommended + /// alternative is the overload that does not take a UrlEncoder. + /// + /// Initializes a new instance of . + /// + /// The . + /// The . + /// The . + /// The . + [Obsolete("This constructor is obsolete and will be removed in a future version. The recommended " + + "alternative is the overload that does not take a UrlEncoder.")] + public TreeRouteBuilder( + ILoggerFactory loggerFactory, + UrlEncoder urlEncoder, + ObjectPool objectPool, + IInlineConstraintResolver constraintResolver) + : this(loggerFactory, objectPool, constraintResolver) + { + if (urlEncoder == null) + { + throw new ArgumentNullException(nameof(urlEncoder)); + } + + _urlEncoder = urlEncoder; + } + + /// + /// Initializes a new instance of . + /// + /// The . + /// The . + /// The . + public TreeRouteBuilder( + ILoggerFactory loggerFactory, + ObjectPool objectPool, + IInlineConstraintResolver constraintResolver) + { + if (loggerFactory == null) + { + throw new ArgumentNullException(nameof(loggerFactory)); + } + + if (objectPool == null) + { + throw new ArgumentNullException(nameof(objectPool)); + } + + if (constraintResolver == null) + { + throw new ArgumentNullException(nameof(constraintResolver)); + } + + _urlEncoder = UrlEncoder.Default; + _objectPool = objectPool; + _constraintResolver = constraintResolver; + + _logger = loggerFactory.CreateLogger(); + _constraintLogger = loggerFactory.CreateLogger(typeof(RouteConstraintMatcher).FullName); + } + + /// + /// Adds a new inbound route to the . + /// + /// The for handling the route. + /// The of the route. + /// The route name. + /// The route order. + /// The . + public InboundRouteEntry MapInbound( + IRouter handler, + RouteTemplate routeTemplate, + string routeName, + int order) + { + if (handler == null) + { + throw new ArgumentNullException(nameof(handler)); + } + + if (routeTemplate == null) + { + throw new ArgumentNullException(nameof(routeTemplate)); + } + + var entry = new InboundRouteEntry() + { + Handler = handler, + Order = order, + Precedence = RoutePrecedence.ComputeInbound(routeTemplate), + RouteName = routeName, + RouteTemplate = routeTemplate, + }; + + var constraintBuilder = new RouteConstraintBuilder(_constraintResolver, routeTemplate.TemplateText); + foreach (var parameter in routeTemplate.Parameters) + { + if (parameter.InlineConstraints != null) + { + if (parameter.IsOptional) + { + constraintBuilder.SetOptional(parameter.Name); + } + + foreach (var constraint in parameter.InlineConstraints) + { + constraintBuilder.AddResolvedConstraint(parameter.Name, constraint.Constraint); + } + } + } + + entry.Constraints = constraintBuilder.Build(); + + entry.Defaults = new RouteValueDictionary(); + foreach (var parameter in entry.RouteTemplate.Parameters) + { + if (parameter.DefaultValue != null) + { + entry.Defaults.Add(parameter.Name, parameter.DefaultValue); + } + } + + InboundEntries.Add(entry); + return entry; + } + + /// + /// Adds a new outbound route to the . + /// + /// The for handling the link generation. + /// The of the route. + /// The containing the route values. + /// The route name. + /// The route order. + /// The . + public OutboundRouteEntry MapOutbound( + IRouter handler, + RouteTemplate routeTemplate, + RouteValueDictionary requiredLinkValues, + string routeName, + int order) + { + if (handler == null) + { + throw new ArgumentNullException(nameof(handler)); + } + + if (routeTemplate == null) + { + throw new ArgumentNullException(nameof(routeTemplate)); + } + + if (requiredLinkValues == null) + { + throw new ArgumentNullException(nameof(requiredLinkValues)); + } + + var entry = new OutboundRouteEntry() + { + Handler = handler, + Order = order, + Precedence = RoutePrecedence.ComputeOutbound(routeTemplate), + RequiredLinkValues = requiredLinkValues, + RouteName = routeName, + RouteTemplate = routeTemplate, + }; + + var constraintBuilder = new RouteConstraintBuilder(_constraintResolver, routeTemplate.TemplateText); + foreach (var parameter in routeTemplate.Parameters) + { + if (parameter.InlineConstraints != null) + { + if (parameter.IsOptional) + { + constraintBuilder.SetOptional(parameter.Name); + } + + foreach (var constraint in parameter.InlineConstraints) + { + constraintBuilder.AddResolvedConstraint(parameter.Name, constraint.Constraint); + } + } + } + + entry.Constraints = constraintBuilder.Build(); + + entry.Defaults = new RouteValueDictionary(); + foreach (var parameter in entry.RouteTemplate.Parameters) + { + if (parameter.DefaultValue != null) + { + entry.Defaults.Add(parameter.Name, parameter.DefaultValue); + } + } + + OutboundEntries.Add(entry); + return entry; + } + + /// + /// Gets the list of . + /// + public IList InboundEntries { get; } = new List(); + + /// + /// Gets the list of . + /// + public IList OutboundEntries { get; } = new List(); + + /// + /// Builds a with the + /// and defined in this . + /// + /// The . + public TreeRouter Build() + { + return Build(version: 0); + } + + /// + /// Builds a with the + /// and defined in this . + /// + /// The version of the . + /// The . + public TreeRouter Build(int version) + { + // Tree route builder builds a tree for each of the different route orders defined by + // the user. When a route needs to be matched, the matching algorithm in tree router + // just iterates over the trees in ascending order when it tries to match the route. + var trees = new Dictionary(); + + foreach (var entry in InboundEntries) + { + UrlMatchingTree tree; + if (!trees.TryGetValue(entry.Order, out tree)) + { + tree = new UrlMatchingTree(entry.Order); + trees.Add(entry.Order, tree); + } + + AddEntryToTree(tree, entry); + } + + return new TreeRouter( + trees.Values.OrderBy(tree => tree.Order).ToArray(), + OutboundEntries, + _urlEncoder, + _objectPool, + _logger, + _constraintLogger, + version); + } + + /// + /// Removes all and from this + /// . + /// + public void Clear() + { + InboundEntries.Clear(); + OutboundEntries.Clear(); + } + + private void AddEntryToTree(UrlMatchingTree tree, InboundRouteEntry entry) + { + // The url matching tree represents all the routes asociated with a given + // order. Each node in the tree represents all the different categories + // a segment can have for which there is a defined inbound route entry. + // Each node contains a set of Matches that indicate all the routes for which + // a URL is a potential match. This list contains the routes with the same + // number of segments and the routes with the same number of segments plus an + // additional catch all parameter (as it can be empty). + // For example, for a set of routes like: + // 'Customer/Index/{id}' + // '{Controller}/{Action}/{*parameters}' + // + // The route tree will look like: + // Root -> + // Literals: Customer -> + // Literals: Index -> + // Parameters: {id} + // Matches: 'Customer/Index/{id}' + // Parameters: {Controller} -> + // Parameters: {Action} -> + // Matches: '{Controller}/{Action}/{*parameters}' + // CatchAlls: {*parameters} + // Matches: '{Controller}/{Action}/{*parameters}' + // + // When the tree router tries to match a route, it iterates the list of url matching trees + // in ascending order. For each tree it traverses each node starting from the root in the + // following order: Literals, constrained parameters, parameters, constrained catch all routes, catch alls. + // When it gets to a node of the same length as the route its trying to match, it simply looks at the list of + // candidates (which is in precence order) and tries to match the url against it. + // + + var current = tree.Root; + var matcher = new TemplateMatcher(entry.RouteTemplate, entry.Defaults); + + for (var i = 0; i < entry.RouteTemplate.Segments.Count; i++) + { + var segment = entry.RouteTemplate.Segments[i]; + if (!segment.IsSimple) + { + // Treat complex segments as a constrained parameter + if (current.ConstrainedParameters == null) + { + current.ConstrainedParameters = new UrlMatchingNode(length: i + 1); + } + + current = current.ConstrainedParameters; + continue; + } + + Debug.Assert(segment.Parts.Count == 1); + var part = segment.Parts[0]; + if (part.IsLiteral) + { + UrlMatchingNode next; + if (!current.Literals.TryGetValue(part.Text, out next)) + { + next = new UrlMatchingNode(length: i + 1); + current.Literals.Add(part.Text, next); + } + + current = next; + continue; + } + + // We accept templates that have intermediate optional values, but we ignore + // those values for route matching. For that reason, we need to add the entry + // to the list of matches, only if the remaining segments are optional. For example: + // /{controller}/{action=Index}/{id} will be equivalent to /{controller}/{action}/{id} + // for the purposes of route matching. + if (part.IsParameter && + RemainingSegmentsAreOptional(entry.RouteTemplate.Segments, i)) + { + current.Matches.Add(new InboundMatch() { Entry = entry, TemplateMatcher = matcher }); + } + + if (part.IsParameter && part.InlineConstraints.Any() && !part.IsCatchAll) + { + if (current.ConstrainedParameters == null) + { + current.ConstrainedParameters = new UrlMatchingNode(length: i + 1); + } + + current = current.ConstrainedParameters; + continue; + } + + if (part.IsParameter && !part.IsCatchAll) + { + if (current.Parameters == null) + { + current.Parameters = new UrlMatchingNode(length: i + 1); + } + + current = current.Parameters; + continue; + } + + if (part.IsParameter && part.InlineConstraints.Any() && part.IsCatchAll) + { + if (current.ConstrainedCatchAlls == null) + { + current.ConstrainedCatchAlls = new UrlMatchingNode(length: i + 1) { IsCatchAll = true }; + } + + current = current.ConstrainedCatchAlls; + continue; + } + + if (part.IsParameter && part.IsCatchAll) + { + if (current.CatchAlls == null) + { + current.CatchAlls = new UrlMatchingNode(length: i + 1) { IsCatchAll = true }; + } + + current = current.CatchAlls; + continue; + } + + Debug.Fail("We shouldn't get here."); + } + + current.Matches.Add(new InboundMatch() { Entry = entry, TemplateMatcher = matcher }); + current.Matches.Sort((x, y) => + { + var result = x.Entry.Precedence.CompareTo(y.Entry.Precedence); + return result == 0 ? x.Entry.RouteTemplate.TemplateText.CompareTo(y.Entry.RouteTemplate.TemplateText) : result; + }); + } + + private static bool RemainingSegmentsAreOptional(IList segments, int currentParameterIndex) + { + for (var i = currentParameterIndex; i < segments.Count; i++) + { + if (!segments[i].IsSimple) + { + // /{complex}-{segment} + return false; + } + + var part = segments[i].Parts[0]; + if (!part.IsParameter) + { + // /literal + return false; + } + + var isOptionlCatchAllOrHasDefaultValue = part.IsOptional || + part.IsCatchAll || + part.DefaultValue != null; + + if (!isOptionlCatchAllOrHasDefaultValue) + { + // /{parameter} + return false; + } + } + + return true; + } + } +} diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Tree/TreeRouter.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Tree/TreeRouter.cs new file mode 100644 index 0000000000..31a1091093 --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Tree/TreeRouter.cs @@ -0,0 +1,423 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Routing.Internal; +using Microsoft.AspNetCore.Routing.Logging; +using Microsoft.AspNetCore.Routing.Template; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.ObjectPool; + +namespace Microsoft.AspNetCore.Routing.Tree +{ + /// + /// An implementation for attribute routing. + /// + public class TreeRouter : IRouter + { + // Key used by routing and action selection to match an attribute route entry to a + // group of action descriptors. + public static readonly string RouteGroupKey = "!__route_group"; + + private readonly LinkGenerationDecisionTree _linkGenerationTree; + private readonly UrlMatchingTree[] _trees; + private readonly IDictionary _namedEntries; + + private readonly ILogger _logger; + private readonly ILogger _constraintLogger; + + /// + /// Creates a new . + /// + /// The list of that contains the route entries. + /// The set of . + /// The . + /// The . + /// The instance. + /// The instance used + /// in . + /// The version of this route. + public TreeRouter( + UrlMatchingTree[] trees, + IEnumerable linkGenerationEntries, + UrlEncoder urlEncoder, + ObjectPool objectPool, + ILogger routeLogger, + ILogger constraintLogger, + int version) + { + if (trees == null) + { + throw new ArgumentNullException(nameof(trees)); + } + + if (linkGenerationEntries == null) + { + throw new ArgumentNullException(nameof(linkGenerationEntries)); + } + + if (urlEncoder == null) + { + throw new ArgumentNullException(nameof(urlEncoder)); + } + + if (objectPool == null) + { + throw new ArgumentNullException(nameof(objectPool)); + } + + if (routeLogger == null) + { + throw new ArgumentNullException(nameof(routeLogger)); + } + + if (constraintLogger == null) + { + throw new ArgumentNullException(nameof(constraintLogger)); + } + + _trees = trees; + _logger = routeLogger; + _constraintLogger = constraintLogger; + + _namedEntries = new Dictionary(StringComparer.OrdinalIgnoreCase); + + var outboundMatches = new List(); + + foreach (var entry in linkGenerationEntries) + { + + var binder = new TemplateBinder(urlEncoder, objectPool, entry.RouteTemplate, entry.Defaults); + var outboundMatch = new OutboundMatch() { Entry = entry, TemplateBinder = binder }; + outboundMatches.Add(outboundMatch); + + // Skip unnamed entries + if (entry.RouteName == null) + { + continue; + } + + // We only need to keep one OutboundMatch per route template + // so in case two entries have the same name and the same template we only keep + // the first entry. + OutboundMatch namedMatch; + if (_namedEntries.TryGetValue(entry.RouteName, out namedMatch) && + !string.Equals( + namedMatch.Entry.RouteTemplate.TemplateText, + entry.RouteTemplate.TemplateText, + StringComparison.OrdinalIgnoreCase)) + { + throw new ArgumentException( + Resources.FormatAttributeRoute_DifferentLinkGenerationEntries_SameName(entry.RouteName), + nameof(linkGenerationEntries)); + } + else if (namedMatch == null) + { + _namedEntries.Add(entry.RouteName, outboundMatch); + } + } + + // The decision tree will take care of ordering for these entries. + _linkGenerationTree = new LinkGenerationDecisionTree(outboundMatches.ToArray()); + + Version = version; + } + + /// + /// Gets the version of this route. + /// + public int Version { get; } + + internal IEnumerable MatchingTrees => _trees; + + /// + public VirtualPathData GetVirtualPath(VirtualPathContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // If it's a named route we will try to generate a link directly and + // if we can't, we will not try to generate it using an unnamed route. + if (context.RouteName != null) + { + return GetVirtualPathForNamedRoute(context); + } + + // The decision tree will give us back all entries that match the provided route data in the correct + // order. We just need to iterate them and use the first one that can generate a link. + var matches = _linkGenerationTree.GetMatches(context); + + if (matches == null) + { + return null; + } + + for (var i = 0; i < matches.Count; i++) + { + var path = GenerateVirtualPath(context, matches[i].Match.Entry, matches[i].Match.TemplateBinder); + if (path != null) + { + return path; + } + } + + return null; + } + + /// + public async Task RouteAsync(RouteContext context) + { + foreach (var tree in _trees) + { + var tokenizer = new PathTokenizer(context.HttpContext.Request.Path); + var root = tree.Root; + + var treeEnumerator = new TreeEnumerator(root, tokenizer); + + // Create a snapshot before processing the route. We'll restore this snapshot before running each + // to restore the state. This is likely an "empty" snapshot, which doesn't allocate. + var snapshot = context.RouteData.PushState(router: null, values: null, dataTokens: null); + + while (treeEnumerator.MoveNext()) + { + var node = treeEnumerator.Current; + foreach (var item in node.Matches) + { + var entry = item.Entry; + var matcher = item.TemplateMatcher; + + try + { + if (!matcher.TryMatch(context.HttpContext.Request.Path, context.RouteData.Values)) + { + continue; + } + + if (!RouteConstraintMatcher.Match( + entry.Constraints, + context.RouteData.Values, + context.HttpContext, + this, + RouteDirection.IncomingRequest, + _constraintLogger)) + { + continue; + } + + _logger.MatchedRoute(entry.RouteName, entry.RouteTemplate.TemplateText); + context.RouteData.Routers.Add(entry.Handler); + + await entry.Handler.RouteAsync(context); + if (context.Handler != null) + { + return; + } + } + finally + { + if (context.Handler == null) + { + // Restore the original values to prevent polluting the route data. + snapshot.Restore(); + } + } + } + } + } + } + + private struct TreeEnumerator : IEnumerator + { + private readonly Stack _stack; + private readonly PathTokenizer _tokenizer; + + public TreeEnumerator(UrlMatchingNode root, PathTokenizer tokenizer) + { + _stack = new Stack(); + _tokenizer = tokenizer; + Current = null; + + _stack.Push(root); + } + + public UrlMatchingNode Current { get; private set; } + + object IEnumerator.Current => Current; + + public void Dispose() + { + } + + public bool MoveNext() + { + if (_stack == null) + { + return false; + } + + while (_stack.Count > 0) + { + var next = _stack.Pop(); + + // In case of wild card segment, the request path segment length can be greater + // Example: + // Template: a/{*path} + // Request Url: a/b/c/d + if (next.IsCatchAll && next.Matches.Count > 0) + { + Current = next; + return true; + } + // Next template has the same length as the url we are trying to match + // The only possible matching segments are either our current matches or + // any catch-all segment after this segment in which the catch all is empty. + else if (next.Depth == _tokenizer.Count) + { + if (next.Matches.Count > 0) + { + Current = next; + return true; + } + else + { + // We can stop looking as any other child node from this node will be + // either a literal, a constrained parameter or a parameter. + // (Catch alls and constrained catch alls will show up as candidate matches). + continue; + } + } + + if (next.CatchAlls != null) + { + _stack.Push(next.CatchAlls); + } + + if (next.ConstrainedCatchAlls != null) + { + _stack.Push(next.ConstrainedCatchAlls); + } + + if (next.Parameters != null) + { + _stack.Push(next.Parameters); + } + + if (next.ConstrainedParameters != null) + { + _stack.Push(next.ConstrainedParameters); + } + + if (next.Literals.Count > 0) + { + UrlMatchingNode node; + Debug.Assert(next.Depth < _tokenizer.Count); + if (next.Literals.TryGetValue(_tokenizer[next.Depth].Value, out node)) + { + _stack.Push(node); + } + } + } + + return false; + } + + public void Reset() + { + _stack.Clear(); + Current = null; + } + } + + private VirtualPathData GetVirtualPathForNamedRoute(VirtualPathContext context) + { + OutboundMatch match; + if (_namedEntries.TryGetValue(context.RouteName, out match)) + { + var path = GenerateVirtualPath(context, match.Entry, match.TemplateBinder); + if (path != null) + { + return path; + } + } + return null; + } + + private VirtualPathData GenerateVirtualPath( + VirtualPathContext context, + OutboundRouteEntry entry, + TemplateBinder binder) + { + // In attribute the context includes the values that are used to select this entry - typically + // these will be the standard 'action', 'controller' and maybe 'area' tokens. However, we don't + // want to pass these to the link generation code, or else they will end up as query parameters. + // + // So, we need to exclude from here any values that are 'required link values', but aren't + // parameters in the template. + // + // Ex: + // template: api/Products/{action} + // required values: { id = "5", action = "Buy", Controller = "CoolProducts" } + // + // result: { id = "5", action = "Buy" } + var inputValues = new RouteValueDictionary(); + foreach (var kvp in context.Values) + { + if (entry.RequiredLinkValues.ContainsKey(kvp.Key)) + { + var parameter = entry.RouteTemplate.GetParameter(kvp.Key); + + if (parameter == null) + { + continue; + } + } + + inputValues.Add(kvp.Key, kvp.Value); + } + + var bindingResult = binder.GetValues(context.AmbientValues, inputValues); + if (bindingResult == null) + { + // A required parameter in the template didn't get a value. + return null; + } + + var matched = RouteConstraintMatcher.Match( + entry.Constraints, + bindingResult.CombinedValues, + context.HttpContext, + this, + RouteDirection.UrlGeneration, + _constraintLogger); + + if (!matched) + { + // A constraint rejected this link. + return null; + } + + var pathData = entry.Handler.GetVirtualPath(context); + if (pathData != null) + { + // If path is non-null then the target router short-circuited, we don't expect this + // in typical MVC scenarios. + return pathData; + } + + var path = binder.BindValues(bindingResult.AcceptedValues); + if (path == null) + { + return null; + } + + return new VirtualPathData(this, path); + } + } +} diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Tree/UrlMatchingNode.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Tree/UrlMatchingNode.cs new file mode 100644 index 0000000000..ffc387efe9 --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Tree/UrlMatchingNode.cs @@ -0,0 +1,81 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +namespace Microsoft.AspNetCore.Routing.Tree +{ + /// + /// A node in a . + /// + [DebuggerDisplay("{DebuggerToString(),nq}")] + public class UrlMatchingNode + { + /// + /// Initializes a new instance of . + /// + /// The length of the path to this node in the . + public UrlMatchingNode(int length) + { + Depth = length; + + Matches = new List(); + Literals = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + /// + /// Gets the length of the path to this node in the . + /// + public int Depth { get; } + + /// + /// Gets or sets a value indicating whether this node represents a catch all segment. + /// + public bool IsCatchAll { get; set; } + + /// + /// Gets the list of matching route entries associated with this node. + /// + /// + /// These entries are sorted by precedence then template. + /// + public List Matches { get; } + + /// + /// Gets the literal segments following this segment. + /// + public Dictionary Literals { get; } + + /// + /// Gets or sets the representing + /// parameter segments with constraints following this segment in the . + /// + public UrlMatchingNode ConstrainedParameters { get; set; } + + /// + /// Gets or sets the representing + /// parameter segments following this segment in the . + /// + public UrlMatchingNode Parameters { get; set; } + + /// + /// Gets or sets the representing + /// catch all parameter segments with constraints following this segment in the . + /// + public UrlMatchingNode ConstrainedCatchAlls { get; set; } + + /// + /// Gets or sets the representing + /// catch all parameter segments following this segment in the . + /// + public UrlMatchingNode CatchAlls { get; set; } + + private string DebuggerToString() + { + return $"Length: {Depth}, Matches: {string.Join(" | ", Matches?.Select(m => $"({m.TemplateMatcher.Template.TemplateText})"))}"; + } + } +} diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Tree/UrlMatchingTree.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Tree/UrlMatchingTree.cs new file mode 100644 index 0000000000..90528d75b9 --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Tree/UrlMatchingTree.cs @@ -0,0 +1,30 @@ +// 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.Tree +{ + /// + /// A tree part of a . + /// + public class UrlMatchingTree + { + /// + /// Initializes a new instance of . + /// + /// The order associated with routes in this . + public UrlMatchingTree(int order) + { + Order = order; + } + + /// + /// Gets the order of the routes associated with this . + /// + public int Order { get; } + + /// + /// Gets the root of the . + /// + public UrlMatchingNode Root { get; } = new UrlMatchingNode(length: 0); + } +} diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/baseline.netcore.json b/src/Routing/src/Microsoft.AspNetCore.Routing/baseline.netcore.json new file mode 100644 index 0000000000..866f3e89cb --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/baseline.netcore.json @@ -0,0 +1,4579 @@ +{ + "AssemblyIdentity": "Microsoft.AspNetCore.Routing, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "Types": [ + { + "Name": "Microsoft.AspNetCore.Builder.MapRouteRouteBuilderExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "MapRoute", + "Parameters": [ + { + "Name": "routeBuilder", + "Type": "Microsoft.AspNetCore.Routing.IRouteBuilder" + }, + { + "Name": "name", + "Type": "System.String" + }, + { + "Name": "template", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.AspNetCore.Routing.IRouteBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "MapRoute", + "Parameters": [ + { + "Name": "routeBuilder", + "Type": "Microsoft.AspNetCore.Routing.IRouteBuilder" + }, + { + "Name": "name", + "Type": "System.String" + }, + { + "Name": "template", + "Type": "System.String" + }, + { + "Name": "defaults", + "Type": "System.Object" + } + ], + "ReturnType": "Microsoft.AspNetCore.Routing.IRouteBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "MapRoute", + "Parameters": [ + { + "Name": "routeBuilder", + "Type": "Microsoft.AspNetCore.Routing.IRouteBuilder" + }, + { + "Name": "name", + "Type": "System.String" + }, + { + "Name": "template", + "Type": "System.String" + }, + { + "Name": "defaults", + "Type": "System.Object" + }, + { + "Name": "constraints", + "Type": "System.Object" + } + ], + "ReturnType": "Microsoft.AspNetCore.Routing.IRouteBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "MapRoute", + "Parameters": [ + { + "Name": "routeBuilder", + "Type": "Microsoft.AspNetCore.Routing.IRouteBuilder" + }, + { + "Name": "name", + "Type": "System.String" + }, + { + "Name": "template", + "Type": "System.String" + }, + { + "Name": "defaults", + "Type": "System.Object" + }, + { + "Name": "constraints", + "Type": "System.Object" + }, + { + "Name": "dataTokens", + "Type": "System.Object" + } + ], + "ReturnType": "Microsoft.AspNetCore.Routing.IRouteBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Builder.RouterMiddleware", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Invoke", + "Parameters": [ + { + "Name": "httpContext", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "next", + "Type": "Microsoft.AspNetCore.Http.RequestDelegate" + }, + { + "Name": "loggerFactory", + "Type": "Microsoft.Extensions.Logging.ILoggerFactory" + }, + { + "Name": "router", + "Type": "Microsoft.AspNetCore.Routing.IRouter" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Builder.RoutingBuilderExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "UseRouter", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + }, + { + "Name": "router", + "Type": "Microsoft.AspNetCore.Routing.IRouter" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "UseRouter", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + }, + { + "Name": "action", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Routing.DefaultInlineConstraintResolver", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Routing.IInlineConstraintResolver" + ], + "Members": [ + { + "Kind": "Method", + "Name": "ResolveConstraint", + "Parameters": [ + { + "Name": "inlineConstraint", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.AspNetCore.Routing.IRouteConstraint", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Routing.IInlineConstraintResolver", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "routeOptions", + "Type": "Microsoft.Extensions.Options.IOptions" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Routing.IInlineConstraintResolver", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "ResolveConstraint", + "Parameters": [ + { + "Name": "inlineConstraint", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.AspNetCore.Routing.IRouteConstraint", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Routing.INamedRouter", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Routing.IRouter" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Name", + "Parameters": [], + "ReturnType": "System.String", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Routing.InlineRouteParameterParser", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "ParseRouteParameter", + "Parameters": [ + { + "Name": "routeParameter", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.AspNetCore.Routing.Template.TemplatePart", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Routing.IRouteBuilder", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_ApplicationBuilder", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_DefaultHandler", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Routing.IRouter", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_DefaultHandler", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Routing.IRouter" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ServiceProvider", + "Parameters": [], + "ReturnType": "System.IServiceProvider", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Routes", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IList", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Build", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Routing.IRouter", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Routing.IRouteCollection", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Routing.IRouter" + ], + "Members": [ + { + "Kind": "Method", + "Name": "Add", + "Parameters": [ + { + "Name": "router", + "Type": "Microsoft.AspNetCore.Routing.IRouter" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Routing.RequestDelegateRouteBuilderExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "MapRoute", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Routing.IRouteBuilder" + }, + { + "Name": "template", + "Type": "System.String" + }, + { + "Name": "handler", + "Type": "Microsoft.AspNetCore.Http.RequestDelegate" + } + ], + "ReturnType": "Microsoft.AspNetCore.Routing.IRouteBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "MapMiddlewareRoute", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Routing.IRouteBuilder" + }, + { + "Name": "template", + "Type": "System.String" + }, + { + "Name": "action", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Routing.IRouteBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "MapDelete", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Routing.IRouteBuilder" + }, + { + "Name": "template", + "Type": "System.String" + }, + { + "Name": "handler", + "Type": "Microsoft.AspNetCore.Http.RequestDelegate" + } + ], + "ReturnType": "Microsoft.AspNetCore.Routing.IRouteBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "MapMiddlewareDelete", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Routing.IRouteBuilder" + }, + { + "Name": "template", + "Type": "System.String" + }, + { + "Name": "action", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Routing.IRouteBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "MapDelete", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Routing.IRouteBuilder" + }, + { + "Name": "template", + "Type": "System.String" + }, + { + "Name": "handler", + "Type": "System.Func" + } + ], + "ReturnType": "Microsoft.AspNetCore.Routing.IRouteBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "MapGet", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Routing.IRouteBuilder" + }, + { + "Name": "template", + "Type": "System.String" + }, + { + "Name": "handler", + "Type": "Microsoft.AspNetCore.Http.RequestDelegate" + } + ], + "ReturnType": "Microsoft.AspNetCore.Routing.IRouteBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "MapMiddlewareGet", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Routing.IRouteBuilder" + }, + { + "Name": "template", + "Type": "System.String" + }, + { + "Name": "action", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Routing.IRouteBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "MapGet", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Routing.IRouteBuilder" + }, + { + "Name": "template", + "Type": "System.String" + }, + { + "Name": "handler", + "Type": "System.Func" + } + ], + "ReturnType": "Microsoft.AspNetCore.Routing.IRouteBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "MapPost", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Routing.IRouteBuilder" + }, + { + "Name": "template", + "Type": "System.String" + }, + { + "Name": "handler", + "Type": "Microsoft.AspNetCore.Http.RequestDelegate" + } + ], + "ReturnType": "Microsoft.AspNetCore.Routing.IRouteBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "MapMiddlewarePost", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Routing.IRouteBuilder" + }, + { + "Name": "template", + "Type": "System.String" + }, + { + "Name": "action", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Routing.IRouteBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "MapPost", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Routing.IRouteBuilder" + }, + { + "Name": "template", + "Type": "System.String" + }, + { + "Name": "handler", + "Type": "System.Func" + } + ], + "ReturnType": "Microsoft.AspNetCore.Routing.IRouteBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "MapPut", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Routing.IRouteBuilder" + }, + { + "Name": "template", + "Type": "System.String" + }, + { + "Name": "handler", + "Type": "Microsoft.AspNetCore.Http.RequestDelegate" + } + ], + "ReturnType": "Microsoft.AspNetCore.Routing.IRouteBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "MapMiddlewarePut", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Routing.IRouteBuilder" + }, + { + "Name": "template", + "Type": "System.String" + }, + { + "Name": "action", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Routing.IRouteBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "MapPut", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Routing.IRouteBuilder" + }, + { + "Name": "template", + "Type": "System.String" + }, + { + "Name": "handler", + "Type": "System.Func" + } + ], + "ReturnType": "Microsoft.AspNetCore.Routing.IRouteBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "MapVerb", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Routing.IRouteBuilder" + }, + { + "Name": "verb", + "Type": "System.String" + }, + { + "Name": "template", + "Type": "System.String" + }, + { + "Name": "handler", + "Type": "System.Func" + } + ], + "ReturnType": "Microsoft.AspNetCore.Routing.IRouteBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "MapVerb", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Routing.IRouteBuilder" + }, + { + "Name": "verb", + "Type": "System.String" + }, + { + "Name": "template", + "Type": "System.String" + }, + { + "Name": "handler", + "Type": "Microsoft.AspNetCore.Http.RequestDelegate" + } + ], + "ReturnType": "Microsoft.AspNetCore.Routing.IRouteBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "MapMiddlewareVerb", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Routing.IRouteBuilder" + }, + { + "Name": "verb", + "Type": "System.String" + }, + { + "Name": "template", + "Type": "System.String" + }, + { + "Name": "action", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Routing.IRouteBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Routing.Route", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Routing.RouteBase", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_RouteTemplate", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "OnRouteMatched", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Routing.RouteContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "OnVirtualPathGenerated", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Routing.VirtualPathContext" + } + ], + "ReturnType": "Microsoft.AspNetCore.Routing.VirtualPathData", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "target", + "Type": "Microsoft.AspNetCore.Routing.IRouter" + }, + { + "Name": "routeTemplate", + "Type": "System.String" + }, + { + "Name": "inlineConstraintResolver", + "Type": "Microsoft.AspNetCore.Routing.IInlineConstraintResolver" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "target", + "Type": "Microsoft.AspNetCore.Routing.IRouter" + }, + { + "Name": "routeTemplate", + "Type": "System.String" + }, + { + "Name": "defaults", + "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary" + }, + { + "Name": "constraints", + "Type": "System.Collections.Generic.IDictionary" + }, + { + "Name": "dataTokens", + "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary" + }, + { + "Name": "inlineConstraintResolver", + "Type": "Microsoft.AspNetCore.Routing.IInlineConstraintResolver" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "target", + "Type": "Microsoft.AspNetCore.Routing.IRouter" + }, + { + "Name": "routeName", + "Type": "System.String" + }, + { + "Name": "routeTemplate", + "Type": "System.String" + }, + { + "Name": "defaults", + "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary" + }, + { + "Name": "constraints", + "Type": "System.Collections.Generic.IDictionary" + }, + { + "Name": "dataTokens", + "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary" + }, + { + "Name": "inlineConstraintResolver", + "Type": "Microsoft.AspNetCore.Routing.IInlineConstraintResolver" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Routing.RouteBase", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "ImplementedInterfaces": [ + "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", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IDictionary", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Constraints", + "Parameters": [ + { + "Name": "value", + "Type": "System.Collections.Generic.IDictionary" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ConstraintResolver", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Routing.IInlineConstraintResolver", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ConstraintResolver", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Routing.IInlineConstraintResolver" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_DataTokens", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Routing.RouteValueDictionary", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_DataTokens", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Defaults", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Routing.RouteValueDictionary", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Defaults", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Name", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Routing.INamedRouter", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Name", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ParsedTemplate", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Routing.Template.RouteTemplate", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ParsedTemplate", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Routing.Template.RouteTemplate" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetConstraints", + "Parameters": [ + { + "Name": "inlineConstraintResolver", + "Type": "Microsoft.AspNetCore.Routing.IInlineConstraintResolver" + }, + { + "Name": "parsedTemplate", + "Type": "Microsoft.AspNetCore.Routing.Template.RouteTemplate" + }, + { + "Name": "constraints", + "Type": "System.Collections.Generic.IDictionary" + } + ], + "ReturnType": "System.Collections.Generic.IDictionary", + "Static": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetDefaults", + "Parameters": [ + { + "Name": "parsedTemplate", + "Type": "Microsoft.AspNetCore.Routing.Template.RouteTemplate" + }, + { + "Name": "defaults", + "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary" + } + ], + "ReturnType": "Microsoft.AspNetCore.Routing.RouteValueDictionary", + "Static": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ToString", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "template", + "Type": "System.String" + }, + { + "Name": "name", + "Type": "System.String" + }, + { + "Name": "constraintResolver", + "Type": "Microsoft.AspNetCore.Routing.IInlineConstraintResolver" + }, + { + "Name": "defaults", + "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary" + }, + { + "Name": "constraints", + "Type": "System.Collections.Generic.IDictionary" + }, + { + "Name": "dataTokens", + "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Routing.RouteBuilder", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Routing.IRouteBuilder" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_ApplicationBuilder", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouteBuilder", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_DefaultHandler", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Routing.IRouter", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouteBuilder", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_DefaultHandler", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Routing.IRouter" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouteBuilder", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ServiceProvider", + "Parameters": [], + "ReturnType": "System.IServiceProvider", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouteBuilder", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Routes", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IList", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouteBuilder", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Build", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Routing.IRouter", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouteBuilder", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "applicationBuilder", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "applicationBuilder", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + }, + { + "Name": "defaultHandler", + "Type": "Microsoft.AspNetCore.Routing.IRouter" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Routing.RouteCollection", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "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", + "Parameters": [ + { + "Name": "index", + "Type": "System.Int32" + } + ], + "ReturnType": "Microsoft.AspNetCore.Routing.IRouter", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Count", + "Parameters": [], + "ReturnType": "System.Int32", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Add", + "Parameters": [ + { + "Name": "router", + "Type": "Microsoft.AspNetCore.Routing.IRouter" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouteCollection", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Routing.RouteConstraintBuilder", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Build", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IDictionary", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddConstraint", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + }, + { + "Name": "value", + "Type": "System.Object" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddResolvedConstraint", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + }, + { + "Name": "constraintText", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SetOptional", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "inlineConstraintResolver", + "Type": "Microsoft.AspNetCore.Routing.IInlineConstraintResolver" + }, + { + "Name": "displayName", + "Type": "System.String" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Routing.RouteConstraintMatcher", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Match", + "Parameters": [ + { + "Name": "constraints", + "Type": "System.Collections.Generic.IDictionary" + }, + { + "Name": "routeValues", + "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary" + }, + { + "Name": "httpContext", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "route", + "Type": "Microsoft.AspNetCore.Routing.IRouter" + }, + { + "Name": "routeDirection", + "Type": "Microsoft.AspNetCore.Routing.RouteDirection" + }, + { + "Name": "logger", + "Type": "Microsoft.Extensions.Logging.ILogger" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Routing.RouteCreationException", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "System.Exception", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "message", + "Type": "System.String" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "message", + "Type": "System.String" + }, + { + "Name": "innerException", + "Type": "System.Exception" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Routing.RouteHandler", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Routing.IRouteHandler", + "Microsoft.AspNetCore.Routing.IRouter" + ], + "Members": [ + { + "Kind": "Method", + "Name": "GetRequestHandler", + "Parameters": [ + { + "Name": "httpContext", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "routeData", + "Type": "Microsoft.AspNetCore.Routing.RouteData" + } + ], + "ReturnType": "Microsoft.AspNetCore.Http.RequestDelegate", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouteHandler", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetVirtualPath", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Routing.VirtualPathContext" + } + ], + "ReturnType": "Microsoft.AspNetCore.Routing.VirtualPathData", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouter", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "RouteAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Routing.RouteContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouter", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "requestDelegate", + "Type": "Microsoft.AspNetCore.Http.RequestDelegate" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Routing.RouteOptions", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_LowercaseUrls", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_LowercaseUrls", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_AppendTrailingSlash", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_AppendTrailingSlash", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ConstraintMap", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IDictionary", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ConstraintMap", + "Parameters": [ + { + "Name": "value", + "Type": "System.Collections.Generic.IDictionary" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Routing.RouteValueEqualityComparer", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "System.Collections.Generic.IEqualityComparer" + ], + "Members": [ + { + "Kind": "Method", + "Name": "Equals", + "Parameters": [ + { + "Name": "x", + "Type": "System.Object" + }, + { + "Name": "y", + "Type": "System.Object" + } + ], + "ReturnType": "System.Boolean", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.Collections.Generic.IEqualityComparer", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetHashCode", + "Parameters": [ + { + "Name": "obj", + "Type": "System.Object" + } + ], + "ReturnType": "System.Int32", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.Collections.Generic.IEqualityComparer", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Routing.RoutingFeature", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Routing.IRoutingFeature" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_RouteData", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Routing.RouteData", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRoutingFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RouteData", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Routing.RouteData" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRoutingFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Routing.Tree.InboundMatch", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Entry", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Routing.Tree.InboundRouteEntry", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Entry", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Routing.Tree.InboundRouteEntry" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_TemplateMatcher", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Routing.Template.TemplateMatcher", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_TemplateMatcher", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Routing.Template.TemplateMatcher" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Routing.Tree.InboundRouteEntry", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Constraints", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IDictionary", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Constraints", + "Parameters": [ + { + "Name": "value", + "Type": "System.Collections.Generic.IDictionary" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Defaults", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Routing.RouteValueDictionary", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Defaults", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Handler", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Routing.IRouter", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Handler", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Routing.IRouter" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Order", + "Parameters": [], + "ReturnType": "System.Int32", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Order", + "Parameters": [ + { + "Name": "value", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Precedence", + "Parameters": [], + "ReturnType": "System.Decimal", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Precedence", + "Parameters": [ + { + "Name": "value", + "Type": "System.Decimal" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_RouteName", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RouteName", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_RouteTemplate", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Routing.Template.RouteTemplate", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RouteTemplate", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Routing.Template.RouteTemplate" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Routing.Tree.OutboundMatch", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Entry", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Routing.Tree.OutboundRouteEntry", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Entry", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Routing.Tree.OutboundRouteEntry" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_TemplateBinder", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Routing.Template.TemplateBinder", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_TemplateBinder", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Routing.Template.TemplateBinder" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Routing.Tree.OutboundRouteEntry", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Constraints", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IDictionary", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Constraints", + "Parameters": [ + { + "Name": "value", + "Type": "System.Collections.Generic.IDictionary" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Defaults", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Routing.RouteValueDictionary", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Defaults", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Handler", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Routing.IRouter", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Handler", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Routing.IRouter" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Order", + "Parameters": [], + "ReturnType": "System.Int32", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Order", + "Parameters": [ + { + "Name": "value", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Precedence", + "Parameters": [], + "ReturnType": "System.Decimal", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Precedence", + "Parameters": [ + { + "Name": "value", + "Type": "System.Decimal" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_RouteName", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RouteName", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_RequiredLinkValues", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Routing.RouteValueDictionary", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RequiredLinkValues", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_RouteTemplate", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Routing.Template.RouteTemplate", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RouteTemplate", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Routing.Template.RouteTemplate" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Routing.Tree.TreeRouteBuilder", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "MapInbound", + "Parameters": [ + { + "Name": "handler", + "Type": "Microsoft.AspNetCore.Routing.IRouter" + }, + { + "Name": "routeTemplate", + "Type": "Microsoft.AspNetCore.Routing.Template.RouteTemplate" + }, + { + "Name": "routeName", + "Type": "System.String" + }, + { + "Name": "order", + "Type": "System.Int32" + } + ], + "ReturnType": "Microsoft.AspNetCore.Routing.Tree.InboundRouteEntry", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "MapOutbound", + "Parameters": [ + { + "Name": "handler", + "Type": "Microsoft.AspNetCore.Routing.IRouter" + }, + { + "Name": "routeTemplate", + "Type": "Microsoft.AspNetCore.Routing.Template.RouteTemplate" + }, + { + "Name": "requiredLinkValues", + "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary" + }, + { + "Name": "routeName", + "Type": "System.String" + }, + { + "Name": "order", + "Type": "System.Int32" + } + ], + "ReturnType": "Microsoft.AspNetCore.Routing.Tree.OutboundRouteEntry", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_InboundEntries", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IList", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OutboundEntries", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IList", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Build", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Routing.Tree.TreeRouter", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Build", + "Parameters": [ + { + "Name": "version", + "Type": "System.Int32" + } + ], + "ReturnType": "Microsoft.AspNetCore.Routing.Tree.TreeRouter", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Clear", + "Parameters": [], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "loggerFactory", + "Type": "Microsoft.Extensions.Logging.ILoggerFactory" + }, + { + "Name": "urlEncoder", + "Type": "System.Text.Encodings.Web.UrlEncoder" + }, + { + "Name": "objectPool", + "Type": "Microsoft.Extensions.ObjectPool.ObjectPool" + }, + { + "Name": "constraintResolver", + "Type": "Microsoft.AspNetCore.Routing.IInlineConstraintResolver" + } + ], + "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": [] + }, + { + "Name": "Microsoft.AspNetCore.Routing.Tree.TreeRouter", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Routing.IRouter" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Version", + "Parameters": [], + "ReturnType": "System.Int32", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetVirtualPath", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Routing.VirtualPathContext" + } + ], + "ReturnType": "Microsoft.AspNetCore.Routing.VirtualPathData", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouter", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "RouteAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Routing.RouteContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouter", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "trees", + "Type": "Microsoft.AspNetCore.Routing.Tree.UrlMatchingTree[]" + }, + { + "Name": "linkGenerationEntries", + "Type": "System.Collections.Generic.IEnumerable" + }, + { + "Name": "urlEncoder", + "Type": "System.Text.Encodings.Web.UrlEncoder" + }, + { + "Name": "objectPool", + "Type": "Microsoft.Extensions.ObjectPool.ObjectPool" + }, + { + "Name": "routeLogger", + "Type": "Microsoft.Extensions.Logging.ILogger" + }, + { + "Name": "constraintLogger", + "Type": "Microsoft.Extensions.Logging.ILogger" + }, + { + "Name": "version", + "Type": "System.Int32" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "RouteGroupKey", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Routing.Tree.UrlMatchingNode", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Depth", + "Parameters": [], + "ReturnType": "System.Int32", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_IsCatchAll", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_IsCatchAll", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Matches", + "Parameters": [], + "ReturnType": "System.Collections.Generic.List", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Literals", + "Parameters": [], + "ReturnType": "System.Collections.Generic.Dictionary", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ConstrainedParameters", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Routing.Tree.UrlMatchingNode", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ConstrainedParameters", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Routing.Tree.UrlMatchingNode" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Parameters", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Routing.Tree.UrlMatchingNode", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Parameters", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Routing.Tree.UrlMatchingNode" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ConstrainedCatchAlls", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Routing.Tree.UrlMatchingNode", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ConstrainedCatchAlls", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Routing.Tree.UrlMatchingNode" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_CatchAlls", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Routing.Tree.UrlMatchingNode", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_CatchAlls", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Routing.Tree.UrlMatchingNode" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "length", + "Type": "System.Int32" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Routing.Tree.UrlMatchingTree", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Order", + "Parameters": [], + "ReturnType": "System.Int32", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Root", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Routing.Tree.UrlMatchingNode", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "order", + "Type": "System.Int32" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Routing.Template.InlineConstraint", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Constraint", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "constraint", + "Type": "System.String" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Routing.Template.RoutePrecedence", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "ComputeInbound", + "Parameters": [ + { + "Name": "template", + "Type": "Microsoft.AspNetCore.Routing.Template.RouteTemplate" + } + ], + "ReturnType": "System.Decimal", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ComputeOutbound", + "Parameters": [ + { + "Name": "template", + "Type": "Microsoft.AspNetCore.Routing.Template.RouteTemplate" + } + ], + "ReturnType": "System.Decimal", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Routing.Template.RouteTemplate", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_TemplateText", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Parameters", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IList", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Segments", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IList", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetSegment", + "Parameters": [ + { + "Name": "index", + "Type": "System.Int32" + } + ], + "ReturnType": "Microsoft.AspNetCore.Routing.Template.TemplateSegment", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetParameter", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.AspNetCore.Routing.Template.TemplatePart", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "template", + "Type": "System.String" + }, + { + "Name": "segments", + "Type": "System.Collections.Generic.List" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Routing.Template.TemplateBinder", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "GetValues", + "Parameters": [ + { + "Name": "ambientValues", + "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary" + }, + { + "Name": "values", + "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary" + } + ], + "ReturnType": "Microsoft.AspNetCore.Routing.Template.TemplateValuesResult", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "BindValues", + "Parameters": [ + { + "Name": "acceptedValues", + "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary" + } + ], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "RoutePartsEqual", + "Parameters": [ + { + "Name": "a", + "Type": "System.Object" + }, + { + "Name": "b", + "Type": "System.Object" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "urlEncoder", + "Type": "System.Text.Encodings.Web.UrlEncoder" + }, + { + "Name": "pool", + "Type": "Microsoft.Extensions.ObjectPool.ObjectPool" + }, + { + "Name": "template", + "Type": "Microsoft.AspNetCore.Routing.Template.RouteTemplate" + }, + { + "Name": "defaults", + "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Routing.Template.TemplateMatcher", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Defaults", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Routing.RouteValueDictionary", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Template", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Routing.Template.RouteTemplate", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TryMatch", + "Parameters": [ + { + "Name": "path", + "Type": "Microsoft.AspNetCore.Http.PathString" + }, + { + "Name": "values", + "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary" + } + ], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "template", + "Type": "Microsoft.AspNetCore.Routing.Template.RouteTemplate" + }, + { + "Name": "defaults", + "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Routing.Template.TemplateParser", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Parse", + "Parameters": [ + { + "Name": "routeTemplate", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.AspNetCore.Routing.Template.RouteTemplate", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Routing.Template.TemplatePart", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "CreateLiteral", + "Parameters": [ + { + "Name": "text", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.AspNetCore.Routing.Template.TemplatePart", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "CreateParameter", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + }, + { + "Name": "isCatchAll", + "Type": "System.Boolean" + }, + { + "Name": "isOptional", + "Type": "System.Boolean" + }, + { + "Name": "defaultValue", + "Type": "System.Object" + }, + { + "Name": "inlineConstraints", + "Type": "System.Collections.Generic.IEnumerable" + } + ], + "ReturnType": "Microsoft.AspNetCore.Routing.Template.TemplatePart", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_IsCatchAll", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_IsLiteral", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_IsParameter", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_IsOptional", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_IsOptionalSeperator", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_IsOptionalSeperator", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Name", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Text", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_DefaultValue", + "Parameters": [], + "ReturnType": "System.Object", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_InlineConstraints", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IEnumerable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Routing.Template.TemplateSegment", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_IsSimple", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Parts", + "Parameters": [], + "ReturnType": "System.Collections.Generic.List", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Routing.Template.TemplateValuesResult", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_AcceptedValues", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Routing.RouteValueDictionary", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_AcceptedValues", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_CombinedValues", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Routing.RouteValueDictionary", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_CombinedValues", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Routing.Constraints.AlphaRouteConstraint", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Routing.Constraints.RegexRouteConstraint", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Routing.Constraints.BoolRouteConstraint", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Routing.IRouteConstraint" + ], + "Members": [ + { + "Kind": "Method", + "Name": "Match", + "Parameters": [ + { + "Name": "httpContext", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "route", + "Type": "Microsoft.AspNetCore.Routing.IRouter" + }, + { + "Name": "routeKey", + "Type": "System.String" + }, + { + "Name": "values", + "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary" + }, + { + "Name": "routeDirection", + "Type": "Microsoft.AspNetCore.Routing.RouteDirection" + } + ], + "ReturnType": "System.Boolean", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouteConstraint", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Routing.Constraints.CompositeRouteConstraint", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Routing.IRouteConstraint" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Constraints", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IEnumerable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Match", + "Parameters": [ + { + "Name": "httpContext", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "route", + "Type": "Microsoft.AspNetCore.Routing.IRouter" + }, + { + "Name": "routeKey", + "Type": "System.String" + }, + { + "Name": "values", + "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary" + }, + { + "Name": "routeDirection", + "Type": "Microsoft.AspNetCore.Routing.RouteDirection" + } + ], + "ReturnType": "System.Boolean", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouteConstraint", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "constraints", + "Type": "System.Collections.Generic.IEnumerable" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Routing.Constraints.DateTimeRouteConstraint", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Routing.IRouteConstraint" + ], + "Members": [ + { + "Kind": "Method", + "Name": "Match", + "Parameters": [ + { + "Name": "httpContext", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "route", + "Type": "Microsoft.AspNetCore.Routing.IRouter" + }, + { + "Name": "routeKey", + "Type": "System.String" + }, + { + "Name": "values", + "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary" + }, + { + "Name": "routeDirection", + "Type": "Microsoft.AspNetCore.Routing.RouteDirection" + } + ], + "ReturnType": "System.Boolean", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouteConstraint", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Routing.Constraints.DecimalRouteConstraint", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Routing.IRouteConstraint" + ], + "Members": [ + { + "Kind": "Method", + "Name": "Match", + "Parameters": [ + { + "Name": "httpContext", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "route", + "Type": "Microsoft.AspNetCore.Routing.IRouter" + }, + { + "Name": "routeKey", + "Type": "System.String" + }, + { + "Name": "values", + "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary" + }, + { + "Name": "routeDirection", + "Type": "Microsoft.AspNetCore.Routing.RouteDirection" + } + ], + "ReturnType": "System.Boolean", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouteConstraint", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Routing.Constraints.DoubleRouteConstraint", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Routing.IRouteConstraint" + ], + "Members": [ + { + "Kind": "Method", + "Name": "Match", + "Parameters": [ + { + "Name": "httpContext", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "route", + "Type": "Microsoft.AspNetCore.Routing.IRouter" + }, + { + "Name": "routeKey", + "Type": "System.String" + }, + { + "Name": "values", + "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary" + }, + { + "Name": "routeDirection", + "Type": "Microsoft.AspNetCore.Routing.RouteDirection" + } + ], + "ReturnType": "System.Boolean", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouteConstraint", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Routing.Constraints.FloatRouteConstraint", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Routing.IRouteConstraint" + ], + "Members": [ + { + "Kind": "Method", + "Name": "Match", + "Parameters": [ + { + "Name": "httpContext", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "route", + "Type": "Microsoft.AspNetCore.Routing.IRouter" + }, + { + "Name": "routeKey", + "Type": "System.String" + }, + { + "Name": "values", + "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary" + }, + { + "Name": "routeDirection", + "Type": "Microsoft.AspNetCore.Routing.RouteDirection" + } + ], + "ReturnType": "System.Boolean", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouteConstraint", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Routing.Constraints.GuidRouteConstraint", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Routing.IRouteConstraint" + ], + "Members": [ + { + "Kind": "Method", + "Name": "Match", + "Parameters": [ + { + "Name": "httpContext", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "route", + "Type": "Microsoft.AspNetCore.Routing.IRouter" + }, + { + "Name": "routeKey", + "Type": "System.String" + }, + { + "Name": "values", + "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary" + }, + { + "Name": "routeDirection", + "Type": "Microsoft.AspNetCore.Routing.RouteDirection" + } + ], + "ReturnType": "System.Boolean", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouteConstraint", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Routing.Constraints.HttpMethodRouteConstraint", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Routing.IRouteConstraint" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_AllowedMethods", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IList", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Match", + "Parameters": [ + { + "Name": "httpContext", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "route", + "Type": "Microsoft.AspNetCore.Routing.IRouter" + }, + { + "Name": "routeKey", + "Type": "System.String" + }, + { + "Name": "values", + "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary" + }, + { + "Name": "routeDirection", + "Type": "Microsoft.AspNetCore.Routing.RouteDirection" + } + ], + "ReturnType": "System.Boolean", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouteConstraint", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "allowedMethods", + "Type": "System.String[]", + "IsParams": true + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Routing.Constraints.IntRouteConstraint", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Routing.IRouteConstraint" + ], + "Members": [ + { + "Kind": "Method", + "Name": "Match", + "Parameters": [ + { + "Name": "httpContext", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "route", + "Type": "Microsoft.AspNetCore.Routing.IRouter" + }, + { + "Name": "routeKey", + "Type": "System.String" + }, + { + "Name": "values", + "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary" + }, + { + "Name": "routeDirection", + "Type": "Microsoft.AspNetCore.Routing.RouteDirection" + } + ], + "ReturnType": "System.Boolean", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouteConstraint", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Routing.Constraints.LengthRouteConstraint", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Routing.IRouteConstraint" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_MinLength", + "Parameters": [], + "ReturnType": "System.Int32", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_MaxLength", + "Parameters": [], + "ReturnType": "System.Int32", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Match", + "Parameters": [ + { + "Name": "httpContext", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "route", + "Type": "Microsoft.AspNetCore.Routing.IRouter" + }, + { + "Name": "routeKey", + "Type": "System.String" + }, + { + "Name": "values", + "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary" + }, + { + "Name": "routeDirection", + "Type": "Microsoft.AspNetCore.Routing.RouteDirection" + } + ], + "ReturnType": "System.Boolean", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouteConstraint", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "length", + "Type": "System.Int32" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "minLength", + "Type": "System.Int32" + }, + { + "Name": "maxLength", + "Type": "System.Int32" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Routing.Constraints.LongRouteConstraint", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Routing.IRouteConstraint" + ], + "Members": [ + { + "Kind": "Method", + "Name": "Match", + "Parameters": [ + { + "Name": "httpContext", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "route", + "Type": "Microsoft.AspNetCore.Routing.IRouter" + }, + { + "Name": "routeKey", + "Type": "System.String" + }, + { + "Name": "values", + "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary" + }, + { + "Name": "routeDirection", + "Type": "Microsoft.AspNetCore.Routing.RouteDirection" + } + ], + "ReturnType": "System.Boolean", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouteConstraint", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Routing.Constraints.MaxLengthRouteConstraint", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Routing.IRouteConstraint" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_MaxLength", + "Parameters": [], + "ReturnType": "System.Int32", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Match", + "Parameters": [ + { + "Name": "httpContext", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "route", + "Type": "Microsoft.AspNetCore.Routing.IRouter" + }, + { + "Name": "routeKey", + "Type": "System.String" + }, + { + "Name": "values", + "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary" + }, + { + "Name": "routeDirection", + "Type": "Microsoft.AspNetCore.Routing.RouteDirection" + } + ], + "ReturnType": "System.Boolean", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouteConstraint", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "maxLength", + "Type": "System.Int32" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Routing.Constraints.MaxRouteConstraint", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Routing.IRouteConstraint" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Max", + "Parameters": [], + "ReturnType": "System.Int64", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Match", + "Parameters": [ + { + "Name": "httpContext", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "route", + "Type": "Microsoft.AspNetCore.Routing.IRouter" + }, + { + "Name": "routeKey", + "Type": "System.String" + }, + { + "Name": "values", + "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary" + }, + { + "Name": "routeDirection", + "Type": "Microsoft.AspNetCore.Routing.RouteDirection" + } + ], + "ReturnType": "System.Boolean", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouteConstraint", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "max", + "Type": "System.Int64" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Routing.Constraints.MinLengthRouteConstraint", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Routing.IRouteConstraint" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_MinLength", + "Parameters": [], + "ReturnType": "System.Int32", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Match", + "Parameters": [ + { + "Name": "httpContext", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "route", + "Type": "Microsoft.AspNetCore.Routing.IRouter" + }, + { + "Name": "routeKey", + "Type": "System.String" + }, + { + "Name": "values", + "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary" + }, + { + "Name": "routeDirection", + "Type": "Microsoft.AspNetCore.Routing.RouteDirection" + } + ], + "ReturnType": "System.Boolean", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouteConstraint", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "minLength", + "Type": "System.Int32" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Routing.Constraints.MinRouteConstraint", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Routing.IRouteConstraint" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Min", + "Parameters": [], + "ReturnType": "System.Int64", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Match", + "Parameters": [ + { + "Name": "httpContext", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "route", + "Type": "Microsoft.AspNetCore.Routing.IRouter" + }, + { + "Name": "routeKey", + "Type": "System.String" + }, + { + "Name": "values", + "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary" + }, + { + "Name": "routeDirection", + "Type": "Microsoft.AspNetCore.Routing.RouteDirection" + } + ], + "ReturnType": "System.Boolean", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouteConstraint", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "min", + "Type": "System.Int64" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Routing.Constraints.OptionalRouteConstraint", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Routing.IRouteConstraint" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_InnerConstraint", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Routing.IRouteConstraint", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Match", + "Parameters": [ + { + "Name": "httpContext", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "route", + "Type": "Microsoft.AspNetCore.Routing.IRouter" + }, + { + "Name": "routeKey", + "Type": "System.String" + }, + { + "Name": "values", + "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary" + }, + { + "Name": "routeDirection", + "Type": "Microsoft.AspNetCore.Routing.RouteDirection" + } + ], + "ReturnType": "System.Boolean", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouteConstraint", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "innerConstraint", + "Type": "Microsoft.AspNetCore.Routing.IRouteConstraint" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Routing.Constraints.RangeRouteConstraint", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Routing.IRouteConstraint" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Min", + "Parameters": [], + "ReturnType": "System.Int64", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Max", + "Parameters": [], + "ReturnType": "System.Int64", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Match", + "Parameters": [ + { + "Name": "httpContext", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "route", + "Type": "Microsoft.AspNetCore.Routing.IRouter" + }, + { + "Name": "routeKey", + "Type": "System.String" + }, + { + "Name": "values", + "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary" + }, + { + "Name": "routeDirection", + "Type": "Microsoft.AspNetCore.Routing.RouteDirection" + } + ], + "ReturnType": "System.Boolean", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouteConstraint", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "min", + "Type": "System.Int64" + }, + { + "Name": "max", + "Type": "System.Int64" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Routing.Constraints.RegexInlineRouteConstraint", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Routing.Constraints.RegexRouteConstraint", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "regexPattern", + "Type": "System.String" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Routing.Constraints.RegexRouteConstraint", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Routing.IRouteConstraint" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Constraint", + "Parameters": [], + "ReturnType": "System.Text.RegularExpressions.Regex", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Match", + "Parameters": [ + { + "Name": "httpContext", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "route", + "Type": "Microsoft.AspNetCore.Routing.IRouter" + }, + { + "Name": "routeKey", + "Type": "System.String" + }, + { + "Name": "values", + "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary" + }, + { + "Name": "routeDirection", + "Type": "Microsoft.AspNetCore.Routing.RouteDirection" + } + ], + "ReturnType": "System.Boolean", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouteConstraint", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "regex", + "Type": "System.Text.RegularExpressions.Regex" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "regexPattern", + "Type": "System.String" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Routing.Constraints.RequiredRouteConstraint", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Routing.IRouteConstraint" + ], + "Members": [ + { + "Kind": "Method", + "Name": "Match", + "Parameters": [ + { + "Name": "httpContext", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "route", + "Type": "Microsoft.AspNetCore.Routing.IRouter" + }, + { + "Name": "routeKey", + "Type": "System.String" + }, + { + "Name": "values", + "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary" + }, + { + "Name": "routeDirection", + "Type": "Microsoft.AspNetCore.Routing.RouteDirection" + } + ], + "ReturnType": "System.Boolean", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouteConstraint", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Routing.Constraints.StringRouteConstraint", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Routing.IRouteConstraint" + ], + "Members": [ + { + "Kind": "Method", + "Name": "Match", + "Parameters": [ + { + "Name": "httpContext", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "route", + "Type": "Microsoft.AspNetCore.Routing.IRouter" + }, + { + "Name": "routeKey", + "Type": "System.String" + }, + { + "Name": "values", + "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary" + }, + { + "Name": "routeDirection", + "Type": "Microsoft.AspNetCore.Routing.RouteDirection" + } + ], + "ReturnType": "System.Boolean", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouteConstraint", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.Extensions.DependencyInjection.RoutingServiceCollectionExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "AddRouting", + "Parameters": [ + { + "Name": "services", + "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection" + } + ], + "ReturnType": "Microsoft.Extensions.DependencyInjection.IServiceCollection", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddRouting", + "Parameters": [ + { + "Name": "services", + "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.Extensions.DependencyInjection.IServiceCollection", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + } + ] +} \ No newline at end of file diff --git a/src/Routing/test/Directory.Build.props b/src/Routing/test/Directory.Build.props new file mode 100644 index 0000000000..02caa062ac --- /dev/null +++ b/src/Routing/test/Directory.Build.props @@ -0,0 +1,20 @@ + + + + + netcoreapp2.1 + $(DeveloperBuildTestTfms) + $(StandardTestTfms);netcoreapp2.0 + $(StandardTestTfms);net461 + + + + + + + + + + + + diff --git a/src/Routing/test/Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests/Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests.csproj b/src/Routing/test/Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests/Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests.csproj new file mode 100644 index 0000000000..fce9e366fc --- /dev/null +++ b/src/Routing/test/Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests/Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests.csproj @@ -0,0 +1,11 @@ + + + + $(StandardTestTfms) + + + + + + + diff --git a/src/Routing/test/Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests/RouteDataTest.cs b/src/Routing/test/Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests/RouteDataTest.cs new file mode 100644 index 0000000000..b8eac36c7e --- /dev/null +++ b/src/Routing/test/Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests/RouteDataTest.cs @@ -0,0 +1,157 @@ +// 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 Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Routing +{ + public class RouteDataTest + { + [Fact] + public void RouteData_DefaultPropertyValues() + { + // Arrange & Act + var routeData = new RouteData(); + + // Assert + Assert.Empty(routeData.DataTokens); + Assert.Empty(routeData.Routers); + Assert.Empty(routeData.Values); + } + + [Fact] + public void RouteData_CopyConstructor() + { + // Arrange & Act + var original = new RouteData(); + + original.DataTokens.Add("data", "token"); + original.Routers.Add(Mock.Of()); + original.Values.Add("route", "value"); + + var routeData = new RouteData(original); + + // Assert + Assert.NotSame(routeData.DataTokens, original.DataTokens); + Assert.Equal(routeData.DataTokens, original.DataTokens); + Assert.NotSame(routeData.Routers, original.Routers); + Assert.Equal(routeData.Routers, original.Routers); + Assert.NotSame(routeData.Values, original.Values); + Assert.Equal(routeData.Values, original.Values); + } + + [Fact] + public void RouteData_PushStateAndRestore_NullValues() + { + // Arrange + var routeData = new RouteData(); + + // Act + var snapshot = routeData.PushState(null, null, null); + var copy = new RouteData(routeData); + snapshot.Restore(); + + // Assert + Assert.Equal(routeData.DataTokens, copy.DataTokens); + Assert.Equal(routeData.Routers, copy.Routers); + Assert.Equal(routeData.Values, copy.Values); + } + + [Fact] + public void RouteData_PushStateAndRestore_EmptyValues() + { + // Arrange + var routeData = new RouteData(); + + // Act + var snapshot = routeData.PushState(null, new RouteValueDictionary(), new RouteValueDictionary()); + var copy = new RouteData(routeData); + snapshot.Restore(); + + // Assert + Assert.Equal(routeData.DataTokens, copy.DataTokens); + Assert.Equal(routeData.Routers, copy.Routers); + Assert.Equal(routeData.Values, copy.Values); + } + + // This is an important semantic for catchall parameters. A null route value shouldn't be + // merged. + [Fact] + public void RouteData_PushStateAndRestore_NullRouteValueNotSet() + { + // Arrange + var original = new RouteData(); + original.Values.Add("bleh", "16"); + + var routeData = new RouteData(original); + + // Act + var snapshot = routeData.PushState( + null, + new RouteValueDictionary(new { bleh = (string)null }), + new RouteValueDictionary()); + snapshot.Restore(); + + // Assert + Assert.Equal(routeData.Values, original.Values); + } + + [Fact] + public void RouteData_PushStateAndThenModify() + { + // Arrange + var routeData = new RouteData(); + + // Act + var snapshot = routeData.PushState(null, null, null); + routeData.DataTokens.Add("data", "token"); + routeData.Routers.Add(Mock.Of()); + routeData.Values.Add("route", "value"); + + var copy = new RouteData(routeData); + snapshot.Restore(); + + // Assert + Assert.Empty(routeData.DataTokens); + Assert.NotEqual(routeData.DataTokens, copy.DataTokens); + Assert.Empty(routeData.Routers); + Assert.NotEqual(routeData.Routers, copy.Routers); + Assert.Empty(routeData.Values); + Assert.NotEqual(routeData.Values, copy.Values); + } + + [Fact] + public void RouteData_PushStateAndThenModify_WithInitialData() + { + // Arrange + var original = new RouteData(); + original.DataTokens.Add("data", "token1"); + original.Routers.Add(Mock.Of()); + original.Values.Add("route", "value1"); + + var routeData = new RouteData(original); + + // Act + var snapshot = routeData.PushState( + Mock.Of(), + new RouteValueDictionary(new { route = "value2" }), + new RouteValueDictionary(new { data = "token2" })); + + routeData.DataTokens.Add("data2", "token"); + routeData.Routers.Add(Mock.Of()); + routeData.Values.Add("route2", "value"); + + var copy = new RouteData(routeData); + snapshot.Restore(); + + // Assert + Assert.Equal(original.DataTokens, routeData.DataTokens); + Assert.NotEqual(routeData.DataTokens, copy.DataTokens); + Assert.Equal(original.Routers, routeData.Routers); + Assert.NotEqual(routeData.Routers, copy.Routers); + Assert.Equal(original.Values, routeData.Values); + Assert.NotEqual(routeData.Values, copy.Values); + } + } +} diff --git a/src/Routing/test/Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests/RouteValueDictionaryTests.cs b/src/Routing/test/Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests/RouteValueDictionaryTests.cs new file mode 100644 index 0000000000..764f773e16 --- /dev/null +++ b/src/Routing/test/Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests/RouteValueDictionaryTests.cs @@ -0,0 +1,1572 @@ +// 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.Testing; +using Xunit; + +namespace Microsoft.AspNetCore.Routing.Tests +{ + public class RouteValueDictionaryTests + { + [Fact] + public void DefaultCtor_UsesEmptyStorage() + { + // Arrange + // Act + var dict = new RouteValueDictionary(); + + // Assert + Assert.Empty(dict); + Assert.IsType(dict._storage); + } + + [Fact] + public void CreateFromNull_UsesEmptyStorage() + { + // Arrange + // Act + var dict = new RouteValueDictionary(null); + + // Assert + Assert.Empty(dict); + Assert.IsType(dict._storage); + } + + [Fact] + public void CreateFromRouteValueDictionary_WithListStorage_CopiesStorage() + { + // Arrange + var other = new RouteValueDictionary() + { + { "1", 1 } + }; + + // Act + var dict = new RouteValueDictionary(other); + + // Assert + Assert.Equal(other, dict); + + var storage = Assert.IsType(dict._storage); + var otherStorage = Assert.IsType(other._storage); + Assert.NotSame(otherStorage, storage); + } + + [Fact] + public void CreateFromRouteValueDictionary_WithPropertyStorage_CopiesStorage() + { + // Arrange + var other = new RouteValueDictionary(new { key = "value" }); + + // Act + var dict = new RouteValueDictionary(other); + + // Assert + Assert.Equal(other, dict); + + var storage = Assert.IsType(dict._storage); + var otherStorage = Assert.IsType(other._storage); + Assert.Same(otherStorage, storage); + } + + [Fact] + public void CreateFromRouteValueDictionary_WithEmptyStorage_SharedInstance() + { + // Arrange + var other = new RouteValueDictionary(); + + // Act + var dict = new RouteValueDictionary(other); + + // Assert + Assert.Equal(other, dict); + + var storage = Assert.IsType(dict._storage); + var otherStorage = Assert.IsType(other._storage); + Assert.Same(otherStorage, storage); + } + + public static IEnumerable IEnumerableKeyValuePairData + { + get + { + var routeValues = new[] + { + new KeyValuePair("Name", "James"), + new KeyValuePair("Age", 30), + new KeyValuePair("Address", new Address() { City = "Redmond", State = "WA" }) + }; + + yield return new object[] { routeValues.ToDictionary(kvp => kvp.Key, kvp => kvp.Value) }; + + yield return new object[] { routeValues.ToList() }; + + yield return new object[] { routeValues }; + } + } + + public static IEnumerable IEnumerableStringValuePairData + { + get + { + var routeValues = new[] + { + new KeyValuePair("First Name", "James"), + new KeyValuePair("Last Name", "Henrik"), + new KeyValuePair("Middle Name", "Bob") + }; + + yield return new object[] { routeValues.ToDictionary(kvp => kvp.Key, kvp => kvp.Value) }; + + yield return new object[] { routeValues.ToList() }; + + yield return new object[] { routeValues }; + } + } + + [Theory] + [MemberData(nameof(IEnumerableKeyValuePairData))] + public void CreateFromIEnumerableKeyValuePair_CopiesValues(object values) + { + // Arrange & Act + var dict = new RouteValueDictionary(values); + + // Assert + Assert.IsType(dict._storage); + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => + { + Assert.Equal("Address", kvp.Key); + var address = Assert.IsType
(kvp.Value); + Assert.Equal("Redmond", address.City); + Assert.Equal("WA", address.State); + }, + kvp => { Assert.Equal("Age", kvp.Key); Assert.Equal(30, kvp.Value); }, + kvp => { Assert.Equal("Name", kvp.Key); Assert.Equal("James", kvp.Value); }); + } + + [Theory] + [MemberData(nameof(IEnumerableStringValuePairData))] + public void CreateFromIEnumerableStringValuePair_CopiesValues(object values) + { + // Arrange & Act + var dict = new RouteValueDictionary(values); + + // Assert + Assert.IsType(dict._storage); + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => { Assert.Equal("First Name", kvp.Key); Assert.Equal("James", kvp.Value); }, + kvp => { Assert.Equal("Last Name", kvp.Key); Assert.Equal("Henrik", kvp.Value); }, + kvp => { Assert.Equal("Middle Name", kvp.Key); Assert.Equal("Bob", kvp.Value); }); + } + + [Fact] + public void CreateFromIEnumerableKeyValuePair_ThrowsExceptionForDuplicateKey() + { + // Arrange + var values = new List>() + { + new KeyValuePair("name", "Billy"), + new KeyValuePair("Name", "Joey"), + }; + + // Act & Assert + ExceptionAssert.ThrowsArgument( + () => new RouteValueDictionary(values), + "values", + $"An element with the key 'Name' already exists in the {nameof(RouteValueDictionary)}."); + } + + [Fact] + public void CreateFromIEnumerableStringValuePair_ThrowsExceptionForDuplicateKey() + { + // Arrange + var values = new List>() + { + new KeyValuePair("name", "Billy"), + new KeyValuePair("Name", "Joey"), + }; + + // Act & Assert + ExceptionAssert.ThrowsArgument( + () => new RouteValueDictionary(values), + "values", + $"An element with the key 'Name' already exists in the {nameof(RouteValueDictionary)}."); + } + + [Fact] + public void CreateFromObject_CopiesPropertiesFromAnonymousType() + { + // Arrange + var obj = new { cool = "beans", awesome = 123 }; + + // Act + var dict = new RouteValueDictionary(obj); + + // Assert + Assert.IsType(dict._storage); + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => { Assert.Equal("awesome", kvp.Key); Assert.Equal(123, kvp.Value); }, + kvp => { Assert.Equal("cool", kvp.Key); Assert.Equal("beans", kvp.Value); }); + } + + [Fact] + public void CreateFromObject_CopiesPropertiesFromRegularType() + { + // Arrange + var obj = new RegularType() { CoolnessFactor = 73 }; + + // Act + var dict = new RouteValueDictionary(obj); + + // Assert + Assert.IsType(dict._storage); + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => + { + Assert.Equal("CoolnessFactor", kvp.Key); + Assert.Equal(73, kvp.Value); + }, + kvp => + { + Assert.Equal("IsAwesome", kvp.Key); + var value = Assert.IsType(kvp.Value); + Assert.False(value); + }); + } + + [Fact] + public void CreateFromObject_CopiesPropertiesFromRegularType_PublicOnly() + { + // Arrange + var obj = new Visibility() { IsPublic = true, ItsInternalDealWithIt = 5 }; + + // Act + var dict = new RouteValueDictionary(obj); + + // Assert + Assert.IsType(dict._storage); + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => + { + Assert.Equal("IsPublic", kvp.Key); + var value = Assert.IsType(kvp.Value); + Assert.True(value); + }); + } + + [Fact] + public void CreateFromObject_CopiesPropertiesFromRegularType_IgnoresStatic() + { + // Arrange + var obj = new StaticProperty(); + + // Act + var dict = new RouteValueDictionary(obj); + + // Assert + Assert.IsType(dict._storage); + Assert.Empty(dict); + } + + [Fact] + public void CreateFromObject_CopiesPropertiesFromRegularType_IgnoresSetOnly() + { + // Arrange + var obj = new SetterOnly() { CoolSetOnly = false }; + + // Act + var dict = new RouteValueDictionary(obj); + + // Assert + Assert.IsType(dict._storage); + Assert.Empty(dict); + } + + [Fact] + public void CreateFromObject_CopiesPropertiesFromRegularType_IncludesInherited() + { + // Arrange + var obj = new Derived() { TotallySweetProperty = true, DerivedProperty = false }; + + // Act + var dict = new RouteValueDictionary(obj); + + // Assert + Assert.IsType(dict._storage); + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => + { + Assert.Equal("DerivedProperty", kvp.Key); + var value = Assert.IsType(kvp.Value); + Assert.False(value); + }, + kvp => + { + Assert.Equal("TotallySweetProperty", kvp.Key); + var value = Assert.IsType(kvp.Value); + Assert.True(value); + }); + } + + [Fact] + public void CreateFromObject_CopiesPropertiesFromRegularType_WithHiddenProperty() + { + // Arrange + var obj = new DerivedHiddenProperty() { DerivedProperty = 5 }; + + // Act + var dict = new RouteValueDictionary(obj); + + // Assert + Assert.IsType(dict._storage); + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => { Assert.Equal("DerivedProperty", kvp.Key); Assert.Equal(5, kvp.Value); }); + } + + [Fact] + public void CreateFromObject_CopiesPropertiesFromRegularType_WithIndexerProperty() + { + // Arrange + var obj = new IndexerProperty(); + + // Act + var dict = new RouteValueDictionary(obj); + + // Assert + Assert.IsType(dict._storage); + Assert.Empty(dict); + } + + [Fact] + public void CreateFromObject_MixedCaseThrows() + { + // Arrange + var obj = new { controller = "Home", Controller = "Home" }; + + var message = + $"The type '{obj.GetType().FullName}' defines properties 'controller' and 'Controller' which differ " + + $"only by casing. This is not supported by {nameof(RouteValueDictionary)} which uses " + + $"case-insensitive comparisons."; + + // Act & Assert + var exception = Assert.Throws(() => + { + var dictionary = new RouteValueDictionary(obj); + }); + + // Ignoring case to make sure we're not testing reflection's ordering. + Assert.Equal(message, exception.Message, ignoreCase: true); + } + + // Our comparer is hardcoded to be OrdinalIgnoreCase no matter what. + [Fact] + public void Comparer_IsOrdinalIgnoreCase() + { + // Arrange + // Act + var dict = new RouteValueDictionary(); + + // Assert + Assert.Same(StringComparer.OrdinalIgnoreCase, dict.Comparer); + } + + // Our comparer is hardcoded to be IsReadOnly==false no matter what. + [Fact] + public void IsReadOnly_False() + { + // Arrange + var dict = new RouteValueDictionary(); + + // Act + var result = ((ICollection>)dict).IsReadOnly; + + // Assert + Assert.False(result); + } + + [Fact] + public void IndexGet_EmptyStorage_ReturnsNull() + { + // Arrange + var dict = new RouteValueDictionary(); + + // Act + var value = dict["key"]; + + // Assert + Assert.Null(value); + Assert.IsType(dict._storage); + } + + [Fact] + public void IndexGet_PropertyStorage_NoMatch_ReturnsNull() + { + // Arrange + var dict = new RouteValueDictionary(new { age = 30 }); + + // Act + var value = dict["key"]; + + // Assert + Assert.Null(value); + Assert.IsType(dict._storage); + } + + [Fact] + public void IndexGet_PropertyStorage_Match_ReturnsValue() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); + + // Act + var value = dict["key"]; + + // Assert + Assert.Equal("value", value); + Assert.IsType(dict._storage); + } + + [Fact] + public void IndexGet_PropertyStorage_MatchIgnoreCase_ReturnsValue() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); + + // Act + var value = dict["kEy"]; + + // Assert + Assert.Equal("value", value); + Assert.IsType(dict._storage); + } + + [Fact] + public void IndexGet_ListStorage_NoMatch_ReturnsNull() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "age", 30 }, + }; + + // Act + var value = dict["key"]; + + // Assert + Assert.Null(value); + Assert.IsType(dict._storage); + } + + [Fact] + public void IndexGet_ListStorage_Match_ReturnsValue() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + // Act + var value = dict["key"]; + + // Assert + Assert.Equal("value", value); + Assert.IsType(dict._storage); + } + + [Fact] + public void IndexGet_ListStorage_MatchIgnoreCase_ReturnsValue() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + // Act + var value = dict["kEy"]; + + // Assert + Assert.Equal("value", value); + Assert.IsType(dict._storage); + } + + [Fact] + public void IndexSet_EmptyStorage_UpgradesToList() + { + // Arrange + var dict = new RouteValueDictionary(); + + // Act + dict["key"] = "value"; + + // Assert + Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType(dict._storage); + } + + [Fact] + public void IndexSet_PropertyStorage_NoMatch_AddsValue() + { + // Arrange + var dict = new RouteValueDictionary(new { age = 30 }); + + // Act + dict["key"] = "value"; + + // Assert + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => { Assert.Equal("age", kvp.Key); Assert.Equal(30, kvp.Value); }, + kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType(dict._storage); + } + + [Fact] + public void IndexSet_PropertyStorage_Match_SetsValue() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); + + // Act + dict["key"] = "value"; + + // Assert + Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType(dict._storage); + } + + [Fact] + public void IndexSet_PropertyStorage_MatchIgnoreCase_SetsValue() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); + + // Act + dict["kEy"] = "value"; + + // Assert + Assert.Collection(dict, kvp => { Assert.Equal("kEy", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType(dict._storage); + } + + [Fact] + public void IndexSet_ListStorage_NoMatch_AddsValue() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "age", 30 }, + }; + + // Act + dict["key"] = "value"; + + // Assert + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => { Assert.Equal("age", kvp.Key); Assert.Equal(30, kvp.Value); }, + kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType(dict._storage); + } + + [Fact] + public void IndexSet_ListStorage_Match_SetsValue() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + // Act + dict["key"] = "value"; + + // Assert + Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType(dict._storage); + } + + [Fact] + public void IndexSet_ListStorage_MatchIgnoreCase_SetsValue() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + // Act + dict["key"] = "value"; + + // Assert + Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType(dict._storage); + } + + [Fact] + public void Count_EmptyStorage() + { + // Arrange + var dict = new RouteValueDictionary(); + + // Act + var count = dict.Count; + + // Assert + Assert.Equal(0, count); + Assert.IsType(dict._storage); + } + + [Fact] + public void Count_PropertyStorage() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value", }); + + // Act + var count = dict.Count; + + // Assert + Assert.Equal(1, count); + Assert.IsType(dict._storage); + } + + [Fact] + public void Count_ListStorage() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + // Act + var count = dict.Count; + + // Assert + Assert.Equal(1, count); + Assert.IsType(dict._storage); + } + + [Fact] + public void Keys_EmptyStorage() + { + // Arrange + var dict = new RouteValueDictionary(); + + // Act + var keys = dict.Keys; + + // Assert + Assert.Empty(keys); + Assert.IsType(dict._storage); + } + + [Fact] + public void Keys_PropertyStorage() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value", }); + + // Act + var keys = dict.Keys; + + // Assert + Assert.Equal(new[] { "key" }, keys); + Assert.IsType(dict._storage); + } + + [Fact] + public void Keys_ListStorage() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + // Act + var keys = dict.Keys; + + // Assert + Assert.Equal(new[] { "key" }, keys); + Assert.IsType(dict._storage); + } + + [Fact] + public void Values_EmptyStorage() + { + // Arrange + var dict = new RouteValueDictionary(); + + // Act + var values = dict.Values; + + // Assert + Assert.Empty(values); + Assert.IsType(dict._storage); + } + + [Fact] + public void Values_PropertyStorage() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value", }); + + // Act + var values = dict.Values; + + // Assert + Assert.Equal(new object[] { "value" }, values); + Assert.IsType(dict._storage); + } + + [Fact] + public void Values_ListStorage() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + // Act + var values = dict.Values; + + // Assert + Assert.Equal(new object[] { "value" }, values); + Assert.IsType(dict._storage); + } + + [Fact] + public void Add_EmptyStorage() + { + // Arrange + var dict = new RouteValueDictionary(); + + // Act + dict.Add("key", "value"); + + // Assert + Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType(dict._storage); + } + + [Fact] + public void Add_PropertyStorage() + { + // Arrange + var dict = new RouteValueDictionary(new { age = 30 }); + + // Act + dict.Add("key", "value"); + + // Assert + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => { Assert.Equal("age", kvp.Key); Assert.Equal(30, kvp.Value); }, + kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType(dict._storage); + } + + [Fact] + public void Add_ListStorage() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "age", 30 }, + }; + + // Act + dict.Add("key", "value"); + + // Assert + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => { Assert.Equal("age", kvp.Key); Assert.Equal(30, kvp.Value); }, + kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType(dict._storage); + } + + [Fact] + public void Add_DuplicateKey() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + var message = $"An element with the key 'key' already exists in the {nameof(RouteValueDictionary)}"; + + // Act & Assert + ExceptionAssert.ThrowsArgument(() => dict.Add("key", "value2"), "key", message); + + // Assert + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType(dict._storage); + } + + [Fact] + public void Add_DuplicateKey_CaseInsensitive() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + var message = $"An element with the key 'kEy' already exists in the {nameof(RouteValueDictionary)}"; + + // Act & Assert + ExceptionAssert.ThrowsArgument(() => dict.Add("kEy", "value2"), "key", message); + + // Assert + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType(dict._storage); + } + + [Fact] + public void Add_KeyValuePair() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "age", 30 }, + }; + + // Act + ((ICollection>)dict).Add(new KeyValuePair("key", "value")); + + // Assert + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => { Assert.Equal("age", kvp.Key); Assert.Equal(30, kvp.Value); }, + kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType(dict._storage); + } + + [Fact] + public void Clear_EmptyStorage() + { + // Arrange + var dict = new RouteValueDictionary(); + + // Act + dict.Clear(); + + // Assert + Assert.Empty(dict); + Assert.IsType(dict._storage); + } + + [Fact] + public void Clear_PropertyStorage_AlreadyEmpty() + { + // Arrange + var dict = new RouteValueDictionary(new { }); + + // Act + dict.Clear(); + + // Assert + Assert.Empty(dict); + Assert.IsType(dict._storage); + } + + [Fact] + public void Clear_PropertyStorage() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); + + // Act + dict.Clear(); + + // Assert + Assert.Empty(dict); + Assert.IsType(dict._storage); + } + + [Fact] + public void Clear_ListStorage() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + // Act + dict.Clear(); + + // Assert + Assert.Empty(dict); + Assert.IsType(dict._storage); + } + + [Fact] + public void Contains_KeyValuePair_True() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + var input = new KeyValuePair("key", "value"); + + // Act + var result = ((ICollection>)dict).Contains(input); + + // Assert + Assert.True(result); + Assert.IsType(dict._storage); + } + + [Fact] + public void Contains_KeyValuePair_True_CaseInsensitive() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + var input = new KeyValuePair("KEY", "value"); + + // Act + var result = ((ICollection>)dict).Contains(input); + + // Assert + Assert.True(result); + Assert.IsType(dict._storage); + } + + [Fact] + public void Contains_KeyValuePair_False() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + var input = new KeyValuePair("other", "value"); + + // Act + var result = ((ICollection>)dict).Contains(input); + + // Assert + Assert.False(result); + Assert.IsType(dict._storage); + } + + // Value comparisons use the default equality comparer. + [Fact] + public void Contains_KeyValuePair_False_ValueComparisonIsDefault() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + var input = new KeyValuePair("key", "valUE"); + + // Act + var result = ((ICollection>)dict).Contains(input); + + // Assert + Assert.False(result); + Assert.IsType(dict._storage); + } + + [Fact] + public void ContainsKey_EmptyStorage() + { + // Arrange + var dict = new RouteValueDictionary(); + + // Act + var result = dict.ContainsKey("key"); + + // Assert + Assert.False(result); + Assert.IsType(dict._storage); + } + + [Fact] + public void ContainsKey_PropertyStorage_False() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); + + // Act + var result = dict.ContainsKey("other"); + + // Assert + Assert.False(result); + Assert.IsType(dict._storage); + } + + [Fact] + public void ContainsKey_PropertyStorage_True() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); + + // Act + var result = dict.ContainsKey("key"); + + // Assert + Assert.True(result); + Assert.IsType(dict._storage); + } + + [Fact] + public void ContainsKey_PropertyStorage_True_CaseInsensitive() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); + + // Act + var result = dict.ContainsKey("kEy"); + + // Assert + Assert.True(result); + Assert.IsType(dict._storage); + } + + [Fact] + public void ContainsKey_ListStorage_False() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + // Act + var result = dict.ContainsKey("other"); + + // Assert + Assert.False(result); + Assert.IsType(dict._storage); + } + + [Fact] + public void ContainsKey_ListStorage_True() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + // Act + var result = dict.ContainsKey("key"); + + // Assert + Assert.True(result); + Assert.IsType(dict._storage); + } + + [Fact] + public void ContainsKey_ListStorage_True_CaseInsensitive() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + // Act + var result = dict.ContainsKey("kEy"); + + // Assert + Assert.True(result); + Assert.IsType(dict._storage); + } + + [Fact] + public void CopyTo() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + var array = new KeyValuePair[2]; + + // Act + ((ICollection>)dict).CopyTo(array, 1); + + // Assert + Assert.Equal( + new KeyValuePair[] + { + default(KeyValuePair), + new KeyValuePair("key", "value") + }, + array); + Assert.IsType(dict._storage); + } + + [Fact] + public void Remove_KeyValuePair_True() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + var input = new KeyValuePair("key", "value"); + + // Act + var result = ((ICollection>)dict).Remove(input); + + // Assert + Assert.True(result); + Assert.Empty(dict); + Assert.IsType(dict._storage); + } + + [Fact] + public void Remove_KeyValuePair_True_CaseInsensitive() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + var input = new KeyValuePair("KEY", "value"); + + // Act + var result = ((ICollection>)dict).Remove(input); + + // Assert + Assert.True(result); + Assert.Empty(dict); + Assert.IsType(dict._storage); + } + + [Fact] + public void Remove_KeyValuePair_False() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + var input = new KeyValuePair("other", "value"); + + // Act + var result = ((ICollection>)dict).Remove(input); + + // Assert + Assert.False(result); + Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType(dict._storage); + } + + // Value comparisons use the default equality comparer. + [Fact] + public void Remove_KeyValuePair_False_ValueComparisonIsDefault() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + var input = new KeyValuePair("key", "valUE"); + + // Act + var result = ((ICollection>)dict).Remove(input); + + // Assert + Assert.False(result); + Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType(dict._storage); + } + + [Fact] + public void Remove_EmptyStorage() + { + // Arrange + var dict = new RouteValueDictionary(); + + // Act + var result = dict.Remove("key"); + + // Assert + Assert.False(result); + Assert.IsType(dict._storage); + } + + [Fact] + public void Remove_PropertyStorage_Empty() + { + // Arrange + var dict = new RouteValueDictionary(new { }); + + // Act + var result = dict.Remove("other"); + + // Assert + Assert.False(result); + Assert.Empty(dict); + Assert.IsType(dict._storage); + } + + [Fact] + public void Remove_PropertyStorage_False() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); + + // Act + var result = dict.Remove("other"); + + // Assert + Assert.False(result); + Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType(dict._storage); + } + + [Fact] + public void Remove_PropertyStorage_True() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); + + // Act + var result = dict.Remove("key"); + + // Assert + Assert.True(result); + Assert.Empty(dict); + Assert.IsType(dict._storage); + } + + [Fact] + public void Remove_PropertyStorage_True_CaseInsensitive() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); + + // Act + var result = dict.Remove("kEy"); + + // Assert + Assert.True(result); + Assert.Empty(dict); + Assert.IsType(dict._storage); + } + + [Fact] + public void Remove_ListStorage_False() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + // Act + var result = dict.Remove("other"); + + // Assert + Assert.False(result); + Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType(dict._storage); + } + + [Fact] + public void Remove_ListStorage_True() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + // Act + var result = dict.Remove("key"); + + // Assert + Assert.True(result); + Assert.Empty(dict); + Assert.IsType(dict._storage); + } + + [Fact] + public void Remove_ListStorage_True_CaseInsensitive() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + // Act + var result = dict.Remove("kEy"); + + // Assert + Assert.True(result); + Assert.Empty(dict); + Assert.IsType(dict._storage); + } + + [Fact] + public void TryGetValue_EmptyStorage() + { + // Arrange + var dict = new RouteValueDictionary(); + + // Act + object value; + var result = dict.TryGetValue("key", out value); + + // Assert + Assert.False(result); + Assert.Null(value); + Assert.IsType(dict._storage); + } + + [Fact] + public void TryGetValue_PropertyStorage_False() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); + + // Act + object value; + var result = dict.TryGetValue("other", out value); + + // Assert + Assert.False(result); + Assert.Null(value); + Assert.IsType(dict._storage); + } + + [Fact] + public void TryGetValue_PropertyStorage_True() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); + + // Act + object value; + var result = dict.TryGetValue("key", out value); + + // Assert + Assert.True(result); + Assert.Equal("value", value); + Assert.IsType(dict._storage); + } + + [Fact] + public void TryGetValue_PropertyStorage_True_CaseInsensitive() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); + + // Act + object value; + var result = dict.TryGetValue("kEy", out value); + + // Assert + Assert.True(result); + Assert.Equal("value", value); + Assert.IsType(dict._storage); + } + + [Fact] + public void TryGetValue_ListStorage_False() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + // Act + object value; + var result = dict.TryGetValue("other", out value); + + // Assert + Assert.False(result); + Assert.Null(value); + Assert.IsType(dict._storage); + } + + [Fact] + public void TryGetValue_ListStorage_True() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + // Act + object value; + var result = dict.TryGetValue("key", out value); + + // Assert + Assert.True(result); + Assert.Equal("value", value); + Assert.IsType(dict._storage); + } + + [Fact] + public void TryGetValue_ListStorage_True_CaseInsensitive() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + // Act + object value; + var result = dict.TryGetValue("kEy", out value); + + // Assert + Assert.True(result); + Assert.Equal("value", value); + Assert.IsType(dict._storage); + } + + [Fact] + public void ListStorage_DynamicallyAdjustsCapacity() + { + // Arrange + var dict = new RouteValueDictionary(); + + // Act 1 + dict.Add("key", "value"); + + // Assert 1 + var storage = Assert.IsType(dict._storage); + Assert.Equal(4, storage.Capacity); + + // Act 2 + dict.Add("key2", "value2"); + dict.Add("key3", "value3"); + dict.Add("key4", "value4"); + dict.Add("key5", "value5"); + + // Assert 2 + Assert.Equal(8, storage.Capacity); + } + + [Fact] + public void ListStorage_RemoveAt_RearrangesInnerArray() + { + // Arrange + var dict = new RouteValueDictionary(); + dict.Add("key", "value"); + dict.Add("key2", "value2"); + dict.Add("key3", "value3"); + + // Assert 1 + var storage = Assert.IsType(dict._storage); + Assert.Equal(3, storage.Count); + + // Act + dict.Remove("key2"); + + // Assert 2 + Assert.Equal(2, storage.Count); + Assert.Equal("key", storage[0].Key); + Assert.Equal("value", storage[0].Value); + Assert.Equal("key3", storage[1].Key); + Assert.Equal("value3", storage[1].Value); + + Assert.Throws(() => storage[2]); + } + + private class RegularType + { + public bool IsAwesome { get; set; } + + public int CoolnessFactor { get; set; } + } + + private class Visibility + { + private string PrivateYo { get; set; } + + internal int ItsInternalDealWithIt { get; set; } + + public bool IsPublic { get; set; } + } + + private class StaticProperty + { + public static bool IsStatic { get; set; } + } + + private class SetterOnly + { + private bool _coolSetOnly; + + public bool CoolSetOnly { set { _coolSetOnly = value; } } + } + + private class Base + { + public bool DerivedProperty { get; set; } + } + + private class Derived : Base + { + public bool TotallySweetProperty { get; set; } + } + + private class DerivedHiddenProperty : Base + { + public new int DerivedProperty { get; set; } + } + + private class IndexerProperty + { + public bool this[string key] + { + get { return false; } + set { } + } + } + + private class Address + { + public string City { get; set; } + + public string State { get; set; } + } + } +} diff --git a/src/Routing/test/Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests/VirtualPathDataTests.cs b/src/Routing/test/Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests/VirtualPathDataTests.cs new file mode 100644 index 0000000000..c21de9ba32 --- /dev/null +++ b/src/Routing/test/Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests/VirtualPathDataTests.cs @@ -0,0 +1,65 @@ +// 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 Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Routing +{ + public class VirtualPathDataTests + { + [Fact] + public void Constructor_CreatesEmptyDataTokensIfNull() + { + // Arrange + var router = Mock.Of(); + var path = "/virtual path"; + + // Act + var pathData = new VirtualPathData(router, path, null); + + // Assert + Assert.Same(router, pathData.Router); + Assert.Equal(path, pathData.VirtualPath); + Assert.NotNull(pathData.DataTokens); + Assert.Empty(pathData.DataTokens); + } + + [Fact] + public void Constructor_CopiesDataTokens() + { + // Arrange + var router = Mock.Of(); + var path = "/virtual path"; + var dataTokens = new RouteValueDictionary(); + dataTokens["TestKey"] = "TestValue"; + + // Act + var pathData = new VirtualPathData(router, path, dataTokens); + + // Assert + Assert.Same(router, pathData.Router); + Assert.Equal(path, pathData.VirtualPath); + Assert.NotNull(pathData.DataTokens); + Assert.Equal("TestValue", pathData.DataTokens["TestKey"]); + Assert.Single(pathData.DataTokens); + Assert.NotSame(dataTokens, pathData.DataTokens); + } + + [Fact] + public void VirtualPath_ReturnsEmptyStringIfNull() + { + // Arrange + var router = Mock.Of(); + + // Act + var pathData = new VirtualPathData(router, virtualPath: null); + + // Assert + Assert.Same(router, pathData.Router); + Assert.Empty(pathData.VirtualPath); + Assert.NotNull(pathData.DataTokens); + Assert.Empty(pathData.DataTokens); + } + } +} diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.DecisionTree.Sources.Tests/DecisionTreeBuilderTest.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.DecisionTree.Sources.Tests/DecisionTreeBuilderTest.cs new file mode 100644 index 0000000000..5e28fd8917 --- /dev/null +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.DecisionTree.Sources.Tests/DecisionTreeBuilderTest.cs @@ -0,0 +1,224 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Xunit; + +namespace Microsoft.AspNetCore.Routing.DecisionTree +{ + public class DecisionTreeBuilderTest + { + [Fact] + public void BuildTree_Empty() + { + // Arrange + var items = new List(); + + // Act + var tree = DecisionTreeBuilder.GenerateTree(items, new ItemClassifier()); + + // Assert + Assert.Empty(tree.Criteria); + Assert.Empty(tree.Matches); + } + + [Fact] + public void BuildTree_TrivialMatch() + { + // Arrange + var items = new List(); + + var item = new Item(); + items.Add(item); + + // Act + var tree = DecisionTreeBuilder.GenerateTree(items, new ItemClassifier()); + + // Assert + Assert.Empty(tree.Criteria); + Assert.Same(item, Assert.Single(tree.Matches)); + } + + [Fact] + public void BuildTree_WithMultipleCriteria() + { + // Arrange + var items = new List(); + + var item = new Item(); + item.Criteria.Add("area", new DecisionCriterionValue(value: "Admin")); + item.Criteria.Add("controller", new DecisionCriterionValue(value: "Users")); + item.Criteria.Add("action", new DecisionCriterionValue(value: "AddUser")); + items.Add(item); + + // Act + var tree = DecisionTreeBuilder.GenerateTree(items, new ItemClassifier()); + + // Assert + Assert.Empty(tree.Matches); + + var area = Assert.Single(tree.Criteria); + Assert.Equal("area", area.Key); + + var admin = Assert.Single(area.Branches); + Assert.Equal("Admin", admin.Key); + Assert.Empty(admin.Value.Matches); + + var controller = Assert.Single(admin.Value.Criteria); + Assert.Equal("controller", controller.Key); + + var users = Assert.Single(controller.Branches); + Assert.Equal("Users", users.Key); + Assert.Empty(users.Value.Matches); + + var action = Assert.Single(users.Value.Criteria); + Assert.Equal("action", action.Key); + + var addUser = Assert.Single(action.Branches); + Assert.Equal("AddUser", addUser.Key); + Assert.Empty(addUser.Value.Criteria); + Assert.Same(item, Assert.Single(addUser.Value.Matches)); + } + + [Fact] + public void BuildTree_WithMultipleItems() + { + // Arrange + var items = new List(); + + var item1 = new Item(); + item1.Criteria.Add("controller", new DecisionCriterionValue(value: "Store")); + item1.Criteria.Add("action", new DecisionCriterionValue(value: "Buy")); + items.Add(item1); + + var item2 = new Item(); + item2.Criteria.Add("controller", new DecisionCriterionValue(value: "Store")); + item2.Criteria.Add("action", new DecisionCriterionValue(value: "Checkout")); + items.Add(item2); + + // Act + var tree = DecisionTreeBuilder.GenerateTree(items, new ItemClassifier()); + + // Assert + Assert.Empty(tree.Matches); + + var action = Assert.Single(tree.Criteria); + Assert.Equal("action", action.Key); + + var buy = action.Branches["Buy"]; + Assert.Empty(buy.Matches); + + var controller = Assert.Single(buy.Criteria); + Assert.Equal("controller", controller.Key); + + var store = Assert.Single(controller.Branches); + Assert.Equal("Store", store.Key); + Assert.Empty(store.Value.Criteria); + Assert.Same(item1, Assert.Single(store.Value.Matches)); + + var checkout = action.Branches["Checkout"]; + Assert.Empty(checkout.Matches); + + controller = Assert.Single(checkout.Criteria); + Assert.Equal("controller", controller.Key); + + store = Assert.Single(controller.Branches); + Assert.Equal("Store", store.Key); + Assert.Empty(store.Value.Criteria); + Assert.Same(item2, Assert.Single(store.Value.Matches)); + } + + [Fact] + public void BuildTree_WithInteriorMatch() + { + // Arrange + var items = new List(); + + var item1 = new Item(); + item1.Criteria.Add("controller", new DecisionCriterionValue(value: "Store")); + item1.Criteria.Add("action", new DecisionCriterionValue(value: "Buy")); + items.Add(item1); + + var item2 = new Item(); + item2.Criteria.Add("controller", new DecisionCriterionValue(value: "Store")); + item2.Criteria.Add("action", new DecisionCriterionValue(value: "Checkout")); + items.Add(item2); + + var item3 = new Item(); + item3.Criteria.Add("action", new DecisionCriterionValue(value: "Buy")); + items.Add(item3); + + // Act + var tree = DecisionTreeBuilder.GenerateTree(items, new ItemClassifier()); + + // Assert + Assert.Empty(tree.Matches); + + var action = Assert.Single(tree.Criteria); + Assert.Equal("action", action.Key); + + var buy = action.Branches["Buy"]; + Assert.Same(item3, Assert.Single(buy.Matches)); + } + + [Fact] + public void BuildTree_WithDivergentCriteria() + { + // Arrange + var items = new List(); + + var item1 = new Item(); + item1.Criteria.Add("controller", new DecisionCriterionValue(value: "Store")); + item1.Criteria.Add("action", new DecisionCriterionValue(value: "Buy")); + items.Add(item1); + + var item2 = new Item(); + item2.Criteria.Add("controller", new DecisionCriterionValue(value: "Store")); + item2.Criteria.Add("action", new DecisionCriterionValue(value: "Checkout")); + items.Add(item2); + + var item3 = new Item(); + item3.Criteria.Add("stub", new DecisionCriterionValue(value: "Bleh")); + items.Add(item3); + + // Act + var tree = DecisionTreeBuilder.GenerateTree(items, new ItemClassifier()); + + // Assert + Assert.Empty(tree.Matches); + + var action = tree.Criteria[0]; + Assert.Equal("action", action.Key); + + var stub = tree.Criteria[1]; + Assert.Equal("stub", stub.Key); + } + + private class Item + { + public Item() + { + Criteria = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + public Dictionary Criteria { get; private set; } + } + + private class ItemClassifier : IClassifier + { + public IEqualityComparer ValueComparer + { + get + { + return new RouteValueEqualityComparer(); + } + } + + public IDictionary GetCriteria(Item item) + { + return item.Criteria; + } + } + } +} \ No newline at end of file diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.DecisionTree.Sources.Tests/Microsoft.AspNetCore.Routing.DecisionTree.Sources.Tests.csproj b/src/Routing/test/Microsoft.AspNetCore.Routing.DecisionTree.Sources.Tests/Microsoft.AspNetCore.Routing.DecisionTree.Sources.Tests.csproj new file mode 100644 index 0000000000..3d127f0370 --- /dev/null +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.DecisionTree.Sources.Tests/Microsoft.AspNetCore.Routing.DecisionTree.Sources.Tests.csproj @@ -0,0 +1,15 @@ + + + + $(StandardTestTfms) + + + + + + + + + + + diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.FunctionalTests/Microsoft.AspNetCore.Routing.FunctionalTests.csproj b/src/Routing/test/Microsoft.AspNetCore.Routing.FunctionalTests/Microsoft.AspNetCore.Routing.FunctionalTests.csproj new file mode 100644 index 0000000000..ee8a57c08f --- /dev/null +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.FunctionalTests/Microsoft.AspNetCore.Routing.FunctionalTests.csproj @@ -0,0 +1,16 @@ + + + + $(StandardTestTfms) + + + + + + + + + + + + diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.FunctionalTests/RoutingSampleTest.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.FunctionalTests/RoutingSampleTest.cs new file mode 100644 index 0000000000..8869103f40 --- /dev/null +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.FunctionalTests/RoutingSampleTest.cs @@ -0,0 +1,84 @@ +// 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.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.TestHost; +using Xunit; + +namespace Microsoft.AspNetCore.Routing.FunctionalTests +{ + public class RoutingSampleTest : IDisposable + { + private readonly HttpClient _client; + private readonly TestServer _testServer; + + public RoutingSampleTest() + { + var webHostBuilder = RoutingSample.Web.Program.GetWebHostBuilder(); + _testServer = new TestServer(webHostBuilder); + _client = _testServer.CreateClient(); + _client.BaseAddress = new Uri("http://localhost"); + } + + [Fact] + public async Task Routing_CanRouteRequestDelegate_ToSpecificHttpVerb() + { + // Arrange + var message = new HttpRequestMessage(HttpMethod.Get, "api/get/5"); + + // Act + var response = await _client.SendAsync(message); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal($"API Get 5", await response.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task Routing_CanRouteRequest_ToSpecificMiddleware() + { + // Arrange + var message = new HttpRequestMessage(HttpMethod.Get, "api/middleware"); + + // Act + var response = await _client.SendAsync(message); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal($"Middleware!", await response.Content.ReadAsStringAsync()); + } + + [Theory] + [InlineData("GET")] + [InlineData("POST")] + [InlineData("PUT")] + [InlineData("PATCH")] + [InlineData("DELETE")] + [InlineData("HEAD")] + [InlineData("OPTIONS")] + public async Task Routing_CanRouteRequest_ToDefaultHandler(string httpVerb) + { + // Arrange + var message = new HttpRequestMessage(new HttpMethod(httpVerb), "api/all/Joe/Duf"); + var expectedBody = $"Verb = {httpVerb} - Path = /api/all/Joe/Duf - Route values - [name, Joe], [lastName, Duf]"; + + // Act + var response = await _client.SendAsync(message); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + Assert.Equal(expectedBody, body); + } + + public void Dispose() + { + _testServer.Dispose(); + _client.Dispose(); + } + } +} diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.FunctionalTests/RoutingTestFixture.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.FunctionalTests/RoutingTestFixture.cs new file mode 100644 index 0000000000..1ced141956 --- /dev/null +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.FunctionalTests/RoutingTestFixture.cs @@ -0,0 +1,34 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Net.Http; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; + +namespace Microsoft.AspNetCore.Routing.FunctionalTests +{ + public class RoutingTestFixture : IDisposable + { + private readonly TestServer _server; + + public RoutingTestFixture() + { + var builder = new WebHostBuilder() + .UseStartup(typeof(TStartup)); + + _server = new TestServer(builder); + + Client = _server.CreateClient(); + Client.BaseAddress = new Uri("http://localhost"); + } + + public HttpClient Client { get; } + + public void Dispose() + { + Client.Dispose(); + _server.Dispose(); + } + } +} diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.FunctionalTests/WebHostBuilderExtensionsTest.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.FunctionalTests/WebHostBuilderExtensionsTest.cs new file mode 100644 index 0000000000..7466006026 --- /dev/null +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.FunctionalTests/WebHostBuilderExtensionsTest.cs @@ -0,0 +1,101 @@ +// 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.IO; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.AspNetCore.Routing.FunctionalTests +{ + public class WebHostBuilderExtensionsTest + { + public static TheoryData, HttpRequestMessage, string> MatchesRequest + { + get + { + return new TheoryData, HttpRequestMessage, string>() + { + { + (rb) => rb.MapGet("greeting/{name}", (req, resp, routeData) => resp.WriteAsync($"Hello! {routeData.Values["name"]}")), + new HttpRequestMessage(HttpMethod.Get, "greeting/James"), + "Hello! James" + }, + { + (rb) => rb.MapPost( + "greeting/{name}", + async (req, resp, routeData) => + { + var streamReader = new StreamReader(req.Body); + var data = await streamReader.ReadToEndAsync(); + await resp.WriteAsync($"{routeData.Values["name"]} {data}"); + }), + new HttpRequestMessage(HttpMethod.Post, "greeting/James") { Content = new StringContent("Biography") }, + "James Biography" + }, + { + (rb) => rb.MapPut( + "greeting/{name}", + async (req, resp, routeData) => + { + var streamReader = new StreamReader(req.Body); + var data = await streamReader.ReadToEndAsync(); + await resp.WriteAsync($"{routeData.Values["name"]} {data}"); + }), + new HttpRequestMessage(HttpMethod.Put, "greeting/James") { Content = new StringContent("Biography") }, + "James Biography" + }, + { + (rb) => rb.MapDelete("greeting/{name}", (req, resp, routeData) => resp.WriteAsync($"Hello! {routeData.Values["name"]}")), + new HttpRequestMessage(HttpMethod.Delete, "greeting/James"), + "Hello! James" + }, + { + (rb) => rb.MapVerb( + "POST", + "greeting/{name}", + async (req, resp, routeData) => + { + var streamReader = new StreamReader(req.Body); + var data = await streamReader.ReadToEndAsync(); + await resp.WriteAsync($"{routeData.Values["name"]} {data}"); + }), + new HttpRequestMessage(HttpMethod.Post, "greeting/James") { Content = new StringContent("Biography") }, + "James Biography" + }, + }; + } + } + + [Theory] + [MemberData(nameof(MatchesRequest))] + public async Task UseRouter_MapGet_MatchesRequest(Action routeBuilder, HttpRequestMessage request, string expected) + { + // Arrange + var webhostbuilder = new WebHostBuilder(); + webhostbuilder + .ConfigureServices(services => services.AddRouting()) + .Configure(app => + { + app.UseRouter(routeBuilder); + }); + var testServer = new TestServer(webhostbuilder); + var client = testServer.CreateClient(); + + // Act + var response = await client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var actual = await response.Content.ReadAsStringAsync(); + Assert.Equal(expected, actual); + } + } +} diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/BuilderExtensionsTest.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/BuilderExtensionsTest.cs new file mode 100644 index 0000000000..ca297f89d2 --- /dev/null +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/BuilderExtensionsTest.cs @@ -0,0 +1,35 @@ +// 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; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Builder +{ + public class BuilderExtensionsTest + { + [Fact] + public void UseRouter_ThrowsInvalidOperationException_IfRoutingMarkerServiceIsNotRegistered() + { + // Arrange + var applicationBuilderMock = new Mock(); + applicationBuilderMock + .Setup(s => s.ApplicationServices) + .Returns(Mock.Of()); + + var router = Mock.Of(); + + // Act & Assert + var exception = Assert.Throws( + () => applicationBuilderMock.Object.UseRouter(router)); + + Assert.Equal( + "Unable to find the required services. Please add all the required services by calling " + + "'IServiceCollection.AddRouting' inside the call to 'ConfigureServices(...)'" + + " in the application startup code.", + exception.Message); + } + } +} diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/ConstraintMatcherTest.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/ConstraintMatcherTest.cs new file mode 100644 index 0000000000..4d9b436e81 --- /dev/null +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/ConstraintMatcherTest.cs @@ -0,0 +1,252 @@ +// 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.Http; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Logging.Testing; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Routing +{ + public class ConstraintMatcherTest + { + private const string _name = "name"; + + [Fact] + public void MatchUrlGeneration_DoesNotLogData() + { + // Arrange + var sink = new TestSink(); + var logger = new TestLogger(_name, sink, enabled: true); + + var routeValueDictionary = new RouteValueDictionary(new { a = "value", b = "value" }); + var constraints = new Dictionary + { + {"a", new PassConstraint()}, + {"b", new FailConstraint()} + }; + + // Act + RouteConstraintMatcher.Match( + constraints: constraints, + routeValues: routeValueDictionary, + httpContext: new Mock().Object, + route: new Mock().Object, + routeDirection: RouteDirection.UrlGeneration, + logger: logger); + + // Assert + // There are no BeginScopes called. + Assert.Empty(sink.Scopes); + + // There are no WriteCores called. + Assert.Empty(sink.Writes); + } + + [Fact] + public void MatchFail_LogsCorrectData() + { + // Arrange & Act + var constraints = new Dictionary + { + {"a", new PassConstraint()}, + {"b", new FailConstraint()} + }; + var sink = SetUpMatch(constraints, loggerEnabled: true); + var expectedMessage = "Route value 'value' with key 'b' did not match the constraint " + + $"'{typeof(FailConstraint).FullName}'."; + + // Assert + Assert.Empty(sink.Scopes); + var write = Assert.Single(sink.Writes); + Assert.Equal(expectedMessage, write.State?.ToString()); + } + + [Fact] + public void MatchSuccess_DoesNotLog() + { + // Arrange & Act + var constraints = new Dictionary + { + {"a", new PassConstraint()}, + {"b", new PassConstraint()} + }; + var sink = SetUpMatch(constraints, false); + + // Assert + Assert.Empty(sink.Scopes); + Assert.Empty(sink.Writes); + } + + [Fact] + public void ReturnsTrueOnValidConstraints() + { + var constraints = new Dictionary + { + {"a", new PassConstraint()}, + {"b", new PassConstraint()} + }; + + var routeValueDictionary = new RouteValueDictionary(new { a = "value", b = "value" }); + + Assert.True(RouteConstraintMatcher.Match( + constraints: constraints, + routeValues: routeValueDictionary, + httpContext: new Mock().Object, + route: new Mock().Object, + routeDirection: RouteDirection.IncomingRequest, + logger: NullLogger.Instance)); + } + + [Fact] + public void ConstraintsGetTheRightKey() + { + var constraints = new Dictionary + { + {"a", new PassConstraint("a")}, + {"b", new PassConstraint("b")} + }; + + var routeValueDictionary = new RouteValueDictionary(new { a = "value", b = "value" }); + + Assert.True(RouteConstraintMatcher.Match( + constraints: constraints, + routeValues: routeValueDictionary, + httpContext: new Mock().Object, + route: new Mock().Object, + routeDirection: RouteDirection.IncomingRequest, + logger: NullLogger.Instance)); + } + + [Fact] + public void ReturnsFalseOnInvalidConstraintsThatDontMatch() + { + var constraints = new Dictionary + { + {"a", new FailConstraint()}, + {"b", new FailConstraint()} + }; + + var routeValueDictionary = new RouteValueDictionary(new { c = "value", d = "value" }); + + Assert.False(RouteConstraintMatcher.Match( + constraints: constraints, + routeValues: routeValueDictionary, + httpContext: new Mock().Object, + route: new Mock().Object, + routeDirection: RouteDirection.IncomingRequest, + logger: NullLogger.Instance)); + } + + [Fact] + public void ReturnsFalseOnInvalidConstraintsThatMatch() + { + var constraints = new Dictionary + { + {"a", new FailConstraint()}, + {"b", new FailConstraint()} + }; + + var routeValueDictionary = new RouteValueDictionary(new { a = "value", b = "value" }); + + Assert.False(RouteConstraintMatcher.Match( + constraints: constraints, + routeValues: routeValueDictionary, + httpContext: new Mock().Object, + route: new Mock().Object, + routeDirection: RouteDirection.IncomingRequest, + logger: NullLogger.Instance)); + } + + [Fact] + public void ReturnsFalseOnValidAndInvalidConstraintsMixThatMatch() + { + var constraints = new Dictionary + { + {"a", new PassConstraint()}, + {"b", new FailConstraint()} + }; + + var routeValueDictionary = new RouteValueDictionary(new { a = "value", b = "value" }); + + Assert.False(RouteConstraintMatcher.Match( + constraints: constraints, + routeValues: routeValueDictionary, + httpContext: new Mock().Object, + route: new Mock().Object, + routeDirection: RouteDirection.IncomingRequest, + logger: NullLogger.Instance)); + } + + [Fact] + public void ReturnsTrueOnNullInput() + { + Assert.True(RouteConstraintMatcher.Match( + constraints: null, + routeValues: new RouteValueDictionary(), + httpContext: new Mock().Object, + route: new Mock().Object, + routeDirection: RouteDirection.IncomingRequest, + logger: NullLogger.Instance)); + } + + private TestSink SetUpMatch(Dictionary constraints, bool loggerEnabled) + { + // Arrange + var sink = new TestSink(); + var logger = new TestLogger(_name, sink, loggerEnabled); + + var routeValueDictionary = new RouteValueDictionary(new { a = "value", b = "value" }); + + // Act + RouteConstraintMatcher.Match( + constraints: constraints, + routeValues: routeValueDictionary, + httpContext: new Mock().Object, + route: new Mock().Object, + routeDirection: RouteDirection.IncomingRequest, + logger: logger); + return sink; + } + + private class PassConstraint : IRouteConstraint + { + private readonly string _expectedKey; + + public PassConstraint(string expectedKey = null) + { + _expectedKey = expectedKey; + } + + public bool Match( + HttpContext httpContext, + IRouter route, + string routeKey, + RouteValueDictionary values, + RouteDirection routeDirection) + { + if (_expectedKey != null) + { + Assert.Equal(_expectedKey, routeKey); + } + + return true; + } + } + + private class FailConstraint : IRouteConstraint + { + public bool Match( + HttpContext httpContext, + IRouter route, + string routeKey, + RouteValueDictionary values, + RouteDirection routeDirection) + { + return false; + } + } + } +} \ No newline at end of file diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/AlphaRouteConstraintTests.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/AlphaRouteConstraintTests.cs new file mode 100644 index 0000000000..2ba9ef5c27 --- /dev/null +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/AlphaRouteConstraintTests.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 Microsoft.AspNetCore.Routing.Constraints; +using Xunit; + +namespace Microsoft.AspNetCore.Routing.Tests +{ + public class AlphaRouteConstraintTests + { + [Theory] + [InlineData("alpha", true)] + [InlineData("a1pha", false)] + [InlineData("ALPHA", true)] + [InlineData("A1PHA", false)] + [InlineData("alPHA", true)] + [InlineData("A1pHA", false)] + [InlineData("AlpHAâ•¥", false)] + [InlineData("", true)] + public void AlphaRouteConstraintTest(string parameterValue, bool expected) + { + // Arrange + var constraint = new AlphaRouteConstraint(); + + // Act + var actual = ConstraintsTestHelper.TestConstraint(constraint, parameterValue); + + // Assert + Assert.Equal(expected, actual); + } + } +} diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/BoolRouteConstraintTests.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/BoolRouteConstraintTests.cs new file mode 100644 index 0000000000..32a2aa595e --- /dev/null +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/BoolRouteConstraintTests.cs @@ -0,0 +1,40 @@ +// 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.Expressions; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing.Constraints; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Routing.Tests +{ + public class BoolRouteConstraintTests + { + [Theory] + [InlineData("true", true)] + [InlineData("TruE", true)] + [InlineData("false", true)] + [InlineData("FalSe", true)] + [InlineData(" FalSe", true)] + [InlineData("True ", true)] + [InlineData(" False ", true)] + [InlineData(true, true)] + [InlineData(false, true)] + [InlineData(1, false)] + [InlineData("not-parseable-as-bool", false)] + public void BoolRouteConstraint(object parameterValue, bool expected) + { + // Arrange + var constraint = new BoolRouteConstraint(); + + // Act + var actual = ConstraintsTestHelper.TestConstraint(constraint, parameterValue); + + // Assert + Assert.Equal(expected, actual); + } + } +} diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/CompositeRouteConstraintTests.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/CompositeRouteConstraintTests.cs new file mode 100644 index 0000000000..b4df74c262 --- /dev/null +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/CompositeRouteConstraintTests.cs @@ -0,0 +1,54 @@ +// 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.Expressions; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing.Constraints; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Routing.Tests +{ + public class CompositeRouteConstraintTests + { + [Theory] + [InlineData(true, true, true)] + [InlineData(true, false, false)] + [InlineData(false, true, false)] + [InlineData(false, false, false)] + public void CompositeRouteConstraint_Match_CallsMatchOnInnerConstraints( + bool inner1Result, + bool inner2Result, + bool expected) + { + // Arrange + var inner1 = MockConstraintWithResult(inner1Result); + var inner2 = MockConstraintWithResult(inner2Result); + + // Act + var constraint = new CompositeRouteConstraint(new[] { inner1.Object, inner2.Object }); + var actual = ConstraintsTestHelper.TestConstraint(constraint, null); + + // Assert + Assert.Equal(expected, actual); + } + + static Expression> ConstraintMatchMethodExpression = + c => c.Match( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny()); + + private static Mock MockConstraintWithResult(bool result) + { + var mock = new Mock(); + mock.Setup(ConstraintMatchMethodExpression) + .Returns(result) + .Verifiable(); + return mock; + } + } +} diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/ConstraintsTestHelper.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/ConstraintsTestHelper.cs new file mode 100644 index 0000000000..a86c7f4910 --- /dev/null +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/ConstraintsTestHelper.cs @@ -0,0 +1,30 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Http; +using Moq; + +namespace Microsoft.AspNetCore.Routing.Tests +{ + public class ConstraintsTestHelper + { + public static bool TestConstraint(IRouteConstraint constraint, object value, Action routeConfig = null) + { + var context = new Mock(); + + var route = new RouteCollection(); + + if (routeConfig != null) + { + routeConfig(route); + } + + var parameterName = "fake"; + var values = new RouteValueDictionary() { { parameterName, value } }; + var routeDirection = RouteDirection.IncomingRequest; + return constraint.Match(context.Object, route, parameterName, values, routeDirection); + } + } +} diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/DateTimeRouteConstraintTests.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/DateTimeRouteConstraintTests.cs new file mode 100644 index 0000000000..fb34db3994 --- /dev/null +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/DateTimeRouteConstraintTests.cs @@ -0,0 +1,53 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Routing.Constraints; +using Xunit; + +namespace Microsoft.AspNetCore.Routing.Tests +{ + public class DateTimeRouteConstraintTests + { + public static IEnumerable GetDateTimeObject + { + get + { + yield return new object[] + { + DateTime.Now, + true + }; + } + } + + [Theory] + [InlineData("12/25/2009", true)] + [InlineData("25/12/2009 11:45:00 PM", false)] + [InlineData("25/12/2009", false)] + [InlineData("11:45:00 PM", true)] + [InlineData("11:45:00", true)] + [InlineData("11:45", true)] + [InlineData("11", false)] + [InlineData("", false)] + [InlineData("Apr 5 2009 11:45:00 PM", true)] + [InlineData("April 5 2009 11:45:00 PM", true)] + [InlineData("12/25/2009 11:45:00 PM", true)] + [InlineData("2009-05-12T11:45:00Z", true)] + [InlineData("not-parseable-as-date", false)] + [InlineData(false, false)] + [MemberData(nameof(GetDateTimeObject))] + public void DateTimeRouteConstraint(object parameterValue, bool expected) + { + // Arrange + var constraint = new DateTimeRouteConstraint(); + + // Act + var actual = ConstraintsTestHelper.TestConstraint(constraint, parameterValue); + + // Assert + Assert.Equal(expected, actual); + } + } +} diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/DecimalRouteConstraintTests.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/DecimalRouteConstraintTests.cs new file mode 100644 index 0000000000..3975317fac --- /dev/null +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/DecimalRouteConstraintTests.cs @@ -0,0 +1,43 @@ +// 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.Constraints; +using Xunit; + +namespace Microsoft.AspNetCore.Routing.Tests +{ + public class DecimalRouteConstraintTests + { + public static IEnumerable GetDecimalObject + { + get + { + yield return new object[] + { + 2m, + true + }; + } + } + + [Theory] + [InlineData("3.14", true)] + [InlineData("9223372036854775808.9223372036854775808", true)] + [InlineData("1.79769313486232E+300", false)] + [InlineData("not-parseable-as-decimal", false)] + [InlineData(false, false)] + [MemberData(nameof(GetDecimalObject))] + public void DecimalRouteConstraint_ApplyConstraint(object parameterValue, bool expected) + { + // Arrange + var constraint = new DecimalRouteConstraint(); + + // Act + var actual = ConstraintsTestHelper.TestConstraint(constraint, parameterValue); + + // Assert + Assert.Equal(expected, actual); + } + } +} diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/DoubleRouteConstraintTests.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/DoubleRouteConstraintTests.cs new file mode 100644 index 0000000000..7f3a2c065d --- /dev/null +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/DoubleRouteConstraintTests.cs @@ -0,0 +1,30 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Routing.Constraints; +using Xunit; + +namespace Microsoft.AspNetCore.Routing.Tests +{ + public class DoubleRouteConstraintTests + { + [Theory] + [InlineData("3.14", true)] + [InlineData(3.14f, true)] + [InlineData(3.14d, true)] + [InlineData("1.79769313486232E+300", true)] + [InlineData("not-parseable-as-double", false)] + [InlineData(false, false)] + public void DoubleRouteConstraint_ApplyConstraint(object parameterValue, bool expected) + { + // Arrange + var constraint = new DoubleRouteConstraint(); + + // Act + var actual = ConstraintsTestHelper.TestConstraint(constraint, parameterValue); + + // Assert + Assert.Equal(expected, actual); + } + } +} diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/FloatRouteConstraintTests.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/FloatRouteConstraintTests.cs new file mode 100644 index 0000000000..0fd1710c1c --- /dev/null +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/FloatRouteConstraintTests.cs @@ -0,0 +1,29 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Routing.Constraints; +using Xunit; + +namespace Microsoft.AspNetCore.Routing.Tests +{ + public class FloatRouteConstraintTests + { + [Theory] + [InlineData("3.14", true)] + [InlineData(3.14, true)] + [InlineData("not-parseable-as-float", false)] + [InlineData(false, false)] + [InlineData("1.79769313486232E+300", false)] + public void FloatRouteConstraint_ApplyConstraint(object parameterValue, bool expected) + { + // Arrange + var constraint = new FloatRouteConstraint(); + + // Act + var actual = ConstraintsTestHelper.TestConstraint(constraint, parameterValue); + + // Assert + Assert.Equal(expected, actual); + } + } +} diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/GuidRouteConstraintTests.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/GuidRouteConstraintTests.cs new file mode 100644 index 0000000000..d53218a83b --- /dev/null +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/GuidRouteConstraintTests.cs @@ -0,0 +1,37 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Routing.Constraints; +using Xunit; + +namespace Microsoft.AspNetCore.Routing.Tests +{ + public class GuidRouteConstraintTests + { + [Theory] + [InlineData("12345678-1234-1234-1234-123456789012", false, true)] + [InlineData("12345678-1234-1234-1234-123456789012", true, true)] + [InlineData("12345678901234567890123456789012", false, true)] + [InlineData("not-parseable-as-guid", false, false)] + [InlineData(12, false, false)] + + public void GuidRouteConstraint_ApplyConstraint(object parameterValue, bool parseBeforeTest, bool expected) + { + // Arrange + if (parseBeforeTest) + { + parameterValue = Guid.Parse(parameterValue.ToString()); + } + + var constraint = new GuidRouteConstraint(); + + // Act + var actual = ConstraintsTestHelper.TestConstraint(constraint, parameterValue); + + // Assert + Assert.Equal(expected, actual); + } + } +} diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/HttpMethodRouteConstraintTests.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/HttpMethodRouteConstraintTests.cs new file mode 100644 index 0000000000..dec3739775 --- /dev/null +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/HttpMethodRouteConstraintTests.cs @@ -0,0 +1,94 @@ +// 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 Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Routing.Constraints +{ + public class HttpMethodRouteConstraintTests + { + [Theory] + [InlineData("GET")] + [InlineData("PosT")] + public void HttpMethodRouteConstraint_IncomingRequest_AcceptsAllowedMethods(string httpMethod) + { + // Arrange + var constraint = new HttpMethodRouteConstraint("GET", "post"); + + var httpContext = new DefaultHttpContext(); + httpContext.Request.Method = httpMethod; + var route = Mock.Of(); + + var values = new RouteValueDictionary(new { }); + + // Act + var result = constraint.Match(httpContext, route, "httpMethod", values, RouteDirection.IncomingRequest); + + // Assert + Assert.True(result); + } + + [Theory] + [InlineData("OPTIONS")] + [InlineData("SomeRandomThing")] + public void HttpMethodRouteConstraint_IncomingRequest_RejectsOtherMethods(string httpMethod) + { + // Arrange + var constraint = new HttpMethodRouteConstraint("GET", "post"); + + var httpContext = new DefaultHttpContext(); + httpContext.Request.Method = httpMethod; + var route = Mock.Of(); + + var values = new RouteValueDictionary(new { }); + + // Act + var result = constraint.Match(httpContext, route, "httpMethod", values, RouteDirection.IncomingRequest); + + // Assert + Assert.False(result); + } + + [Theory] + [InlineData("GET")] + [InlineData("PosT")] + public void HttpMethodRouteConstraint_UrlGeneration_AcceptsAllowedMethods(string httpMethod) + { + // Arrange + var constraint = new HttpMethodRouteConstraint("GET", "post"); + + var httpContext = new DefaultHttpContext(); + var route = Mock.Of(); + + var values = new RouteValueDictionary(new { httpMethod = httpMethod }); + + // Act + var result = constraint.Match(httpContext, route, "httpMethod", values, RouteDirection.UrlGeneration); + + // Assert + Assert.True(result); + } + + [Theory] + [InlineData("OPTIONS")] + [InlineData("SomeRandomThing")] + public void HttpMethodRouteConstraint_UrlGeneration_RejectsOtherMethods(string httpMethod) + { + // Arrange + var constraint = new HttpMethodRouteConstraint("GET", "post"); + + var httpContext = new DefaultHttpContext(); + var route = Mock.Of(); + + var values = new RouteValueDictionary(new { httpMethod = httpMethod }); + + // Act + var result = constraint.Match(httpContext, route, "httpMethod", values, RouteDirection.UrlGeneration); + + // Assert + Assert.False(result); + } + } +} diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/IntRouteConstraintsTests.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/IntRouteConstraintsTests.cs new file mode 100644 index 0000000000..ff70fe21b7 --- /dev/null +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/IntRouteConstraintsTests.cs @@ -0,0 +1,29 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Routing.Constraints; +using Xunit; + +namespace Microsoft.AspNetCore.Routing.Tests +{ + public class IntRouteConstraintsTests + { + [Theory] + [InlineData(42, true)] + [InlineData("42", true)] + [InlineData(3.14, false)] + [InlineData("43.567", false)] + [InlineData("42a", false)] + public void IntRouteConstraint_Match_AppliesConstraint(object parameterValue, bool expected) + { + // Arrange + var constraint = new IntRouteConstraint(); + + // Act + var actual = ConstraintsTestHelper.TestConstraint(constraint, parameterValue); + + // Assert + Assert.Equal(expected, actual); + } + } +} diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/LengthRouteConstraintTests.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/LengthRouteConstraintTests.cs new file mode 100644 index 0000000000..707056a01c --- /dev/null +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/LengthRouteConstraintTests.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 Microsoft.AspNetCore.Routing.Constraints; +using Microsoft.AspNetCore.Testing; +using Xunit; + +namespace Microsoft.AspNetCore.Routing.Tests +{ + public class LengthRouteConstraintTests + { + [Theory] + [InlineData(3, "123", true)] + [InlineData(3, "1234", false)] + [InlineData(0, "", true)] + public void LengthRouteConstraint_ExactLength_Tests(int length, string parameterValue, bool expected) + { + // Arrange + var constraint = new LengthRouteConstraint(length); + + // Act + var actual = ConstraintsTestHelper.TestConstraint(constraint, parameterValue); + + // Assert + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(3, 5, "12", false)] + [InlineData(3, 5, "123", true)] + [InlineData(3, 5, "1234", true)] + [InlineData(3, 5, "12345", true)] + [InlineData(3, 5, "123456", false)] + public void LengthRouteConstraint_Range_Tests(int min, int max, string parameterValue, bool expected) + { + // Arrange + var constraint = new LengthRouteConstraint(min, max); + + // Act + var actual = ConstraintsTestHelper.TestConstraint(constraint, parameterValue); + + // Assert + Assert.Equal(expected, actual); + } + + [Fact] + public void LengthRouteConstraint_SettingLengthLessThanZero_Throws() + { + // Arrange + var expectedMessage = "Value must be greater than or equal to 0."; + + // Act & Assert + ExceptionAssert.ThrowsArgumentOutOfRange( + () => new LengthRouteConstraint(-1), + "length", + expectedMessage, + -1); + } + + [Fact] + public void LengthRouteConstraint_SettingMinLengthLessThanZero_Throws() + { + // Arrange + var expectedMessage = "Value must be greater than or equal to 0."; + + // Act & Assert + ExceptionAssert.ThrowsArgumentOutOfRange( + () => new LengthRouteConstraint(-1, 3), + "minLength", + expectedMessage, + -1); + } + + [Fact] + public void LengthRouteConstraint_SettingMaxLengthLessThanZero_Throws() + { + // Arrange + var expectedMessage = "Value must be greater than or equal to 0."; + + // Act & Assert + ExceptionAssert.ThrowsArgumentOutOfRange( + () => new LengthRouteConstraint(0, -1), + "maxLength", + expectedMessage, + -1); + } + + [Fact] + public void LengthRouteConstraint_MinGreaterThanMax_Throws() + { + // Arrange + var expectedMessage = "The value for argument 'minLength' should be less than or equal to the " + + "value for the argument 'maxLength'."; + + // Arrange Act & Assert + ExceptionAssert.ThrowsArgumentOutOfRange( + () => new LengthRouteConstraint(3, 2), + "minLength", + expectedMessage, + 3); + } + } +} diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/LongRouteConstraintTests.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/LongRouteConstraintTests.cs new file mode 100644 index 0000000000..99977ee753 --- /dev/null +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/LongRouteConstraintTests.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. + +using Microsoft.AspNetCore.Routing.Constraints; +using Xunit; + +namespace Microsoft.AspNetCore.Routing.Tests +{ + public class LongRouteConstraintTests + { + [Theory] + [InlineData(42, true)] + [InlineData(42L, true)] + [InlineData("42", true)] + [InlineData("9223372036854775807", true)] + [InlineData(3.14, false)] + [InlineData("43.567", false)] + [InlineData("42a", false)] + public void LongRouteConstraintTest(object parameterValue, bool expected) + { + // Arrange + var constraint = new LongRouteConstraint(); + + // Act + var actual = ConstraintsTestHelper.TestConstraint(constraint, parameterValue); + + // Assert + Assert.Equal(expected, actual); + } + } +} diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/MaxLengthRouteConstraintTests.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/MaxLengthRouteConstraintTests.cs new file mode 100644 index 0000000000..5d603705e2 --- /dev/null +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/MaxLengthRouteConstraintTests.cs @@ -0,0 +1,43 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Routing.Constraints; +using Microsoft.AspNetCore.Testing; +using Xunit; + +namespace Microsoft.AspNetCore.Routing.Tests +{ + public class MaxLengthRouteConstraintTests + { + [Theory] + [InlineData(3, "", true)] + [InlineData(3, "12", true)] + [InlineData(3, "123", true)] + [InlineData(3, "1234", false)] + public void MaxLengthRouteConstraint_ApplyConstraint(int min, string parameterValue, bool expected) + { + // Arrange + var constraint = new MaxLengthRouteConstraint(min); + + // Act + var actual = ConstraintsTestHelper.TestConstraint(constraint, parameterValue); + + // Assert + Assert.Equal(expected, actual); + } + + [Fact] + public void MaxLengthRouteConstraint_SettingMaxLengthLessThanZero_Throws() + { + // Arrange + var expectedMessage = "Value must be greater than or equal to 0."; + + // Act & Assert + ExceptionAssert.ThrowsArgumentOutOfRange( + () => new MaxLengthRouteConstraint(-1), + "maxLength", + expectedMessage, + -1); + } + } +} diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/MaxRouteConstraintTests.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/MaxRouteConstraintTests.cs new file mode 100644 index 0000000000..dc962d0948 --- /dev/null +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/MaxRouteConstraintTests.cs @@ -0,0 +1,27 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Routing.Constraints; +using Xunit; + +namespace Microsoft.AspNetCore.Routing.Tests +{ + public class MaxRouteConstraintTests + { + [Theory] + [InlineData(3, 2, true)] + [InlineData(3, 3, true)] + [InlineData(3, 4, false)] + public void MaxRouteConstraint_ApplyConstraint(long max, int parameterValue, bool expected) + { + // Arrange + var constraint = new MaxRouteConstraint(max); + + // Act + var actual = ConstraintsTestHelper.TestConstraint(constraint, parameterValue); + + // Assert + Assert.Equal(expected, actual); + } + } +} diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/MinLengthRouteConstraintTests.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/MinLengthRouteConstraintTests.cs new file mode 100644 index 0000000000..c8e7bab2de --- /dev/null +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/MinLengthRouteConstraintTests.cs @@ -0,0 +1,43 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Routing.Constraints; +using Microsoft.AspNetCore.Testing; +using Xunit; + +namespace Microsoft.AspNetCore.Routing.Tests +{ + public class MinLengthRouteConstraintTests + { + [Theory] + [InlineData(3, "1234", true)] + [InlineData(3, "123", true)] + [InlineData(3, "12", false)] + [InlineData(3, "", false)] + public void MinLengthRouteConstraint_ApplyConstraint(int min, string parameterValue, bool expected) + { + // Arrange + var constraint = new MinLengthRouteConstraint(min); + + // Act + var actual = ConstraintsTestHelper.TestConstraint(constraint, parameterValue); + + // Assert + Assert.Equal(expected, actual); + } + + [Fact] + public void MinLengthRouteConstraint_SettingMinLengthLessThanZero_Throws() + { + // Arrange + var expectedMessage = "Value must be greater than or equal to 0."; + + // Act & Assert + ExceptionAssert.ThrowsArgumentOutOfRange( + () => new MinLengthRouteConstraint(-1), + "minLength", + expectedMessage, + -1); + } + } +} diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/MinRouteConstraintTests.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/MinRouteConstraintTests.cs new file mode 100644 index 0000000000..b21b2ded74 --- /dev/null +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/MinRouteConstraintTests.cs @@ -0,0 +1,27 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Routing.Constraints; +using Xunit; + +namespace Microsoft.AspNetCore.Routing.Tests +{ + public class MinRouteConstraintTests + { + [Theory] + [InlineData(3, 4, true)] + [InlineData(3, 3, true)] + [InlineData(3, 2, false)] + public void MinRouteConstraint_ApplyConstraint(long min, int parameterValue, bool expected) + { + // Arrange + var constraint = new MinRouteConstraint(min); + + // Act + var actual = ConstraintsTestHelper.TestConstraint(constraint, parameterValue); + + // Assert + Assert.Equal(expected, actual); + } + } +} diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/RangeRouteConstraintTests.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/RangeRouteConstraintTests.cs new file mode 100644 index 0000000000..99d9c64ba1 --- /dev/null +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/RangeRouteConstraintTests.cs @@ -0,0 +1,48 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Routing.Constraints; +using Microsoft.AspNetCore.Testing; +using Xunit; + +namespace Microsoft.AspNetCore.Routing.Tests +{ + public class RangeRouteConstraintTests + { + [Theory] + [InlineData(long.MinValue, long.MaxValue, 2, true)] + [InlineData(3, 5, 3, true)] + [InlineData(3, 5, 4, true)] + [InlineData(3, 5, 5, true)] + [InlineData(3, 5, 6, false)] + [InlineData(3, 5, 2, false)] + [InlineData(3, 3, 2, false)] + [InlineData(3, 3, 3, true)] + public void RangeRouteConstraintTest_ValidValue_ApplyConstraint(long min, long max, int parameterValue, bool expected) + { + // Arrange + var constraint = new RangeRouteConstraint(min, max); + + // Act + var actual = ConstraintsTestHelper.TestConstraint(constraint, parameterValue); + + // Assert + Assert.Equal(expected, actual); + } + + [Fact] + public void RangeRouteConstraint_MinGreaterThanMax_Throws() + { + // Arrange + var expectedMessage = "The value for argument 'min' should be less than or equal to the value for the " + + "argument 'max'."; + + // Act & Assert + ExceptionAssert.ThrowsArgumentOutOfRange( + () => new RangeRouteConstraint(3, 2), + "min", + expectedMessage, + 3L); + } + } +} diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/RegexInlineRouteConstraintTests.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/RegexInlineRouteConstraintTests.cs new file mode 100644 index 0000000000..a87f6519c7 --- /dev/null +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/RegexInlineRouteConstraintTests.cs @@ -0,0 +1,92 @@ +// 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.Constraints; +using Microsoft.AspNetCore.Testing; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Routing.Tests +{ + public class RegexInlineRouteConstraintTests + { + [Theory] + [InlineData("abc", "abc", true)] // simple match + [InlineData("Abc", "abc", true)] // case insensitive match + [InlineData("Abc ", "abc", true)] // Extra space on input match (because we don't add ^({0})$ + [InlineData("Abcd", "abc", true)] // Extra char + [InlineData("^Abcd", "abc", true)] // Extra special char + [InlineData("Abc", " abc", false)] // Missing char + public void RegexInlineConstraintBuildRegexVerbatimFromInput( + string routeValue, + string constraintValue, + bool shouldMatch) + { + // Arrange + var constraint = new RegexInlineRouteConstraint(constraintValue); + var values = new RouteValueDictionary(new { controller = routeValue }); + + // Act + var match = constraint.Match( + new DefaultHttpContext(), + route: new Mock().Object, + routeKey: "controller", + values: values, + routeDirection: RouteDirection.IncomingRequest); + + // Assert + Assert.Equal(shouldMatch, match); + } + + [Fact] + public void RegexInlineConstraint_FailsIfKeyIsNotFoundInRouteValues() + { + // Arrange + var constraint = new RegexInlineRouteConstraint("^abc$"); + var values = new RouteValueDictionary(new { action = "abc" }); + + // Act + var match = constraint.Match( + new DefaultHttpContext(), + route: new Mock().Object, + routeKey: "controller", + values: values, + routeDirection: RouteDirection.IncomingRequest); + + // Assert + Assert.False(match); + } + + [Theory] + [InlineData("tr-TR")] + [InlineData("en-US")] + public void RegexInlineConstraint_IsCultureInsensitive(string culture) + { + if (TestPlatformHelper.IsMono) + { + // The Regex in Mono returns true when matching the Turkish I for the a-z range which causes the test + // to fail. Tracked via #100. + return; + } + + // Arrange + var constraint = new RegexInlineRouteConstraint("^([a-z]+)$"); + var values = new RouteValueDictionary(new { controller = "\u0130" }); // Turkish upper-case dotted I + + using (new CultureReplacer(culture)) + { + // Act + var match = constraint.Match( + new DefaultHttpContext(), + route: new Mock().Object, + routeKey: "controller", + values: values, + routeDirection: RouteDirection.IncomingRequest); + + // Assert + Assert.False(match); + } + } + } +} diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/RegexRouteConstraintTests.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/RegexRouteConstraintTests.cs new file mode 100644 index 0000000000..7affd5c034 --- /dev/null +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/RegexRouteConstraintTests.cs @@ -0,0 +1,134 @@ +// 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.Text.RegularExpressions; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing.Constraints; +using Microsoft.AspNetCore.Testing; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Routing.Tests +{ + public class RegexRouteConstraintTests + { + [Theory] + [InlineData("abc", "abc", true)] // simple match + [InlineData("Abc", "abc", true)] // case insensitive match + [InlineData("Abc ", "abc", true)] // Extra space on input match (because we don't add ^({0})$ + [InlineData("Abcd", "abc", true)] // Extra char + [InlineData("^Abcd", "abc", true)] // Extra special char + [InlineData("Abc", " abc", false)] // Missing char + [InlineData("123-456-2334", @"^\d{3}-\d{3}-\d{4}$", true)] // ssn + [InlineData(@"12/4/2013", @"^\d{1,2}\/\d{1,2}\/\d{4}$", true)] // date + [InlineData(@"abc@def.com", @"^\w+[\w\.]*\@\w+((-\w+)|(\w*))\.[a-z]{2,3}$", true)] // email + public void RegexConstraintBuildRegexVerbatimFromInput( + string routeValue, + string constraintValue, + bool shouldMatch) + { + // Arrange + var constraint = new RegexRouteConstraint(constraintValue); + var values = new RouteValueDictionary(new { controller = routeValue }); + + // Act + var match = constraint.Match( + new DefaultHttpContext(), + route: new Mock().Object, + routeKey: "controller", + values: values, + routeDirection: RouteDirection.IncomingRequest); + + // Assert + Assert.Equal(shouldMatch, match); + } + + [Fact] + public void RegexConstraint_TakesRegexAsInput_SimpleMatch() + { + // Arrange + var constraint = new RegexRouteConstraint(new Regex("^abc$")); + var values = new RouteValueDictionary(new { controller = "abc" }); + + // Act + var match = constraint.Match( + new DefaultHttpContext(), + route: new Mock().Object, + routeKey: "controller", + values: values, + routeDirection: RouteDirection.IncomingRequest); + + // Assert + Assert.True(match); + } + + [Fact] + public void RegexConstraintConstructedWithRegex_SimpleFailedMatch() + { + // Arrange + var constraint = new RegexRouteConstraint(new Regex("^abc$")); + var values = new RouteValueDictionary(new { controller = "Abc" }); + + // Act + var match = constraint.Match( + new DefaultHttpContext(), + route: new Mock().Object, + routeKey: "controller", + values: values, + routeDirection: RouteDirection.IncomingRequest); + + // Assert + Assert.False(match); + } + + [Fact] + public void RegexConstraintFailsIfKeyIsNotFoundInRouteValues() + { + // Arrange + var constraint = new RegexRouteConstraint(new Regex("^abc$")); + var values = new RouteValueDictionary(new { action = "abc" }); + + // Act + var match = constraint.Match( + new DefaultHttpContext(), + route: new Mock().Object, + routeKey: "controller", + values: values, + routeDirection: RouteDirection.IncomingRequest); + + // Assert + Assert.False(match); + } + + [Theory] + [InlineData("tr-TR")] + [InlineData("en-US")] + public void RegexConstraintIsCultureInsensitiveWhenConstructedWithString(string culture) + { + if (TestPlatformHelper.IsMono) + { + // The Regex in Mono returns true when matching the Turkish I for the a-z range which causes the test + // to fail. Tracked via #100. + return; + } + + // Arrange + var constraint = new RegexRouteConstraint("^([a-z]+)$"); + var values = new RouteValueDictionary(new { controller = "\u0130" }); // Turkish upper-case dotted I + + using (new CultureReplacer(culture)) + { + // Act + var match = constraint.Match( + httpContext: new Mock().Object, + route: new Mock().Object, + routeKey: "controller", + values: values, + routeDirection: RouteDirection.IncomingRequest); + + // Assert + Assert.False(match); + } + } + } +} diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/RequiredRouteConstraintTests.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/RequiredRouteConstraintTests.cs new file mode 100644 index 0000000000..31ec2b2bf0 --- /dev/null +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/RequiredRouteConstraintTests.cs @@ -0,0 +1,93 @@ +// 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.Constraints; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Routing.Tests +{ + public class RequiredRouteConstraintTests + { + [Theory] + [InlineData(RouteDirection.IncomingRequest)] + [InlineData(RouteDirection.UrlGeneration)] + public void RequiredRouteConstraint_NoValue(RouteDirection direction) + { + // Arrange + var constraint = new RequiredRouteConstraint(); + + // Act + var result = constraint.Match( + new DefaultHttpContext(), + Mock.Of(), + "area", + new RouteValueDictionary(new { controller = "Home", action = "Index" }), + direction); + + // Assert + Assert.False(result); + } + + [Theory] + [InlineData(RouteDirection.IncomingRequest)] + [InlineData(RouteDirection.UrlGeneration)] + public void RequiredRouteConstraint_Null(RouteDirection direction) + { + // Arrange + var constraint = new RequiredRouteConstraint(); + + // Act + var result = constraint.Match( + new DefaultHttpContext(), + Mock.Of(), + "area", + new RouteValueDictionary(new { controller = "Home", action = "Index", area = (string)null }), + direction); + + // Assert + Assert.False(result); + } + + [Theory] + [InlineData(RouteDirection.IncomingRequest)] + [InlineData(RouteDirection.UrlGeneration)] + public void RequiredRouteConstraint_EmptyString(RouteDirection direction) + { + // Arrange + var constraint = new RequiredRouteConstraint(); + + // Act + var result = constraint.Match( + new DefaultHttpContext(), + Mock.Of(), + "area", + new RouteValueDictionary(new { controller = "Home", action = "Index", area = string.Empty}), + direction); + + // Assert + Assert.False(result); + } + + [Theory] + [InlineData(RouteDirection.IncomingRequest)] + [InlineData(RouteDirection.UrlGeneration)] + public void RequiredRouteConstraint_WithValue(RouteDirection direction) + { + // Arrange + var constraint = new RequiredRouteConstraint(); + + // Act + var result = constraint.Match( + new DefaultHttpContext(), + Mock.Of(), + "area", + new RouteValueDictionary(new { controller = "Home", action = "Index", area = "Store" }), + direction); + + // Assert + Assert.True(result); + } + } +} diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/StringRouteConstraintTest.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/StringRouteConstraintTest.cs new file mode 100644 index 0000000000..9e9f178286 --- /dev/null +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/StringRouteConstraintTest.cs @@ -0,0 +1,157 @@ +// 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.Constraints; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Routing.Constraints +{ + public class StringRouteConstraintTest + { + [Fact] + public void StringRouteConstraintSimpleTrueWithRouteDirectionIncomingRequestTest() + { + // Arrange + var constraint = new StringRouteConstraint("home"); + + // Act + var values = new RouteValueDictionary(new { controller = "home" }); + + var match = constraint.Match( + new DefaultHttpContext(), + route: new Mock().Object, + routeKey: "controller", + values: values, + routeDirection: RouteDirection.IncomingRequest); + + // Assert + Assert.True(match); + } + + [Fact] + public void StringRouteConstraintSimpleTrueWithRouteDirectionUrlGenerationTest() + { + // Arrange + var constraint = new StringRouteConstraint("home"); + + // Act + var values = new RouteValueDictionary(new { controller = "home" }); + + var match = constraint.Match( + new DefaultHttpContext(), + route: new Mock().Object, + routeKey: "controller", + values: values, + routeDirection: RouteDirection.UrlGeneration); + + // Assert + Assert.True(match); + } + + [Fact] + public void StringRouteConstraintSimpleFalseWithRouteDirectionIncomingRequestTest() + { + // Arrange + var constraint = new StringRouteConstraint("admin"); + + // Act + var values = new RouteValueDictionary(new { controller = "home" }); + + var match = constraint.Match( + new DefaultHttpContext(), + route: new Mock().Object, + routeKey: "controller", + values: values, + routeDirection: RouteDirection.IncomingRequest); + + // Assert + Assert.False(match); + } + + [Fact] + public void StringRouteConstraintSimpleFalseWithRouteDirectionUrlGenerationTest() + { + // Arrange + var constraint = new StringRouteConstraint("admin"); + + // Act + var values = new RouteValueDictionary(new { controller = "home" }); + + var match = constraint.Match( + new DefaultHttpContext(), + route: new Mock().Object, + routeKey: "controller", + values: values, + routeDirection: RouteDirection.UrlGeneration); + + // Assert + Assert.False(match); + } + + [Fact] + public void StringRouteConstraintKeyNotFoundWithRouteDirectionIncomingRequestTest() + { + // Arrange + var constraint = new StringRouteConstraint("admin"); + + // Act + var values = new RouteValueDictionary(new { controller = "admin" }); + + var match = constraint.Match( + new DefaultHttpContext(), + route: new Mock().Object, + routeKey: "action", + values: values, + routeDirection: RouteDirection.IncomingRequest); + + // Assert + Assert.False(match); + } + + [Fact] + public void StringRouteConstraintKeyNotFoundWithRouteDirectionUrlGenerationTest() + { + // Arrange + var constraint = new StringRouteConstraint("admin"); + + // Act + var values = new RouteValueDictionary(new { controller = "admin" }); + + var match = constraint.Match( + new DefaultHttpContext(), + route: new Mock().Object, + routeKey: "action", + values: values, + routeDirection: RouteDirection.UrlGeneration); + + // Assert + Assert.False(match); + } + + [Theory] + [InlineData("User", "uSer", true)] + [InlineData("User.Admin", "User.Admin", true)] + [InlineData(@"User\Admin", "User\\Admin", true)] + [InlineData(null, "user", false)] + public void StringRouteConstraintEscapingCaseSensitiveAndRouteNullTest(string routeValue, string constraintValue, bool expected) + { + // Arrange + var constraint = new StringRouteConstraint(constraintValue); + + // Act + var values = new RouteValueDictionary(new { controller = routeValue }); + + var match = constraint.Match( + new DefaultHttpContext(), + route: new Mock().Object, + routeKey: "controller", + values: values, + routeDirection: RouteDirection.IncomingRequest); + + // Assert + Assert.Equal(expected, match); + } + } +} \ No newline at end of file diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/DefaultInlineConstraintResolverTest.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/DefaultInlineConstraintResolverTest.cs new file mode 100644 index 0000000000..bba78fb6b2 --- /dev/null +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/DefaultInlineConstraintResolverTest.cs @@ -0,0 +1,372 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing.Constraints; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Routing.Tests +{ + public class DefaultInlineConstraintResolverTest + { + private IInlineConstraintResolver _constraintResolver; + + public DefaultInlineConstraintResolverTest() + { + var routeOptions = new RouteOptions(); + _constraintResolver = GetInlineConstraintResolver(routeOptions); + } + + [Fact] + public void ResolveConstraint_RequiredConstraint_ResolvesCorrectly() + { + // Arrange & Act + var constraint = _constraintResolver.ResolveConstraint("required"); + + // Assert + Assert.IsType(constraint); + } + + [Fact] + public void ResolveConstraint_IntConstraint_ResolvesCorrectly() + { + // Arrange & Act + var constraint = _constraintResolver.ResolveConstraint("int"); + + // Assert + Assert.IsType(constraint); + } + + [Fact] + public void ResolveConstraint_IntConstraintWithArgument_Throws() + { + // Arrange, Act & Assert + var ex = Assert.Throws( + () => _constraintResolver.ResolveConstraint("int(5)")); + + Assert.Equal("Could not find a constructor for constraint type 'IntRouteConstraint'" + + " with the following number of parameters: 1.", + ex.Message); + } + + [Fact] + public void ResolveConstraint_AlphaConstraint() + { + // Arrange & Act + var constraint = _constraintResolver.ResolveConstraint("alpha"); + + // Assert + Assert.IsType(constraint); + } + + [Fact] + public void ResolveConstraint_RegexInlineConstraint_WithAComma_PassesAsASingleArgument() + { + // Arrange & Act + var constraint = _constraintResolver.ResolveConstraint("regex(ab,1)"); + + // Assert + Assert.IsType(constraint); + } + + [Fact] + public void ResolveConstraint_RegexInlineConstraint_WithCurlyBraces_Balanced() + { + // Arrange & Act + var constraint = _constraintResolver.ResolveConstraint( + @"regex(\\b(?\\d{1,2})/(?\\d{1,2})/(?\\d{2,4})\\b)"); + + // Assert + Assert.IsType(constraint); + } + + [Fact] + public void ResolveConstraint_BoolConstraint() + { + // Arrange & Act + var constraint = _constraintResolver.ResolveConstraint("bool"); + + // Assert + Assert.IsType(constraint); + } + + [Fact] + public void ResolveConstraint_CompositeConstraintIsNotRegistered() + { + // Arrange, Act & Assert + Assert.Null(_constraintResolver.ResolveConstraint("composite")); + } + + [Fact] + public void ResolveConstraint_DateTimeConstraint() + { + // Arrange & Act + var constraint = _constraintResolver.ResolveConstraint("datetime"); + + // Assert + Assert.IsType(constraint); + } + + [Fact] + public void ResolveConstraint_DecimalConstraint() + { + // Arrange & Act + var constraint = _constraintResolver.ResolveConstraint("decimal"); + + // Assert + Assert.IsType(constraint); + } + + [Fact] + public void ResolveConstraint_DoubleConstraint() + { + // Arrange & Act + var constraint = _constraintResolver.ResolveConstraint("double"); + + // Assert + Assert.IsType(constraint); + } + + [Fact] + public void ResolveConstraint_FloatConstraint() + { + // Arrange & Act + var constraint = _constraintResolver.ResolveConstraint("float"); + + // Assert + Assert.IsType(constraint); + } + + [Fact] + public void ResolveConstraint_GuidConstraint() + { + // Arrange & Act + var constraint = _constraintResolver.ResolveConstraint("guid"); + + // Assert + Assert.IsType(constraint); + } + + [Fact] + public void ResolveConstraint_IntConstraint() + { + // Arrange & Act + var constraint = _constraintResolver.ResolveConstraint("int"); + + // Assert + Assert.IsType(constraint); + } + + [Fact] + public void ResolveConstraint_LengthConstraint() + { + // Arrange & Act + var constraint = _constraintResolver.ResolveConstraint("length(5)"); + + // Assert + Assert.IsType(constraint); + Assert.Equal(5, ((LengthRouteConstraint)constraint).MinLength); + Assert.Equal(5, ((LengthRouteConstraint)constraint).MaxLength); + } + + [Fact] + public void ResolveConstraint_LengthRangeConstraint() + { + // Arrange & Act + var constraint = _constraintResolver.ResolveConstraint("length(5, 10)"); + + // Assert + var lengthConstraint = Assert.IsType(constraint); + Assert.Equal(5, lengthConstraint.MinLength); + Assert.Equal(10, lengthConstraint.MaxLength); + } + + [Fact] + public void ResolveConstraint_LongRangeConstraint() + { + // Arrange & Act + var constraint = _constraintResolver.ResolveConstraint("long"); + + // Assert + Assert.IsType(constraint); + } + + [Fact] + public void ResolveConstraint_MaxConstraint() + { + // Arrange & Act + var constraint = _constraintResolver.ResolveConstraint("max(10)"); + + // Assert + Assert.IsType(constraint); + Assert.Equal(10, ((MaxRouteConstraint)constraint).Max); + } + + [Fact] + public void ResolveConstraint_MaxLengthConstraint() + { + // Arrange & Act + var constraint = _constraintResolver.ResolveConstraint("maxlength(10)"); + + // Assert + Assert.IsType(constraint); + Assert.Equal(10, ((MaxLengthRouteConstraint)constraint).MaxLength); + } + + [Fact] + public void ResolveConstraint_MinConstraint() + { + // Arrange & Act + var constraint = _constraintResolver.ResolveConstraint("min(3)"); + + // Assert + Assert.IsType(constraint); + Assert.Equal(3, ((MinRouteConstraint)constraint).Min); + } + + [Fact] + public void ResolveConstraint_MinLengthConstraint() + { + // Arrange & Act + var constraint = _constraintResolver.ResolveConstraint("minlength(3)"); + + // Assert + Assert.IsType(constraint); + Assert.Equal(3, ((MinLengthRouteConstraint)constraint).MinLength); + } + + [Fact] + public void ResolveConstraint_RangeConstraint() + { + // Arrange & Act + var constraint = _constraintResolver.ResolveConstraint("range(5, 10)"); + + // Assert + Assert.IsType(constraint); + var rangeConstraint = (RangeRouteConstraint)constraint; + Assert.Equal(5, rangeConstraint.Min); + Assert.Equal(10, rangeConstraint.Max); + } + + [Fact] + public void ResolveConstraint_SupportsCustomConstraints() + { + // Arrange + var routeOptions = new RouteOptions(); + routeOptions.ConstraintMap.Add("custom", typeof(CustomRouteConstraint)); + var resolver = GetInlineConstraintResolver(routeOptions); + + // Act + var constraint = resolver.ResolveConstraint("custom(argument)"); + + // Assert + Assert.IsType(constraint); + } + + [Fact] + public void ResolveConstraint_CustomConstraintThatDoesNotImplementIRouteConstraint_Throws() + { + // Arrange + var routeOptions = new RouteOptions(); + routeOptions.ConstraintMap.Add("custom", typeof(string)); + var resolver = GetInlineConstraintResolver(routeOptions); + + // Act & Assert + var ex = Assert.Throws(() => resolver.ResolveConstraint("custom")); + Assert.Equal("The constraint type 'System.String' which is mapped to constraint key 'custom'" + + " must implement the 'IRouteConstraint' interface.", + ex.Message); + } + + [Fact] + public void ResolveConstraint_AmbiguousConstructors_Throws() + { + // Arrange + var routeOptions = new RouteOptions(); + routeOptions.ConstraintMap.Add("custom", typeof(MultiConstructorRouteConstraint)); + var resolver = GetInlineConstraintResolver(routeOptions); + + // Act & Assert + var ex = Assert.Throws(() => resolver.ResolveConstraint("custom(5,6)")); + Assert.Equal("The constructor to use for activating the constraint type 'MultiConstructorRouteConstraint' is ambiguous." + + " Multiple constructors were found with the following number of parameters: 2.", + ex.Message); + } + + // These are cases which parsing does not catch and we'll end up here + [Theory] + [InlineData("regex(abc")] + [InlineData("int/")] + [InlineData("in{t")] + public void ResolveConstraint_Invalid_Throws(string constraint) + { + // Arrange + var routeOptions = new RouteOptions(); + var resolver = GetInlineConstraintResolver(routeOptions); + + // Act & Assert + Assert.Null(resolver.ResolveConstraint(constraint)); + } + + [Fact] + public void ResolveConstraint_NoMatchingConstructor_Throws() + { + // Arrange + // Act & Assert + var ex = Assert.Throws(() => _constraintResolver.ResolveConstraint("int(5,6)")); + Assert.Equal("Could not find a constructor for constraint type 'IntRouteConstraint'" + + " with the following number of parameters: 2.", + ex.Message); + } + + private IInlineConstraintResolver GetInlineConstraintResolver(RouteOptions routeOptions) + { + var optionsAccessor = new Mock>(); + optionsAccessor.SetupGet(o => o.Value).Returns(routeOptions); + return new DefaultInlineConstraintResolver(optionsAccessor.Object); + } + + private class MultiConstructorRouteConstraint : IRouteConstraint + { + public MultiConstructorRouteConstraint(string pattern, int intArg) + { + } + + public MultiConstructorRouteConstraint(int intArg, string pattern) + { + } + + public bool Match(HttpContext httpContext, + IRouter route, + string routeKey, + RouteValueDictionary values, + RouteDirection routeDirection) + { + return true; + } + } + + private class CustomRouteConstraint : IRouteConstraint + { + public CustomRouteConstraint(string pattern) + { + Pattern = pattern; + } + + public string Pattern { get; private set; } + public bool Match(HttpContext httpContext, + IRouter route, + string routeKey, + RouteValueDictionary values, + RouteDirection routeDirection) + { + return true; + } + } + } +} diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/InlineRouteParameterParserTests.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/InlineRouteParameterParserTests.cs new file mode 100644 index 0000000000..0e570c49cc --- /dev/null +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/InlineRouteParameterParserTests.cs @@ -0,0 +1,992 @@ +// 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.Template; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.AspNetCore.Routing.Tests +{ + public class InlineRouteParameterParserTests + { + [Theory] + [InlineData("=")] + [InlineData(":")] + public void ParseRouteParameter_WithoutADefaultValue(string parameterName) + { + // Arrange & Act + var templatePart = ParseParameter(parameterName); + + // Assert + Assert.Equal(parameterName, templatePart.Name); + Assert.Null(templatePart.DefaultValue); + Assert.Empty(templatePart.InlineConstraints); + } + + [Fact] + public void ParseRouteParameter_WithEmptyDefaultValue() + { + // Arrange & Act + var templatePart = ParseParameter("param="); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Equal("", templatePart.DefaultValue); + Assert.Empty(templatePart.InlineConstraints); + } + + [Fact] + public void ParseRouteParameter_WithoutAConstraintName() + { + // Arrange & Act + var templatePart = ParseParameter("param:"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Null(templatePart.DefaultValue); + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Empty(constraint.Constraint); + } + + [Fact] + public void ParseRouteParameter_WithoutAConstraintNameOrParameterName() + { + // Arrange & Act + var templatePart = ParseParameter("param:="); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Equal("", templatePart.DefaultValue); + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Empty(constraint.Constraint); + } + + [Fact] + public void ParseRouteParameter_WithADefaultValueContainingConstraintSeparator() + { + // Arrange & Act + var templatePart = ParseParameter("param=:"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Equal(":", templatePart.DefaultValue); + Assert.Empty(templatePart.InlineConstraints); + } + + [Fact] + public void ParseRouteParameter_ConstraintAndDefault_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter("param:int=111111"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Equal("111111", templatePart.DefaultValue); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal("int", constraint.Constraint); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithArgumentsAndDefault_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\d+)=111111"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Equal("111111", templatePart.DefaultValue); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(\d+)", constraint.Constraint); + } + + [Fact] + public void ParseRouteParameter_ConstraintAndOptional_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:int?"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.True(templatePart.IsOptional); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal("int", constraint.Constraint); + } + + [Fact] + public void ParseRouteParameter_ConstraintAndOptional_WithDefaultValue_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:int=12?"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Equal("12", templatePart.DefaultValue); + Assert.True(templatePart.IsOptional); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal("int", constraint.Constraint); + } + + [Fact] + public void ParseRouteParameter_ConstraintAndOptional_WithDefaultValueWithQuestionMark_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:int=12??"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Equal("12?", templatePart.DefaultValue); + Assert.True(templatePart.IsOptional); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal("int", constraint.Constraint); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithArgumentsAndOptional_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\d+)?"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.True(templatePart.IsOptional); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(\d+)", constraint.Constraint); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithArgumentsAndOptional_WithDefaultValue_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\d+)=abc?"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.True(templatePart.IsOptional); + + Assert.Equal("abc", templatePart.DefaultValue); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(\d+)", constraint.Constraint); + } + + [Fact] + public void ParseRouteParameter_ChainedConstraints_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(d+):test(w+)"); + + // Assert + Assert.Equal("param", templatePart.Name); + + Assert.Collection(templatePart.InlineConstraints, + constraint => Assert.Equal(@"test(d+)", constraint.Constraint), + constraint => Assert.Equal(@"test(w+)", constraint.Constraint)); + } + + [Fact] + public void ParseRouteParameter_ChainedConstraints_DoubleDelimiters_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param::test(d+)::test(w+)"); + + // Assert + Assert.Equal("param", templatePart.Name); + + Assert.Collection(templatePart.InlineConstraints, + constraint => Assert.Empty(constraint.Constraint), + constraint => Assert.Equal(@"test(d+)", constraint.Constraint), + constraint => Assert.Empty(constraint.Constraint), + constraint => Assert.Equal(@"test(w+)", constraint.Constraint)); + } + + [Fact] + public void ParseRouteParameter_ChainedConstraints_ColonInPattern_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\d+):test(\w:+)"); + + // Assert + Assert.Equal("param", templatePart.Name); + + Assert.Collection(templatePart.InlineConstraints, + constraint => Assert.Equal(@"test(\d+)", constraint.Constraint), + constraint => Assert.Equal(@"test(\w:+)", constraint.Constraint)); + } + + [Fact] + public void ParseRouteParameter_ChainedConstraints_WithDefaultValue_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\d+):test(\w+)=qwer"); + + // Assert + Assert.Equal("param", templatePart.Name); + + Assert.Equal("qwer", templatePart.DefaultValue); + + Assert.Collection(templatePart.InlineConstraints, + constraint => Assert.Equal(@"test(\d+)", constraint.Constraint), + constraint => Assert.Equal(@"test(\w+)", constraint.Constraint)); + } + + [Fact] + public void ParseRouteParameter_ChainedConstraints_WithDefaultValue_DoubleDelimiters_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\d+)::test(\w+)==qwer"); + + // Assert + Assert.Equal("param", templatePart.Name); + + Assert.Equal("=qwer", templatePart.DefaultValue); + + Assert.Collection(templatePart.InlineConstraints, + constraint => Assert.Equal(@"test(\d+)", constraint.Constraint), + constraint => Assert.Empty(constraint.Constraint), + constraint => Assert.Equal(@"test(\w+)", constraint.Constraint)); + } + + [Theory] + [InlineData("=")] + [InlineData("+=")] + [InlineData(">= || <= || ==")] + public void ParseRouteParameter_WithDefaultValue_ContainingDelimiter(string defaultValue) + { + // Arrange & Act + var templatePart = ParseParameter($"comparison-operator:length(6)={defaultValue}"); + + // Assert + Assert.Equal("comparison-operator", templatePart.Name); + Assert.Equal(defaultValue, templatePart.DefaultValue); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal("length(6)", constraint.Constraint); + } + + [Fact] + public void ParseRouteTemplate_ConstraintsDefaultsAndOptionalsInMultipleSections_ParsedCorrectly() + { + // Arrange & Act + var template = ParseRouteTemplate(@"some/url-{p1:int:test(3)=hello}/{p2=abc}/{p3?}"); + + // Assert + var parameters = template.Parameters.ToArray(); + + var param1 = parameters[0]; + Assert.Equal("p1", param1.Name); + Assert.Equal("hello", param1.DefaultValue); + Assert.False(param1.IsOptional); + + Assert.Collection(param1.InlineConstraints, + constraint => Assert.Equal("int", constraint.Constraint), + constraint => Assert.Equal("test(3)", constraint.Constraint) + ); + + var param2 = parameters[1]; + Assert.Equal("p2", param2.Name); + Assert.Equal("abc", param2.DefaultValue); + Assert.False(param2.IsOptional); + + var param3 = parameters[2]; + Assert.Equal("p3", param3.Name); + Assert.True(param3.IsOptional); + } + + [Fact] + public void ParseRouteParameter_NoTokens_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter("world"); + + // Assert + Assert.Equal("world", templatePart.Name); + } + + [Fact] + public void ParseRouteParameter_ParamDefault_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter("param=world"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Equal("world", templatePart.DefaultValue); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithClosingBraceInPattern_ClosingBraceIsParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\})"); + + // Assert + Assert.Equal("param", templatePart.Name); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(\})", constraint.Constraint); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithClosingBraceInPattern_WithDefaultValue_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\})=wer"); + + // Assert + Assert.Equal("param", templatePart.Name); + + Assert.Equal("wer", templatePart.DefaultValue); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(\})", constraint.Constraint); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithClosingParenInPattern_ClosingParenIsParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\))"); + + // Assert + Assert.Equal("param", templatePart.Name); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(\))", constraint.Constraint); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithClosingParenInPattern_WithDefaultValue_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\))=fsd"); + + // Assert + Assert.Equal("param", templatePart.Name); + + Assert.Equal("fsd", templatePart.DefaultValue); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(\))", constraint.Constraint); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithColonInPattern_ColonIsParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(:)"); + + // Assert + Assert.Equal("param", templatePart.Name); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(:)", constraint.Constraint); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithColonInPattern_WithDefaultValue_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(:)=mnf"); + + // Assert + Assert.Equal("param", templatePart.Name); + + Assert.Equal("mnf", templatePart.DefaultValue); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(:)", constraint.Constraint); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithColonsInPattern_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(a:b:c)"); + + // Assert + Assert.Equal("param", templatePart.Name); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(a:b:c)", constraint.Constraint); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithColonInParamName_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@":param:test=12"); + + // Assert + Assert.Equal(":param", templatePart.Name); + + Assert.Equal("12", templatePart.DefaultValue); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal("test", constraint.Constraint); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithTwoColonInParamName_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@":param::test=12"); + + // Assert + Assert.Equal(":param", templatePart.Name); + + Assert.Equal("12", templatePart.DefaultValue); + + Assert.Collection(templatePart.InlineConstraints, + constraint => Assert.Empty(constraint.Constraint), + constraint => Assert.Equal("test", constraint.Constraint)); + } + + [Fact] + public void ParseRouteParameter_EmptyConstraint_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@":param:test:"); + + // Assert + Assert.Equal(":param", templatePart.Name); + + Assert.Collection(templatePart.InlineConstraints, + constraint => Assert.Equal("test", constraint.Constraint), + constraint => Assert.Empty(constraint.Constraint)); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithCommaInPattern_PatternIsParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\w,\w)"); + + // Assert + Assert.Equal("param", templatePart.Name); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(\w,\w)", constraint.Constraint); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithCommaInName_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"par,am:test(\w)"); + + // Assert + Assert.Equal("par,am", templatePart.Name); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(\w)", constraint.Constraint); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithCommaInPattern_WithDefaultValue_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\w,\w)=jsd"); + + // Assert + Assert.Equal("param", templatePart.Name); + + Assert.Equal("jsd", templatePart.DefaultValue); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(\w,\w)", constraint.Constraint); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithEqualsFollowedByQuestionMark_PatternIsParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:int=?"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Equal("", templatePart.DefaultValue); + + Assert.True(templatePart.IsOptional); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal("int", constraint.Constraint); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithEqualsSignInPattern_PatternIsParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(=)"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Null(templatePart.DefaultValue); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal("test(=)", constraint.Constraint); + } + + [Fact] + public void ParseRouteParameter_EqualsSignInDefaultValue_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param=test=bar"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Equal("test=bar", templatePart.DefaultValue); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithEqualEqualSignInPattern_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(a==b)"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Null(templatePart.DefaultValue); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal("test(a==b)", constraint.Constraint); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithEqualEqualSignInPattern_WithDefaultValue_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(a==b)=dvds"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Equal("dvds", templatePart.DefaultValue); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal("test(a==b)", constraint.Constraint); + } + + [Fact] + public void ParseRouteParameter_EqualEqualSignInName_WithDefaultValue_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"par==am:test=dvds"); + + // Assert + Assert.Equal("par", templatePart.Name); + Assert.Equal("=am:test=dvds", templatePart.DefaultValue); + } + + [Fact] + public void ParseRouteParameter_EqualEqualSignInDefaultValue_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test==dvds"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Equal("=dvds", templatePart.DefaultValue); + } + + [Fact] + public void ParseRouteParameter_DefaultValueWithColonAndParens_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"par=am:test(asd)"); + + // Assert + Assert.Equal("par", templatePart.Name); + Assert.Equal("am:test(asd)", templatePart.DefaultValue); + } + + [Fact] + public void ParseRouteParameter_DefaultValueWithEqualsSignIn_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"par=test(am):est=asd"); + + // Assert + Assert.Equal("par", templatePart.Name); + Assert.Equal("test(am):est=asd", templatePart.DefaultValue); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithEqualsSignInPattern_WithDefaultValue_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(=)=sds"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Equal("sds", templatePart.DefaultValue); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal("test(=)", constraint.Constraint); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithOpenBraceInPattern_PatternIsParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\{)"); + + // Assert + Assert.Equal("param", templatePart.Name); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(\{)", constraint.Constraint); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithOpenBraceInName_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"par{am:test(\sd)"); + + // Assert + Assert.Equal("par{am", templatePart.Name); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(\sd)", constraint.Constraint); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithOpenBraceInPattern_WithDefaultValue_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\{)=xvc"); + + // Assert + Assert.Equal("param", templatePart.Name); + + Assert.Equal("xvc", templatePart.DefaultValue); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(\{)", constraint.Constraint); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithOpenParenInName_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"par(am:test(\()"); + + // Assert + Assert.Equal("par(am", templatePart.Name); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(\()", constraint.Constraint); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithOpenParenInPattern_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\()"); + + // Assert + Assert.Equal("param", templatePart.Name); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(\()", constraint.Constraint); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithOpenParenNoCloseParen_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(#$%"); + + // Assert + Assert.Equal("param", templatePart.Name); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal("test(#$%", constraint.Constraint); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithOpenParenAndColon_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(#:test1"); + + // Assert + Assert.Equal("param", templatePart.Name); + + Assert.Collection(templatePart.InlineConstraints, + constraint => Assert.Equal(@"test(#", constraint.Constraint), + constraint => Assert.Equal(@"test1", constraint.Constraint)); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithOpenParenAndColonWithDefaultValue_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(abc:somevalue):name(test1:differentname=default-value"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Equal("default-value", templatePart.DefaultValue); + + Assert.Collection(templatePart.InlineConstraints, + constraint => Assert.Equal(@"test(abc:somevalue)", constraint.Constraint), + constraint => Assert.Equal(@"name(test1", constraint.Constraint), + constraint => Assert.Equal(@"differentname", constraint.Constraint)); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithOpenParenAndDefaultValue_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(constraintvalue=test1"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Equal("test1", templatePart.DefaultValue); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(constraintvalue", constraint.Constraint); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithOpenParenInPattern_WithDefaultValue_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\()=djk"); + + // Assert + Assert.Equal("param", templatePart.Name); + + Assert.Equal("djk", templatePart.DefaultValue); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(\()", constraint.Constraint); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithQuestionMarkInPattern_PatternIsParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\?)"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Null(templatePart.DefaultValue); + Assert.False(templatePart.IsOptional); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(\?)", constraint.Constraint); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithQuestionMarkInPattern_Optional_PatternIsParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\?)?"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Null(templatePart.DefaultValue); + Assert.True(templatePart.IsOptional); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(\?)", constraint.Constraint); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithQuestionMarkInPattern_WithDefaultValue_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\?)=sdf"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Equal("sdf", templatePart.DefaultValue); + Assert.False(templatePart.IsOptional); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(\?)", constraint.Constraint); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithQuestionMarkInPattern_Optional_WithDefaultValue_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\?)=sdf?"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Equal("sdf", templatePart.DefaultValue); + Assert.True(templatePart.IsOptional); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(\?)", constraint.Constraint); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithQuestionMarkInName_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"par?am:test(\?)"); + + // Assert + Assert.Equal("par?am", templatePart.Name); + Assert.Null(templatePart.DefaultValue); + Assert.False(templatePart.IsOptional); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(\?)", constraint.Constraint); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithClosedParenAndColonInPattern_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(#):$)"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Null(templatePart.DefaultValue); + Assert.False(templatePart.IsOptional); + + Assert.Collection(templatePart.InlineConstraints, + constraint => Assert.Equal(@"test(#)", constraint.Constraint), + constraint => Assert.Equal(@"$)", constraint.Constraint)); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithColonAndClosedParenInPattern_PatternIsParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(#:)$)"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Null(templatePart.DefaultValue); + Assert.False(templatePart.IsOptional); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(#:)$)", constraint.Constraint); + } + + [Fact] + public void ParseRouteParameter_ContainingMultipleUnclosedParenthesisInConstraint() + { + // Arrange & Act + var templatePart = ParseParameter(@"foo:regex(\\(\\(\\(\\()"); + + // Assert + Assert.Equal("foo", templatePart.Name); + Assert.Null(templatePart.DefaultValue); + Assert.False(templatePart.IsOptional); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"regex(\\(\\(\\(\\()", constraint.Constraint); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithBraces_PatternIsParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)"); // ssn + + // Assert + Assert.Equal("p1", templatePart.Name); + Assert.Null(templatePart.DefaultValue); + Assert.False(templatePart.IsOptional); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"regex(^\d{{3}}-\d{{3}}-\d{{4}}$)", constraint.Constraint); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithBraces_WithDefaultValue() + { + // Arrange & Act + var templatePart = ParseParameter(@"p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)=123-456-7890"); // ssn + + // Assert + Assert.Equal("p1", templatePart.Name); + Assert.Equal("123-456-7890", templatePart.DefaultValue); + Assert.False(templatePart.IsOptional); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"regex(^\d{{3}}-\d{{3}}-\d{{4}}$)", constraint.Constraint); + } + + [Theory] + [InlineData("", "")] + [InlineData("?", "")] + [InlineData("*", "")] + [InlineData(" ", " ")] + [InlineData("\t", "\t")] + [InlineData("#!@#$%Q@#@%", "#!@#$%Q@#@%")] + [InlineData(",,,", ",,,")] + public void ParseRouteParameter_ParameterWithoutInlineConstraint_ReturnsTemplatePartWithEmptyInlineValues( + string parameter, + string expectedParameterName) + { + // Arrange & Act + var templatePart = ParseParameter(parameter); + + // Assert + Assert.Equal(expectedParameterName, templatePart.Name); + Assert.Empty(templatePart.InlineConstraints); + Assert.Null(templatePart.DefaultValue); + } + + + private TemplatePart ParseParameter(string routeParameter) + { + var _constraintResolver = GetConstraintResolver(); + var templatePart = InlineRouteParameterParser.ParseRouteParameter(routeParameter); + return templatePart; + } + + private static RouteTemplate ParseRouteTemplate(string template) + { + var _constraintResolver = GetConstraintResolver(); + return TemplateParser.Parse(template); + } + + private static IInlineConstraintResolver GetConstraintResolver() + { + var services = new ServiceCollection().AddOptions(); + services.Configure(options => + options + .ConstraintMap + .Add("test", typeof(TestRouteConstraint))); + var serviceProvider = services.BuildServiceProvider(); + var accessor = serviceProvider.GetRequiredService>(); + return new DefaultInlineConstraintResolver(accessor); + } + + private class TestRouteConstraint : IRouteConstraint + { + public TestRouteConstraint(string pattern) + { + Pattern = pattern; + } + + public string Pattern { get; private set; } + public bool Match(HttpContext httpContext, + IRouter route, + string routeKey, + RouteValueDictionary values, + RouteDirection routeDirection) + { + throw new NotImplementedException(); + } + } + } +} diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Internal/LinkGenerationDecisionTreeTest.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Internal/LinkGenerationDecisionTreeTest.cs new file mode 100644 index 0000000000..8a35edcf13 --- /dev/null +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Internal/LinkGenerationDecisionTreeTest.cs @@ -0,0 +1,339 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing.Template; +using Microsoft.AspNetCore.Routing.Tree; +using Xunit; + +namespace Microsoft.AspNetCore.Routing.Internal.Routing +{ + public class LinkGenerationDecisionTreeTest + { + [Fact] + public void SelectSingleEntry_NoCriteria() + { + // Arrange + var entries = new List(); + + var entry = CreateMatch(new { }); + entries.Add(entry); + + var tree = new LinkGenerationDecisionTree(entries); + + var context = CreateContext(new { }); + + // Act + var matches = tree.GetMatches(context); + + // Assert + Assert.Same(entry, Assert.Single(matches).Match); + } + + [Fact] + public void SelectSingleEntry_MultipleCriteria() + { + // Arrange + var entries = new List(); + + var entry = CreateMatch(new { controller = "Store", action = "Buy" }); + entries.Add(entry); + + var tree = new LinkGenerationDecisionTree(entries); + + var context = CreateContext(new { controller = "Store", action = "Buy" }); + + // Act + var matches = tree.GetMatches(context); + + // Assert + Assert.Same(entry, Assert.Single(matches).Match); + } + + [Fact] + public void SelectSingleEntry_MultipleCriteria_AmbientValues() + { + // Arrange + var entries = new List(); + + var entry = CreateMatch(new { controller = "Store", action = "Buy" }); + entries.Add(entry); + + var tree = new LinkGenerationDecisionTree(entries); + + var context = CreateContext(values: null, ambientValues: new { controller = "Store", action = "Buy" }); + + // Act + var matches = tree.GetMatches(context); + + // Assert + var match = Assert.Single(matches); + Assert.Same(entry, match.Match); + Assert.False(match.IsFallbackMatch); + } + + [Fact] + public void SelectSingleEntry_MultipleCriteria_Replaced() + { + // Arrange + var entries = new List(); + + var entry = CreateMatch(new { controller = "Store", action = "Buy" }); + entries.Add(entry); + + var tree = new LinkGenerationDecisionTree(entries); + + var context = CreateContext( + values: new { action = "Buy" }, + ambientValues: new { controller = "Store", action = "Cart" }); + + // Act + var matches = tree.GetMatches(context); + + // Assert + var match = Assert.Single(matches); + Assert.Same(entry, match.Match); + Assert.False(match.IsFallbackMatch); + } + + [Fact] + public void SelectSingleEntry_MultipleCriteria_AmbientValue_Ignored() + { + // Arrange + var entries = new List(); + + var entry = CreateMatch(new { controller = "Store", action = (string)null }); + entries.Add(entry); + + var tree = new LinkGenerationDecisionTree(entries); + + var context = CreateContext( + values: new { controller = "Store" }, + ambientValues: new { controller = "Store", action = "Buy" }); + + // Act + var matches = tree.GetMatches(context); + + // Assert + var match = Assert.Single(matches); + Assert.Same(entry, match.Match); + Assert.True(match.IsFallbackMatch); + } + + [Fact] + public void SelectSingleEntry_MultipleCriteria_NoMatch() + { + // Arrange + var entries = new List(); + + var entry = CreateMatch(new { controller = "Store", action = "Buy" }); + entries.Add(entry); + + var tree = new LinkGenerationDecisionTree(entries); + + var context = CreateContext(new { controller = "Store", action = "AddToCart" }); + + // Act + var matches = tree.GetMatches(context); + + // Assert + Assert.Empty(matches); + } + + [Fact] + public void SelectSingleEntry_MultipleCriteria_AmbientValue_NoMatch() + { + // Arrange + var entries = new List(); + + var entry = CreateMatch(new { controller = "Store", action = "Buy" }); + entries.Add(entry); + + var tree = new LinkGenerationDecisionTree(entries); + + var context = CreateContext( + values: new { controller = "Store" }, + ambientValues: new { controller = "Store", action = "Cart" }); + + // Act + var matches = tree.GetMatches(context); + + // Assert + Assert.Empty(matches); + } + + [Fact] + public void SelectMultipleEntries_OneDoesntMatch() + { + // Arrange + var entries = new List(); + + var entry1 = CreateMatch(new { controller = "Store", action = "Buy" }); + entries.Add(entry1); + + var entry2 = CreateMatch(new { controller = "Store", action = "Cart" }); + entries.Add(entry2); + + var tree = new LinkGenerationDecisionTree(entries); + + var context = CreateContext( + values: new { controller = "Store" }, + ambientValues: new { controller = "Store", action = "Buy" }); + + // Act + var matches = tree.GetMatches(context); + + // Assert + Assert.Same(entry1, Assert.Single(matches).Match); + } + + [Fact] + public void SelectMultipleEntries_BothMatch_CriteriaSubset() + { + // Arrange + var entries = new List(); + + var entry1 = CreateMatch(new { controller = "Store", action = "Buy" }); + entries.Add(entry1); + + var entry2 = CreateMatch(new { controller = "Store" }); + entry2.Entry.Order = 1; + entries.Add(entry2); + + var tree = new LinkGenerationDecisionTree(entries); + + var context = CreateContext( + values: new { controller = "Store" }, + ambientValues: new { controller = "Store", action = "Buy" }); + + // Act + var matches = tree.GetMatches(context).Select(m => m.Match).ToList(); + + // Assert + Assert.Equal(entries, matches); + } + + [Fact] + public void SelectMultipleEntries_BothMatch_NonOverlappingCriteria() + { + // Arrange + var entries = new List(); + + var entry1 = CreateMatch(new { controller = "Store", action = "Buy" }); + entries.Add(entry1); + + var entry2 = CreateMatch(new { slug = "1234" }); + entry2.Entry.Order = 1; + entries.Add(entry2); + + var tree = new LinkGenerationDecisionTree(entries); + + var context = CreateContext(new { controller = "Store", action = "Buy", slug = "1234" }); + + // Act + var matches = tree.GetMatches(context).Select(m => m.Match).ToList(); + + // Assert + Assert.Equal(entries, matches); + } + + // Precedence is ignored for sorting because they have different order + [Fact] + public void SelectMultipleEntries_BothMatch_OrderedByOrder() + { + // Arrange + var entries = new List(); + + var entry1 = CreateMatch(new { controller = "Store", action = "Buy" }); + entry1.Entry.Precedence = 0; + entries.Add(entry1); + + var entry2 = CreateMatch(new { controller = "Store", action = "Buy" }); + entry2.Entry.Order = 1; + entry2.Entry.Precedence = 1; + entries.Add(entry2); + + var tree = new LinkGenerationDecisionTree(entries); + + var context = CreateContext(new { controller = "Store", action = "Buy" }); + + // Act + var matches = tree.GetMatches(context).Select(m => m.Match).ToList(); + + // Assert + Assert.Equal(entries, matches); + } + + // Precedence is used for sorting because they have the same order + [Fact] + public void SelectMultipleEntries_BothMatch_OrderedByPrecedence() + { + // Arrange + var entries = new List(); + + var entry1 = CreateMatch(new { controller = "Store", action = "Buy" }); + entry1.Entry.Precedence = 1; + entries.Add(entry1); + + var entry2 = CreateMatch(new { controller = "Store", action = "Buy" }); + entry2.Entry.Precedence = 0; + entries.Add(entry2); + + var tree = new LinkGenerationDecisionTree(entries); + + var context = CreateContext(new { controller = "Store", action = "Buy" }); + + // Act + var matches = tree.GetMatches(context).Select(m => m.Match).ToList(); + + // Assert + Assert.Equal(entries, matches); + } + + // Template is used for sorting because they have the same order + [Fact] + public void SelectMultipleEntries_BothMatch_OrderedByTemplate() + { + // Arrange + var entries = new List(); + + var entry1 = CreateMatch(new { controller = "Store", action = "Buy" }); + entry1.Entry.RouteTemplate = TemplateParser.Parse("a"); + entries.Add(entry1); + + var entry2 = CreateMatch(new { controller = "Store", action = "Buy" }); + entry2.Entry.RouteTemplate = TemplateParser.Parse("b"); + entries.Add(entry2); + + var tree = new LinkGenerationDecisionTree(entries); + + var context = CreateContext(new { controller = "Store", action = "Buy" }); + + // Act + var matches = tree.GetMatches(context).Select(m => m.Match).ToList(); + + // Assert + Assert.Equal(entries, matches); + } + + private OutboundMatch CreateMatch(object requiredValues) + { + var match = new OutboundMatch(); + match.Entry = new OutboundRouteEntry(); + match.Entry.RequiredLinkValues = new RouteValueDictionary(requiredValues); + return match; + } + + private VirtualPathContext CreateContext(object values, object ambientValues = null) + { + var context = new VirtualPathContext( + new DefaultHttpContext(), + new RouteValueDictionary(ambientValues), + new RouteValueDictionary(values)); + + return context; + } + } +} \ No newline at end of file diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Internal/PathTokenizerTest.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Internal/PathTokenizerTest.cs new file mode 100644 index 0000000000..78b9685e63 --- /dev/null +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Internal/PathTokenizerTest.cs @@ -0,0 +1,117 @@ +// 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.Extensions.Primitives; +using Xunit; + +namespace Microsoft.AspNetCore.Routing.Internal +{ + public class PathTokenizerTest + { + public static TheoryData TokenizationData + { + get + { + return new TheoryData + { + { string.Empty, new StringSegment[] { } }, + { "/", new StringSegment[] { } }, + { "//", new StringSegment[] { new StringSegment("//", 1, 0) } }, + { + "///", + new StringSegment[] + { + new StringSegment("///", 1, 0), + new StringSegment("///", 2, 0), + } + }, + { + "////", + new StringSegment[] + { + new StringSegment("////", 1, 0), + new StringSegment("////", 2, 0), + new StringSegment("////", 3, 0), + } + }, + { "/zero", new StringSegment[] { new StringSegment("/zero", 1, 4) } }, + { "/zero/", new StringSegment[] { new StringSegment("/zero/", 1, 4) } }, + { + "/zero/one", + new StringSegment[] + { + new StringSegment("/zero/one", 1, 4), + new StringSegment("/zero/one", 6, 3), + } + }, + { + "/zero/one/", + new StringSegment[] + { + new StringSegment("/zero/one/", 1, 4), + new StringSegment("/zero/one/", 6, 3), + } + }, + { + "/zero/one/two", + new StringSegment[] + { + new StringSegment("/zero/one/two", 1, 4), + new StringSegment("/zero/one/two", 6, 3), + new StringSegment("/zero/one/two", 10, 3), + } + }, + { + "/zero/one/two/", + new StringSegment[] + { + new StringSegment("/zero/one/two/", 1, 4), + new StringSegment("/zero/one/two/", 6, 3), + new StringSegment("/zero/one/two/", 10, 3), + } + }, + }; + } + } + + [Theory] + [MemberData(nameof(TokenizationData))] + public void PathTokenizer_Count(string path, StringSegment[] expectedSegments) + { + // Arrange + var tokenizer = new PathTokenizer(new PathString(path)); + + // Act + var count = tokenizer.Count; + + // Assert + Assert.Equal(expectedSegments.Length, count); + } + + [Theory] + [MemberData(nameof(TokenizationData))] + public void PathTokenizer_Indexer(string path, StringSegment[] expectedSegments) + { + // Arrange + var tokenizer = new PathTokenizer(new PathString(path)); + + // Act & Assert + for (var i = 0; i < expectedSegments.Length; i++) + { + Assert.Equal(expectedSegments[i], tokenizer[i]); + } + } + + [Theory] + [MemberData(nameof(TokenizationData))] + public void PathTokenizer_Enumerator(string path, StringSegment[] expectedSegments) + { + // Arrange + var tokenizer = new PathTokenizer(new PathString(path)); + + // Act & Assert + Assert.Equal(expectedSegments, tokenizer); + } + } +} diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Logging/WriteContext.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Logging/WriteContext.cs new file mode 100644 index 0000000000..4a5fa51e04 --- /dev/null +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Logging/WriteContext.cs @@ -0,0 +1,25 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Routing +{ + public class WriteContext + { + public LogLevel LogLevel { get; set; } + + public int EventId { get; set; } + + public object State { get; set; } + + public Exception Exception { get; set; } + + public Func Formatter { get; set; } + + public object Scope { get; set; } + + public string LoggerName { get; set; } + } +} \ No newline at end of file diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Microsoft.AspNetCore.Routing.Tests.csproj b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Microsoft.AspNetCore.Routing.Tests.csproj new file mode 100644 index 0000000000..ee389c233d --- /dev/null +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Microsoft.AspNetCore.Routing.Tests.csproj @@ -0,0 +1,19 @@ + + + + $(StandardTestTfms) + + + + + + + + + + + + + + + diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RequestDelegateRouteBuilderExtensionsTest.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RequestDelegateRouteBuilderExtensionsTest.cs new file mode 100644 index 0000000000..534c193a93 --- /dev/null +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RequestDelegateRouteBuilderExtensionsTest.cs @@ -0,0 +1,159 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.ObjectPool; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Routing +{ + // These are really more like integration tests. They verify that these extensions + // add routes that behave as advertised. + public class RequestDelegateRouteBuilderExtensionsTest + { + private static readonly RequestDelegate NullHandler = (c) => Task.FromResult(0); + + public static TheoryData, Action> MatchingActions + { + get + { + return new TheoryData, Action>() + { + { b => { b.MapRoute("api/{id}", NullHandler); }, null }, + { b => { b.MapMiddlewareRoute("api/{id}", app => { }); }, null }, + + { b => { b.MapDelete("api/{id}", NullHandler); }, c => { c.Request.Method = "DELETE"; } }, + { b => { b.MapMiddlewareDelete("api/{id}", app => { }); }, c => { c.Request.Method = "DELETE"; } }, + { b => { b.MapGet("api/{id}", NullHandler); }, c => { c.Request.Method = "GET"; } }, + { b => { b.MapMiddlewareGet("api/{id}", app => { }); }, c => { c.Request.Method = "GET"; } }, + { b => { b.MapPost("api/{id}", NullHandler); }, c => { c.Request.Method = "POST"; } }, + { b => { b.MapMiddlewarePost("api/{id}", app => { }); }, c => { c.Request.Method = "POST"; } }, + { b => { b.MapPut("api/{id}", NullHandler); }, c => { c.Request.Method = "PUT"; } }, + { b => { b.MapMiddlewarePut("api/{id}", app => { }); }, c => { c.Request.Method = "PUT"; } }, + + { b => { b.MapVerb("PUT", "api/{id}", NullHandler); }, c => { c.Request.Method = "PUT"; } }, + { b => { b.MapMiddlewareVerb("PUT", "api/{id}", app => { }); }, c => { c.Request.Method = "PUT"; } }, + }; + } + } + + [Theory] + [MemberData(nameof(MatchingActions))] + public async Task Map_MatchesRequest( + Action routeSetup, + Action requestSetup) + { + // Arrange + var services = CreateServices(); + + var context = CreateRouteContext(services); + context.HttpContext.Request.Path = new PathString("/api/5"); + requestSetup?.Invoke(context.HttpContext); + + var builder = CreateRouteBuilder(services); + routeSetup(builder); + var route = builder.Build(); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.Same(NullHandler, context.Handler); + } + + public static TheoryData, Action> NonmatchingActions + { + get + { + return new TheoryData, Action>() + { + { b => { b.MapRoute("api/{id}/extra", NullHandler); }, null }, + { b => { b.MapMiddlewareRoute("api/{id}/extra", app => { }); }, null }, + + { b => { b.MapDelete("api/{id}", NullHandler); }, c => { c.Request.Method = "GET"; } }, + { b => { b.MapMiddlewareDelete("api/{id}", app => { }); }, c => { c.Request.Method = "PUT"; } }, + { b => { b.MapDelete("api/{id}/extra", NullHandler); }, c => { c.Request.Method = "DELETE"; } }, + { b => { b.MapMiddlewareDelete("api/{id}/extra", app => { }); }, c => { c.Request.Method = "DELETE"; } }, + { b => { b.MapGet("api/{id}", NullHandler); }, c => { c.Request.Method = "PUT"; } }, + { b => { b.MapMiddlewareGet("api/{id}", app => { }); }, c => { c.Request.Method = "POST"; } }, + { b => { b.MapGet("api/{id}/extra", NullHandler); }, c => { c.Request.Method = "GET"; } }, + { b => { b.MapMiddlewareGet("api/{id}/extra", app => { }); }, c => { c.Request.Method = "GET"; } }, + { b => { b.MapPost("api/{id}", NullHandler); }, c => { c.Request.Method = "MEH"; } }, + { b => { b.MapMiddlewarePost("api/{id}", app => { }); }, c => { c.Request.Method = "DELETE"; } }, + { b => { b.MapPost("api/{id}/extra", NullHandler); }, c => { c.Request.Method = "POST"; } }, + { b => { b.MapMiddlewarePost("api/{id}/extra", app => { }); }, c => { c.Request.Method = "POST"; } }, + { b => { b.MapPut("api/{id}", NullHandler); }, c => { c.Request.Method = "BLEH"; } }, + { b => { b.MapMiddlewarePut("api/{id}", app => { }); }, c => { c.Request.Method = "HEAD"; } }, + { b => { b.MapPut("api/{id}/extra", NullHandler); }, c => { c.Request.Method = "PUT"; } }, + { b => { b.MapMiddlewarePut("api/{id}/extra", app => { }); }, c => { c.Request.Method = "PUT"; } }, + + { b => { b.MapVerb("PUT", "api/{id}", NullHandler); }, c => { c.Request.Method = "POST"; } }, + { b => { b.MapMiddlewareVerb("PUT", "api/{id}", app => { }); }, c => { c.Request.Method = "HEAD"; } }, + { b => { b.MapVerb("PUT", "api/{id}/extra", NullHandler); }, c => { c.Request.Method = "PUT"; } }, + { b => { b.MapMiddlewareVerb("PUT", "api/{id}/extra", app => { }); }, c => { c.Request.Method = "PUT"; } }, + }; + } + } + + [Theory] + [MemberData(nameof(NonmatchingActions))] + public async Task Map_DoesNotMatchRequest( + Action routeSetup, + Action requestSetup) + { + // Arrange + var services = CreateServices(); + + var context = CreateRouteContext(services); + context.HttpContext.Request.Path = new PathString("/api/5"); + requestSetup?.Invoke(context.HttpContext); + + var builder = CreateRouteBuilder(services); + routeSetup(builder); + var route = builder.Build(); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.Null(context.Handler); + } + + private static IServiceProvider CreateServices() + { + var services = new ServiceCollection(); + services.AddSingleton(); + services.AddOptions(); + services.AddRouting(); + services.AddLogging(); + return services.BuildServiceProvider(); + } + + private static RouteContext CreateRouteContext(IServiceProvider services) + { + var httpContext = new DefaultHttpContext(); + httpContext.RequestServices = services; + return new RouteContext(httpContext); + } + + private static IRouteBuilder CreateRouteBuilder(IServiceProvider services) + { + var applicationBuilder = new Mock(); + applicationBuilder.SetupAllProperties(); + + applicationBuilder + .Setup(b => b.New().Build()) + .Returns(NullHandler); + + applicationBuilder.Object.ApplicationServices = services; + + var routeBuilder = new RouteBuilder(applicationBuilder.Object); + return routeBuilder; + } + } +} diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RouteBuilderTest.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RouteBuilderTest.cs new file mode 100644 index 0000000000..edd3e09cba --- /dev/null +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RouteBuilderTest.cs @@ -0,0 +1,55 @@ +// 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.Builder; +using Microsoft.AspNetCore.Routing.Internal; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Routing +{ + public class RouteBuilderTest + { + [Fact] + public void Ctor_SetsPropertyValues() + { + // Arrange + var services = new ServiceCollection(); + services.AddSingleton(typeof(RoutingMarkerService)); + var applicationServices = services.BuildServiceProvider(); + var applicationBuilderMock = new Mock(); + applicationBuilderMock.Setup(a => a.ApplicationServices).Returns(applicationServices); + var applicationBuilder = applicationBuilderMock.Object; + var defaultHandler = Mock.Of(); + + // Act + var builder = new RouteBuilder(applicationBuilder, defaultHandler); + + // Assert + Assert.Same(applicationBuilder, builder.ApplicationBuilder); + Assert.Same(defaultHandler, builder.DefaultHandler); + Assert.Same(applicationServices, builder.ServiceProvider); + } + + [Fact] + public void Ctor_ThrowsInvalidOperationException_IfRoutingMarkerServiceIsNotRegistered() + { + // Arrange + var applicationBuilderMock = new Mock(); + applicationBuilderMock + .Setup(s => s.ApplicationServices) + .Returns(Mock.Of()); + + // Act & Assert + var exception = Assert.Throws(() => new RouteBuilder(applicationBuilderMock.Object)); + + Assert.Equal( + "Unable to find the required services. Please add all the required services by calling " + + "'IServiceCollection.AddRouting' inside the call to 'ConfigureServices(...)'" + + " in the application startup code.", + exception.Message); + } + } +} diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RouteCollectionTest.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RouteCollectionTest.cs new file mode 100644 index 0000000000..c0d5a0187a --- /dev/null +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RouteCollectionTest.cs @@ -0,0 +1,675 @@ +// 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.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Logging.Testing; +using Microsoft.Extensions.ObjectPool; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Routing +{ + public class RouteCollectionTest + { + private static readonly RequestDelegate NullHandler = (c) => Task.FromResult(0); + + [Theory] + [InlineData(@"Home/Index/23", "/home/index/23", true, false)] + [InlineData(@"Home/Index/23", "/Home/Index/23", false, false)] + [InlineData(@"Home/Index/23", "/home/index/23/", true, true)] + [InlineData(@"Home/Index/23", "/Home/Index/23/", false, true)] + [InlineData(@"Home/Index/23?Param1=ABC&Param2=Xyz", "/Home/Index/23/?Param1=ABC&Param2=Xyz", false, true)] + [InlineData(@"Home/Index/23?Param1=ABC&Param2=Xyz", "/Home/Index/23?Param1=ABC&Param2=Xyz", false, false)] + [InlineData(@"Home/Index/23?Param1=ABC&Param2=Xyz", "/home/index/23/?Param1=ABC&Param2=Xyz", true, true)] + [InlineData(@"Home/Index/23#Param1=ABC&Param2=Xyz", "/Home/Index/23/#Param1=ABC&Param2=Xyz", false, true)] + [InlineData(@"Home/Index/23#Param1=ABC&Param2=Xyz", "/home/index/23#Param1=ABC&Param2=Xyz", true, false)] + [InlineData(@"Home/Index/23/?Param1=ABC&Param2=Xyz", "/home/index/23/?Param1=ABC&Param2=Xyz", true, true)] + [InlineData(@"Home/Index/23/#Param1=ABC&Param2=Xyz", "/home/index/23/#Param1=ABC&Param2=Xyz", true, false)] + public void GetVirtualPath_CanLowerCaseUrls_And_AppendTrailingSlash_BasedOnOptions( + string returnUrl, + string expectedUrl, + bool lowercaseUrls, + bool appendTrailingSlash) + { + // Arrange + var target = new Mock(MockBehavior.Strict); + target + .Setup(e => e.GetVirtualPath(It.IsAny())) + .Returns(new VirtualPathData(target.Object, returnUrl)); + + var routeCollection = new RouteCollection(); + routeCollection.Add(target.Object); + var virtualPathContext = CreateVirtualPathContext( + options: GetRouteOptions( + lowerCaseUrls: lowercaseUrls, + appendTrailingSlash: appendTrailingSlash)); + + // Act + var pathData = routeCollection.GetVirtualPath(virtualPathContext); + + // Assert + Assert.Equal(expectedUrl, pathData.VirtualPath); + Assert.Same(target.Object, pathData.Router); + Assert.Empty(pathData.DataTokens); + } + + [Theory] + [InlineData(@"\u0130", @"/\u0130", true)] + [InlineData(@"\u0049", @"/\u0049", true)] + [InlineData(@"�ino", @"/�ino", true)] + public void GetVirtualPath_DoesntLowerCaseUrls_Invariant( + string returnUrl, + string lowercaseUrl, + bool lowercaseUrls) + { + // Arrange + var target = new Mock(MockBehavior.Strict); + target + .Setup(e => e.GetVirtualPath(It.IsAny())) + .Returns(new VirtualPathData(target.Object, returnUrl)); + + var routeCollection = new RouteCollection(); + routeCollection.Add(target.Object); + var virtualPathContext = CreateVirtualPathContext(options: GetRouteOptions(lowercaseUrls)); + + // Act + var pathData = routeCollection.GetVirtualPath(virtualPathContext); + + // Assert + Assert.Equal(lowercaseUrl, pathData.VirtualPath); + Assert.Same(target.Object, pathData.Router); + Assert.Empty(pathData.DataTokens); + } + + [Theory] + [MemberData(nameof(DataTokensTestData))] + public void GetVirtualPath_ReturnsDataTokens(RouteValueDictionary dataTokens, string routerName) + { + // Arrange + var virtualPath = "/TestVirtualPath"; + + var pathContextValues = new RouteValueDictionary { { "controller", virtualPath } }; + + var pathContext = CreateVirtualPathContext( + pathContextValues, + GetRouteOptions(), + routerName); + + var route = CreateTemplateRoute("{controller}", routerName, dataTokens); + var routeCollection = new RouteCollection(); + routeCollection.Add(route); + + var expectedDataTokens = dataTokens ?? new RouteValueDictionary(); + + // Act + var pathData = routeCollection.GetVirtualPath(pathContext); + + // Assert + Assert.NotNull(pathData); + Assert.Same(route, pathData.Router); + + Assert.Equal(virtualPath, pathData.VirtualPath); + + Assert.Equal(expectedDataTokens.Count, pathData.DataTokens.Count); + foreach (var dataToken in expectedDataTokens) + { + Assert.True(pathData.DataTokens.ContainsKey(dataToken.Key)); + Assert.Equal(dataToken.Value, pathData.DataTokens[dataToken.Key]); + } + } + + [Fact] + public async Task RouteAsync_FirstMatches() + { + // Arrange + var routes = new RouteCollection(); + + var route1 = CreateRoute(accept: true); + routes.Add(route1.Object); + + var route2 = CreateRoute(accept: false); + routes.Add(route2.Object); + + var context = CreateRouteContext("/Cool"); + + // Act + await routes.RouteAsync(context); + + // Assert + route1.Verify(e => e.RouteAsync(It.IsAny()), Times.Exactly(1)); + route2.Verify(e => e.RouteAsync(It.IsAny()), Times.Exactly(0)); + Assert.NotNull(context.Handler); + + Assert.Equal(1, context.RouteData.Routers.Count); + Assert.Same(route1.Object, context.RouteData.Routers[0]); + } + + [Fact] + public async Task RouteAsync_SecondMatches() + { + // Arrange + + var routes = new RouteCollection(); + var route1 = CreateRoute(accept: false); + routes.Add(route1.Object); + + var route2 = CreateRoute(accept: true); + routes.Add(route2.Object); + + var context = CreateRouteContext("/Cool"); + + // Act + await routes.RouteAsync(context); + + // Assert + route1.Verify(e => e.RouteAsync(It.IsAny()), Times.Exactly(1)); + route2.Verify(e => e.RouteAsync(It.IsAny()), Times.Exactly(1)); + Assert.NotNull(context.Handler); + + Assert.Equal(1, context.RouteData.Routers.Count); + Assert.Same(route2.Object, context.RouteData.Routers[0]); + } + + [Fact] + public async Task RouteAsync_NoMatch() + { + // Arrange + var routes = new RouteCollection(); + var route1 = CreateRoute(accept: false); + routes.Add(route1.Object); + + var route2 = CreateRoute(accept: false); + routes.Add(route2.Object); + + var context = CreateRouteContext("/Cool"); + + // Act + await routes.RouteAsync(context); + + // Assert + route1.Verify(e => e.RouteAsync(It.IsAny()), Times.Exactly(1)); + route2.Verify(e => e.RouteAsync(It.IsAny()), Times.Exactly(1)); + Assert.Null(context.Handler); + + Assert.Empty(context.RouteData.Routers); + } + + [Theory] + [InlineData(false, "/RouteName")] + [InlineData(true, "/routename")] + public void NamedRouteTests_GetNamedRoute_ReturnsValue(bool lowercaseUrls, string expectedUrl) + { + // Arrange + var routeCollection = GetNestedRouteCollection(new string[] { "Route1", "Route2", "RouteName", "Route3" }); + var virtualPathContext = CreateVirtualPathContext( + routeName: "RouteName", + options: GetRouteOptions(lowercaseUrls)); + + // Act + var pathData = routeCollection.GetVirtualPath(virtualPathContext); + + // Assert + Assert.Equal(expectedUrl, pathData.VirtualPath); + var namedRouter = Assert.IsAssignableFrom(pathData.Router); + Assert.Equal(virtualPathContext.RouteName, namedRouter.Name); + Assert.Empty(pathData.DataTokens); + } + + [Fact] + public void NamedRouteTests_GetNamedRoute_RouteNotFound() + { + // Arrange + var routeCollection = GetNestedRouteCollection(new string[] { "Route1", "Route2", "Route3" }); + var virtualPathContext = CreateVirtualPathContext("NonExistantRoute"); + + // Act + var stringVirtualPath = routeCollection.GetVirtualPath(virtualPathContext); + + // Assert + Assert.Null(stringVirtualPath); + } + + [Fact] + public void NamedRouteTests_GetNamedRoute_AmbiguousRoutesInCollection_DoesNotThrowForUnambiguousRoute() + { + // Arrange + var routeCollection = GetNestedRouteCollection(new string[] { "Route1", "Route2", "Route3", "Route4" }); + + // Add Duplicate route. + routeCollection.Add(CreateNamedRoute("Route3")); + var virtualPathContext = CreateVirtualPathContext(routeName: "Route1", options: GetRouteOptions(true)); + + // Act + var pathData = routeCollection.GetVirtualPath(virtualPathContext); + + // Assert + Assert.Equal("/route1", pathData.VirtualPath); + var namedRouter = Assert.IsAssignableFrom(pathData.Router); + Assert.Equal("Route1", namedRouter.Name); + Assert.Empty(pathData.DataTokens); + } + + [Fact] + public void NamedRouteTests_GetNamedRoute_AmbiguousRoutesInCollection_ThrowsForAmbiguousRoute() + { + // Arrange + var ambiguousRoute = "ambiguousRoute"; + var routeCollection = GetNestedRouteCollection(new string[] { "Route1", "Route2", ambiguousRoute, "Route4" }); + + // Add Duplicate route. + routeCollection.Add(CreateNamedRoute(ambiguousRoute)); + var virtualPathContext = CreateVirtualPathContext(routeName: ambiguousRoute, options: GetRouteOptions()); + + // Act & Assert + var ex = Assert.Throws(() => routeCollection.GetVirtualPath(virtualPathContext)); + Assert.Equal( + "The supplied route name 'ambiguousRoute' is ambiguous and matched more than one route.", + ex.Message); + } + + [Fact] + public void GetVirtualPath_AmbiguousRoutes_RequiresRouteValueValidation_Error() + { + // Arrange + var namedRoute = CreateNamedRoute("Ambiguous", accept: false); + + var routeCollection = new RouteCollection(); + routeCollection.Add(namedRoute); + + var innerRouteCollection = new RouteCollection(); + innerRouteCollection.Add(namedRoute); + routeCollection.Add(innerRouteCollection); + + var virtualPathContext = CreateVirtualPathContext("Ambiguous"); + + // Act & Assert + var ex = Assert.Throws(() => routeCollection.GetVirtualPath(virtualPathContext)); + Assert.Equal("The supplied route name 'Ambiguous' is ambiguous and matched more than one route.", ex.Message); + } + + // "Integration" tests for RouteCollection + + public static IEnumerable IntegrationTestData + { + get + { + yield return new object[] { + "{controller}/{action}", + new RouteValueDictionary { { "controller", "Home" }, { "action", "Index" } }, + "/home/index", + true }; + + yield return new object[] { + "{controller}/{action}/", + new RouteValueDictionary { { "controller", "Home" }, { "action", "Index" } }, + "/Home/Index", + false }; + + yield return new object[] { + "api/{action}/", + new RouteValueDictionary { { "action", "Create" } }, + "/api/create", + true }; + + yield return new object[] { + "api/{action}/{id}", + new RouteValueDictionary { + { "action", "Create" }, + { "id", "23" }, + { "Param1", "Value1" }, + { "Param2", "Value2" } }, + "/api/create/23?Param1=Value1&Param2=Value2", + true }; + + yield return new object[] { + "api/{action}/{id}", + new RouteValueDictionary { + { "action", "Create" }, + { "id", "23" }, + { "Param1", "Value1" }, + { "Param2", "Value2" } }, + "/api/Create/23?Param1=Value1&Param2=Value2", + false }; + } + } + + [Theory] + [MemberData(nameof(IntegrationTestData))] + public void GetVirtualPath_Success( + string template, + RouteValueDictionary values, + string expectedUrl, + bool lowercaseUrls) + { + // Arrange + var routeCollection = new RouteCollection(); + var route = CreateTemplateRoute(template); + routeCollection.Add(route); + var context = CreateVirtualPathContext(values, options: GetRouteOptions(lowercaseUrls)); + + // Act + var pathData = routeCollection.GetVirtualPath(context); + + // Assert + Assert.Equal(expectedUrl, pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } + + public static IEnumerable RestoresRouteDataForEachRouterData + { + get + { + // Here 'area' segment doesn't have a value but the later segments have values. This is an invalid + // route match and the url generation should look into the next available route in the collection. + yield return new object[] { + new Route[] + { + CreateTemplateRoute("{area?}/{controller=Home}/{action=Index}/{id?}", "1"), + CreateTemplateRoute("{controller=Home}/{action=Index}/{id?}", "2") + }, + new RouteValueDictionary(new { controller = "Test", action = "Index" }), + "/Test", + "2" }; + + // Here the segment 'a' is valid but 'b' is not as it would be empty. This would be an invalid route match, but + // the route value of 'a' should still be present to be evaluated for the next available route. + yield return new object[] { + new[] + { + CreateTemplateRoute("{a}/{b?}/{c}", "1"), + CreateTemplateRoute("{a=Home}/{b=Index}", "2") + }, + new RouteValueDictionary(new { a = "Test", c = "Foo" }), + "/Test?c=Foo", + "2" }; + } + } + + [Theory] + [MemberData(nameof(RestoresRouteDataForEachRouterData))] + public void GetVirtualPath_RestoresRouteData_ForEachRouter( + Route[] routes, + RouteValueDictionary routeValues, + string expectedUrl, + string expectedRouteToMatch) + { + // Arrange + var routeCollection = new RouteCollection(); + foreach (var route in routes) + { + routeCollection.Add(route); + } + var context = CreateVirtualPathContext(routeValues); + + // Act + var pathData = routeCollection.GetVirtualPath(context); + + // Assert + Assert.Equal(expectedUrl, pathData.VirtualPath); + Assert.Same(expectedRouteToMatch, ((INamedRouter)pathData.Router).Name); + Assert.Empty(pathData.DataTokens); + } + + [Fact] + public void GetVirtualPath_NoBestEffort_NoMatch() + { + // Arrange + var route1 = CreateRoute(accept: false, match: false, matchValue: "bad"); + var route2 = CreateRoute(accept: false, match: false, matchValue: "bad"); + var route3 = CreateRoute(accept: false, match: false, matchValue: "bad"); + + var routeCollection = new RouteCollection(); + routeCollection.Add(route1.Object); + routeCollection.Add(route2.Object); + routeCollection.Add(route3.Object); + + var virtualPathContext = CreateVirtualPathContext(); + + // Act + var path = routeCollection.GetVirtualPath(virtualPathContext); + + Assert.Null(path); + + // All of these should be called + route1.Verify(r => r.GetVirtualPath(It.IsAny()), Times.Once()); + route2.Verify(r => r.GetVirtualPath(It.IsAny()), Times.Once()); + route3.Verify(r => r.GetVirtualPath(It.IsAny()), Times.Once()); + } + + // DataTokens test data for RouterCollection.GetVirtualPath + public static IEnumerable DataTokensTestData + { + get + { + yield return new object[] { null, null }; + yield return new object[] { new RouteValueDictionary(), null }; + yield return new object[] { new RouteValueDictionary() { { "tokenKey", "tokenValue" } }, null }; + + yield return new object[] { null, "routerA" }; + yield return new object[] { new RouteValueDictionary(), "routerA" }; + yield return new object[] { new RouteValueDictionary() { { "tokenKey", "tokenValue" } }, "routerA" }; + } + } + + private static RouteCollection GetRouteCollectionWithNamedRoutes(IEnumerable routeNames) + { + var routes = new RouteCollection(); + foreach (var routeName in routeNames) + { + var route1 = CreateNamedRoute(routeName, accept: true); + routes.Add(route1); + } + + return routes; + } + + private static RouteCollection GetNestedRouteCollection(string[] routeNames) + { + var random = new Random(); + int index = random.Next(0, routeNames.Length - 1); + var first = routeNames.Take(index).ToArray(); + var second = routeNames.Skip(index).ToArray(); + + var rc1 = GetRouteCollectionWithNamedRoutes(first); + var rc2 = GetRouteCollectionWithNamedRoutes(second); + var rc3 = new RouteCollection(); + var rc4 = new RouteCollection(); + + rc1.Add(rc3); + rc4.Add(rc2); + + // Add a few unnamedRoutes. + rc1.Add(CreateRoute(accept: false).Object); + rc2.Add(CreateRoute(accept: false).Object); + rc3.Add(CreateRoute(accept: false).Object); + rc3.Add(CreateRoute(accept: false).Object); + rc4.Add(CreateRoute(accept: false).Object); + rc4.Add(CreateRoute(accept: false).Object); + + var routeCollection = new RouteCollection(); + routeCollection.Add(rc1); + routeCollection.Add(rc4); + + return routeCollection; + } + + private static INamedRouter CreateNamedRoute(string name, bool accept = false, string matchValue = null) + { + if (matchValue == null) + { + matchValue = name; + } + + var target = new Mock(MockBehavior.Strict); + target + .Setup(e => e.GetVirtualPath(It.IsAny())) + .Returns(c => + c.RouteName == name ? new VirtualPathData(target.Object, matchValue) : null) + .Verifiable(); + + target + .SetupGet(e => e.Name) + .Returns(name); + + target + .Setup(e => e.RouteAsync(It.IsAny())) + .Callback((c) => c.Handler = accept ? NullHandler : null) + .Returns(Task.FromResult(null)) + .Verifiable(); + + return target.Object; + } + + private static Route CreateTemplateRoute( + string template, + string routerName = null, + RouteValueDictionary dataTokens = null, + IInlineConstraintResolver constraintResolver = null) + { + var target = new Mock(MockBehavior.Strict); + target + .Setup(e => e.GetVirtualPath(It.IsAny())) + .Returns(rc => null); + + if (constraintResolver == null) + { + constraintResolver = new Mock().Object; + } + + return new Route( + target.Object, + routerName, + template, + defaults: null, + constraints: null, + dataTokens: dataTokens, + inlineConstraintResolver: constraintResolver); + } + + private static VirtualPathContext CreateVirtualPathContext( + string routeName = null, + ILoggerFactory loggerFactory = null, + Action options = null) + { + if (loggerFactory == null) + { + loggerFactory = NullLoggerFactory.Instance; + } + + var request = new Mock(MockBehavior.Strict); + + var services = new ServiceCollection(); + services.AddSingleton(); + services.AddOptions(); + services.AddRouting(); + if (options != null) + { + services.Configure(options); + } + + var context = new Mock(MockBehavior.Strict); + context.SetupGet(m => m.RequestServices).Returns(services.BuildServiceProvider()); + context.SetupGet(c => c.Request).Returns(request.Object); + + return new VirtualPathContext(context.Object, null, null, routeName); + } + + private static VirtualPathContext CreateVirtualPathContext( + RouteValueDictionary values, + Action options = null, + string routeName = null) + { + var services = new ServiceCollection(); + services.AddSingleton(NullLoggerFactory.Instance); + services.AddSingleton(); + services.AddOptions(); + services.AddRouting(); + if (options != null) + { + services.Configure(options); + } + + var context = new DefaultHttpContext + { + RequestServices = services.BuildServiceProvider(), + }; + + return new VirtualPathContext( + context, + ambientValues: null, + values: values, + routeName: routeName); + } + + private static RouteContext CreateRouteContext( + string requestPath, + ILoggerFactory loggerFactory = null, + RouteOptions options = null) + { + if (loggerFactory == null) + { + loggerFactory = NullLoggerFactory.Instance; + } + + if (options == null) + { + options = new RouteOptions(); + } + + var request = new Mock(MockBehavior.Strict); + request.SetupGet(r => r.Path).Returns(requestPath); + + var optionsAccessor = new Mock>(MockBehavior.Strict); + optionsAccessor.SetupGet(o => o.Value).Returns(options); + + var context = new Mock(MockBehavior.Strict); + context.Setup(m => m.RequestServices.GetService(typeof(ILoggerFactory))) + .Returns(loggerFactory); + context.Setup(m => m.RequestServices.GetService(typeof(IOptions))) + .Returns(optionsAccessor.Object); + context.SetupGet(c => c.Request).Returns(request.Object); + + return new RouteContext(context.Object); + } + + private static Mock CreateRoute( + bool accept = true, + bool match = false, + string matchValue = "value") + { + var target = new Mock(MockBehavior.Strict); + target + .Setup(e => e.GetVirtualPath(It.IsAny())) + .Returns(accept || match ? new VirtualPathData(target.Object, matchValue) : null) + .Verifiable(); + + target + .Setup(e => e.RouteAsync(It.IsAny())) + .Callback((c) => c.Handler = accept ? NullHandler : null) + .Returns(Task.FromResult(null)) + .Verifiable(); + + return target; + } + + private static Action GetRouteOptions( + bool lowerCaseUrls = false, + bool appendTrailingSlash = false) + { + return (options) => + { + options.LowercaseUrls = lowerCaseUrls; + options.AppendTrailingSlash = appendTrailingSlash; + }; + } + } +} diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RouteConstraintBuilderTest.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RouteConstraintBuilderTest.cs new file mode 100644 index 0000000000..9eca4d7759 --- /dev/null +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RouteConstraintBuilderTest.cs @@ -0,0 +1,190 @@ +// 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.Http; +using Microsoft.AspNetCore.Routing.Constraints; +using Microsoft.AspNetCore.Testing; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Routing +{ + public class RouteConstraintBuilderTest + { + [Fact] + public void AddConstraint_String_CreatesARegex() + { + // Arrange + var builder = CreateBuilder("{controller}/{action}"); + builder.AddConstraint("controller", "abc"); + + // Act + var result = builder.Build(); + + // Assert + Assert.Equal(1, result.Count); + Assert.Equal("controller", result.First().Key); + + Assert.IsType(Assert.Single(result).Value); + } + + [Fact] + public void AddConstraint_IRouteConstraint() + { + // Arrange + var originalConstraint = Mock.Of(); + + var builder = CreateBuilder("{controller}/{action}"); + builder.AddConstraint("controller", originalConstraint); + + // Act + var result = builder.Build(); + + // Assert + Assert.Equal(1, result.Count); + + var kvp = Assert.Single(result); + Assert.Equal("controller", kvp.Key); + + Assert.Same(originalConstraint, kvp.Value); + } + + [Fact] + public void AddResolvedConstraint_IRouteConstraint() + { + // Arrange + var builder = CreateBuilder("{controller}/{action}"); + builder.AddResolvedConstraint("controller", "int"); + + // Act + var result = builder.Build(); + + // Assert + Assert.Equal(1, result.Count); + + var kvp = Assert.Single(result); + Assert.Equal("controller", kvp.Key); + + Assert.IsType(kvp.Value); + } + + [Fact] + public void AddConstraint_InvalidType_Throws() + { + // Arrange + var builder = CreateBuilder("{controller}/{action}"); + + // Act & Assert + ExceptionAssert.Throws( + () => builder.AddConstraint("controller", 5), + "The constraint entry 'controller' - '5' on the route " + + "'{controller}/{action}' must have a string value or be of a type which implements '" + + typeof(IRouteConstraint) + "'."); + } + + [Fact] + public void AddResolvedConstraint_NotFound_Throws() + { + // Arrange + var unresolvedConstraint = @"test"; + + var builder = CreateBuilder("{controller}/{action}"); + + // Act & Assert + ExceptionAssert.Throws( + () => builder.AddResolvedConstraint("controller", unresolvedConstraint), + @"The constraint entry 'controller' - '" + unresolvedConstraint + "' on the route " + + "'{controller}/{action}' could not be resolved by the constraint resolver " + + "of type 'DefaultInlineConstraintResolver'."); + } + + [Fact] + public void AddResolvedConstraint_ForOptionalParameter() + { + var builder = CreateBuilder("{controller}/{action}/{id}"); + builder.SetOptional("id"); + builder.AddResolvedConstraint("id", "int"); + + var result = builder.Build(); + Assert.Equal(1, result.Count); + Assert.Equal("id", result.First().Key); + Assert.IsType(Assert.Single(result).Value); + } + + [Fact] + public void AddResolvedConstraint_SetOptionalParameter_AfterAddingTheParameter() + { + var builder = CreateBuilder("{controller}/{action}/{id}"); + builder.AddResolvedConstraint("id", "int"); + builder.SetOptional("id"); + + var result = builder.Build(); + Assert.Equal(1, result.Count); + Assert.Equal("id", result.First().Key); + Assert.IsType(Assert.Single(result).Value); + } + + [Fact] + public void AddResolvedConstraint_And_AddConstraint_ForOptionalParameter() + { + var builder = CreateBuilder("{controller}/{action}/{name}"); + builder.SetOptional("name"); + builder.AddResolvedConstraint("name", "alpha"); + var minLenConstraint = new MinLengthRouteConstraint(10); + builder.AddConstraint("name", minLenConstraint); + + var result = builder.Build(); + Assert.Equal(1, result.Count); + Assert.Equal("name", result.First().Key); + Assert.IsType(Assert.Single(result).Value); + var optionalConstraint = (OptionalRouteConstraint)result.First().Value; + var compositeConstraint = Assert.IsType(optionalConstraint.InnerConstraint); ; + Assert.Equal(2, compositeConstraint.Constraints.Count()); + + Assert.Single(compositeConstraint.Constraints, c => c is MinLengthRouteConstraint); + Assert.Single(compositeConstraint.Constraints, c => c is AlphaRouteConstraint); + } + + [Theory] + [InlineData("abc", "abc", true)] // simple case + [InlineData("abc", "bbb|abc", true)] // Regex or + [InlineData("Abc", "abc", true)] // Case insensitive + [InlineData("Abc ", "abc", false)] // Matches whole (but no trimming) + [InlineData("Abcd", "abc", false)] // Matches whole (additional non whitespace char) + [InlineData("Abc", " abc", false)] // Matches whole (less one char) + public void StringConstraintsMatchingScenarios(string routeValue, + string constraintValue, + bool shouldMatch) + { + // Arrange + var routeValues = new RouteValueDictionary(new { controller = routeValue }); + + var builder = CreateBuilder("{controller}/{action}"); + builder.AddConstraint("controller", constraintValue); + + var constraint = Assert.Single(builder.Build()).Value; + + Assert.Equal(shouldMatch, + constraint.Match( + httpContext: new Mock().Object, + route: new Mock().Object, + routeKey: "controller", + values: routeValues, + routeDirection: RouteDirection.IncomingRequest)); + } + + private static RouteConstraintBuilder CreateBuilder(string template) + { + var options = new Mock>(MockBehavior.Strict); + options + .SetupGet(o => o.Value) + .Returns(new RouteOptions()); + + var inlineConstraintResolver = new DefaultInlineConstraintResolver(options.Object); + return new RouteConstraintBuilder(inlineConstraintResolver, template); + } + } +} diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RouteOptionsTests.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RouteOptionsTests.cs new file mode 100644 index 0000000000..6dc6ceb2ad --- /dev/null +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RouteOptionsTests.cs @@ -0,0 +1,49 @@ +// 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.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.AspNetCore.Routing.Tests +{ + public class RouteOptionsTests + { + [Fact] + public void ConfigureRouting_ConfiguresOptionsProperly() + { + // Arrange + var services = new ServiceCollection(); + services.AddOptions(); + + // Act + services.AddRouting(options => options.ConstraintMap.Add("foo", typeof(TestRouteConstraint))); + var serviceProvider = services.BuildServiceProvider(); + + // Assert + var accessor = serviceProvider.GetRequiredService>(); + Assert.Equal("TestRouteConstraint", accessor.Value.ConstraintMap["foo"].Name); + } + + private class TestRouteConstraint : IRouteConstraint + { + public TestRouteConstraint(string pattern) + { + Pattern = pattern; + } + + public string Pattern { get; private set; } + public bool Match( + HttpContext httpContext, + IRouter route, + string routeKey, + RouteValueDictionary values, + RouteDirection routeDirection) + { + throw new NotImplementedException(); + } + } + } +} diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RouteTest.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RouteTest.cs new file mode 100644 index 0000000000..a09935fcd5 --- /dev/null +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RouteTest.cs @@ -0,0 +1,1863 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Linq; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing.Constraints; +using Microsoft.AspNetCore.Routing.Internal; +using Microsoft.AspNetCore.Testing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.ObjectPool; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.WebEncoders.Testing; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Routing +{ + public class RouteTest + { + private static readonly RequestDelegate NullHandler = (c) => Task.FromResult(0); + private static IInlineConstraintResolver _inlineConstraintResolver = GetInlineConstraintResolver(); + + [Fact] + public void CreateTemplate_InlineConstraint_Regex_Malformed() + { + // Arrange + var template = @"{controller}/{action}/ {p1:regex(abc} "; + var mockTarget = new Mock(MockBehavior.Strict); + + var exception = Assert.Throws( + () => new Route( + mockTarget.Object, + template, + defaults: null, + constraints: null, + dataTokens: null, + inlineConstraintResolver: _inlineConstraintResolver)); + + var expected = "An error occurred while creating the route with name '' and template" + + $" '{template}'."; + Assert.Equal(expected, exception.Message); + + Assert.NotNull(exception.InnerException); + expected = "The constraint entry 'p1' - 'regex(abc' on the route " + + "'{controller}/{action}/ {p1:regex(abc} ' could not be resolved by the constraint resolver of type " + + $"'{nameof(DefaultInlineConstraintResolver)}'."; + Assert.Equal(expected, exception.InnerException.Message); + } + + [Fact] + public async Task RouteAsync_MergesExistingRouteData_IfRouteMatches() + { + // Arrange + var template = "{controller}/{action}/{id:int}"; + + var context = CreateRouteContext("/Home/Index/5"); + + var originalRouteDataValues = context.RouteData.Values; + originalRouteDataValues.Add("country", "USA"); + + var originalDataTokens = context.RouteData.DataTokens; + originalDataTokens.Add("company", "Contoso"); + + IDictionary routeValues = null; + var mockTarget = new Mock(MockBehavior.Strict); + mockTarget + .Setup(s => s.RouteAsync(It.IsAny())) + .Callback(ctx => + { + routeValues = ctx.RouteData.Values; + ctx.Handler = NullHandler; + }) + .Returns(Task.FromResult(true)); + + var route = new Route( + mockTarget.Object, + template, + defaults: null, + constraints: null, + dataTokens: new RouteValueDictionary(new { today = "Friday" }), + inlineConstraintResolver: _inlineConstraintResolver); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.NotNull(routeValues); + + Assert.True(routeValues.ContainsKey("country")); + Assert.Equal("USA", routeValues["country"]); + Assert.True(routeValues.ContainsKey("id")); + Assert.Equal("5", routeValues["id"]); + + Assert.True(context.RouteData.Values.ContainsKey("country")); + Assert.Equal("USA", context.RouteData.Values["country"]); + Assert.True(context.RouteData.Values.ContainsKey("id")); + Assert.Equal("5", context.RouteData.Values["id"]); + Assert.Same(originalRouteDataValues, context.RouteData.Values); + + Assert.Equal("Contoso", context.RouteData.DataTokens["company"]); + Assert.Equal("Friday", context.RouteData.DataTokens["today"]); + Assert.Same(originalDataTokens, context.RouteData.DataTokens); + } + + [Fact] + public async Task RouteAsync_MergesExistingRouteData_PassedToConstraint() + { + // Arrange + var template = "{controller}/{action}/{id:int}"; + + var context = CreateRouteContext("/Home/Index/5"); + var originalRouteDataValues = context.RouteData.Values; + originalRouteDataValues.Add("country", "USA"); + + var originalDataTokens = context.RouteData.DataTokens; + originalDataTokens.Add("company", "Contoso"); + + IDictionary routeValues = null; + var mockTarget = new Mock(MockBehavior.Strict); + mockTarget + .Setup(s => s.RouteAsync(It.IsAny())) + .Callback(ctx => + { + routeValues = ctx.RouteData.Values; + ctx.Handler = NullHandler; + }) + .Returns(Task.FromResult(true)); + + var constraint = new CapturingConstraint(); + + var route = new Route( + mockTarget.Object, + template, + defaults: null, + constraints: new RouteValueDictionary(new { action = constraint }), + dataTokens: new RouteValueDictionary(new { today = "Friday" }), + inlineConstraintResolver: _inlineConstraintResolver); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.NotNull(routeValues); + + Assert.True(routeValues.ContainsKey("country")); + Assert.Equal("USA", routeValues["country"]); + Assert.True(routeValues.ContainsKey("id")); + Assert.Equal("5", routeValues["id"]); + + Assert.True(constraint.Values.ContainsKey("country")); + Assert.Equal("USA", constraint.Values["country"]); + Assert.True(constraint.Values.ContainsKey("id")); + Assert.Equal("5", constraint.Values["id"]); + + Assert.True(context.RouteData.Values.ContainsKey("country")); + Assert.Equal("USA", context.RouteData.Values["country"]); + Assert.True(context.RouteData.Values.ContainsKey("id")); + Assert.Equal("5", context.RouteData.Values["id"]); + + Assert.Equal("Contoso", context.RouteData.DataTokens["company"]); + Assert.Equal("Friday", context.RouteData.DataTokens["today"]); + } + + [Fact] + public async Task RouteAsync_InlineConstraint_OptionalParameter() + { + // Arrange + var template = "{controller}/{action}/{id:int?}"; + + var context = CreateRouteContext("/Home/Index/5"); + + IDictionary routeValues = null; + var mockTarget = new Mock(MockBehavior.Strict); + mockTarget + .Setup(s => s.RouteAsync(It.IsAny())) + .Callback(ctx => + { + routeValues = ctx.RouteData.Values; + ctx.Handler = NullHandler; + }) + .Returns(Task.FromResult(true)); + + var route = new Route( + mockTarget.Object, + template, + defaults: null, + constraints: null, + dataTokens: null, + inlineConstraintResolver: _inlineConstraintResolver); + + Assert.NotEmpty(route.Constraints); + Assert.IsType(route.Constraints["id"]); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.NotNull(context.Handler); + Assert.True(routeValues.ContainsKey("id")); + Assert.Equal("5", routeValues["id"]); + + Assert.True(context.RouteData.Values.ContainsKey("id")); + Assert.Equal("5", context.RouteData.Values["id"]); + } + + [Fact] + public async Task RouteAsync_InlineConstraint_Regex() + { + // Arrange + var template = @"{controller}/{action}/{ssn:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)}"; + + var context = CreateRouteContext("/Home/Index/123-456-7890"); + + IDictionary routeValues = null; + var mockTarget = new Mock(MockBehavior.Strict); + mockTarget + .Setup(s => s.RouteAsync(It.IsAny())) + .Callback(ctx => + { + routeValues = ctx.RouteData.Values; + ctx.Handler = NullHandler; + }) + .Returns(Task.FromResult(true)); + + var route = new Route( + mockTarget.Object, + template, + defaults: null, + constraints: null, + dataTokens: null, + inlineConstraintResolver: _inlineConstraintResolver); + + Assert.NotEmpty(route.Constraints); + Assert.IsType(route.Constraints["ssn"]); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.NotNull(context.Handler); + Assert.True(routeValues.ContainsKey("ssn")); + Assert.Equal("123-456-7890", routeValues["ssn"]); + + Assert.True(context.RouteData.Values.ContainsKey("ssn")); + Assert.Equal("123-456-7890", context.RouteData.Values["ssn"]); + } + + [Fact] + public async Task RouteAsync_InlineConstraint_OptionalParameter_NotPresent() + { + // Arrange + var template = "{controller}/{action}/{id:int?}"; + + var context = CreateRouteContext("/Home/Index"); + + IDictionary routeValues = null; + var mockTarget = new Mock(MockBehavior.Strict); + mockTarget + .Setup(s => s.RouteAsync(It.IsAny())) + .Callback(ctx => + { + routeValues = ctx.RouteData.Values; + ctx.Handler = NullHandler; + }) + .Returns(Task.FromResult(true)); + + var route = new Route( + mockTarget.Object, + template, + defaults: null, + constraints: null, + dataTokens: null, + inlineConstraintResolver: _inlineConstraintResolver); + + Assert.NotEmpty(route.Constraints); + Assert.IsType(route.Constraints["id"]); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.NotNull(context.Handler); + Assert.NotNull(routeValues); + Assert.False(routeValues.ContainsKey("id")); + Assert.False(context.RouteData.Values.ContainsKey("id")); + } + + [Fact] + public async Task RouteAsync_InlineConstraint_OptionalParameter_WithInConstructorConstraint() + { + // Arrange + var template = "{controller}/{action}/{id:int?}"; + + var context = CreateRouteContext("/Home/Index/5"); + + IDictionary routeValues = null; + var mockTarget = new Mock(MockBehavior.Strict); + mockTarget + .Setup(s => s.RouteAsync(It.IsAny())) + .Callback(ctx => + { + routeValues = ctx.RouteData.Values; + ctx.Handler = NullHandler; + }) + .Returns(Task.FromResult(true)); + + var constraints = new Dictionary(); + constraints.Add("id", new RangeRouteConstraint(1, 20)); + + var route = new Route( + mockTarget.Object, + template, + defaults: null, + constraints: constraints, + dataTokens: null, + inlineConstraintResolver: _inlineConstraintResolver); + + Assert.NotEmpty(route.Constraints); + Assert.IsType(route.Constraints["id"]); + var innerConstraint = ((OptionalRouteConstraint)route.Constraints["id"]).InnerConstraint; + Assert.IsType(innerConstraint); + var compositeConstraint = (CompositeRouteConstraint)innerConstraint; + Assert.Equal(2, compositeConstraint.Constraints.Count()); + + Assert.Single(compositeConstraint.Constraints, c => c is IntRouteConstraint); + Assert.Single(compositeConstraint.Constraints, c => c is RangeRouteConstraint); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.NotNull(context.Handler); + Assert.True(routeValues.ContainsKey("id")); + Assert.Equal("5", routeValues["id"]); + + Assert.True(context.RouteData.Values.ContainsKey("id")); + Assert.Equal("5", context.RouteData.Values["id"]); + } + + [Fact] + public async Task RouteAsync_InlineConstraint_OptionalParameter_ConstraintFails() + { + // Arrange + var template = "{controller}/{action}/{id:range(1,20)?}"; + + var context = CreateRouteContext("/Home/Index/100"); + + IDictionary routeValues = null; + var mockTarget = new Mock(MockBehavior.Strict); + mockTarget + .Setup(s => s.RouteAsync(It.IsAny())) + .Callback(ctx => + { + routeValues = ctx.RouteData.Values; + ctx.Handler = NullHandler; + }) + .Returns(Task.FromResult(true)); + + var route = new Route( + mockTarget.Object, + template, + defaults: null, + constraints: null, + dataTokens: null, + inlineConstraintResolver: _inlineConstraintResolver); + + Assert.NotEmpty(route.Constraints); + Assert.IsType(route.Constraints["id"]); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.Null(context.Handler); + } + + // PathString in HttpAbstractions guarantees a leading slash - so no value in testing other cases. + [Fact] + public async Task Match_Success_LeadingSlash() + { + // Arrange + var route = CreateRoute("{controller}/{action}"); + var context = CreateRouteContext("/Home/Index"); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.NotNull(context.Handler); + Assert.Equal(2, context.RouteData.Values.Count); + Assert.Equal("Home", context.RouteData.Values["controller"]); + Assert.Equal("Index", context.RouteData.Values["action"]); + } + + [Fact] + public async Task Match_Success_RootUrl() + { + // Arrange + var route = CreateRoute(""); + var context = CreateRouteContext("/"); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.NotNull(context.Handler); + Assert.Empty(context.RouteData.Values); + } + + [Fact] + public async Task Match_Success_Defaults() + { + // Arrange + var route = CreateRoute("{controller}/{action}", new { action = "Index" }); + var context = CreateRouteContext("/Home"); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.NotNull(context.Handler); + Assert.Equal(2, context.RouteData.Values.Count); + Assert.Equal("Home", context.RouteData.Values["controller"]); + Assert.Equal("Index", context.RouteData.Values["action"]); + } + + [Fact] + public async Task Match_Success_CopiesDataTokens() + { + // Arrange + var route = CreateRoute( + "{controller}/{action}", + defaults: new { action = "Index" }, + dataTokens: new { culture = "en-CA" }); + + var context = CreateRouteContext("/Home"); + + // Act + await route.RouteAsync(context); + Assert.NotNull(context.Handler); + + // This should not affect the route - RouteData.DataTokens is a copy + context.RouteData.DataTokens.Add("company", "contoso"); + + // Assert + Assert.Single(route.DataTokens); + Assert.Single(route.DataTokens, kvp => kvp.Key == "culture" && ((string)kvp.Value) == "en-CA"); + } + + [Fact] + public async Task Match_Fails() + { + // Arrange + var route = CreateRoute("{controller}/{action}"); + var context = CreateRouteContext("/Home"); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.Null(context.Handler); + } + + [Fact] + public async Task Match_RejectedByHandler() + { + // Arrange + var route = CreateRoute("{controller}", handleRequest: false); + var context = CreateRouteContext("/Home"); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.Null(context.Handler); + + var value = Assert.Single(context.RouteData.Values); + Assert.Equal("controller", value.Key); + Assert.Equal("Home", Assert.IsType(value.Value)); + } + + [Fact] + public async Task Match_SetsRouters() + { + // Arrange + var target = CreateTarget(handleRequest: true); + var route = CreateRoute(target, "{controller}"); + var context = CreateRouteContext("/Home"); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.NotNull(context.Handler); + Assert.Equal(1, context.RouteData.Routers.Count); + Assert.Same(target, context.RouteData.Routers[0]); + } + + [Fact] + public async Task Match_RouteValuesDoesntThrowOnKeyNotFound() + { + // Arrange + var route = CreateRoute("{controller}/{action}"); + var context = CreateRouteContext("/Home/Index"); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.Null(context.RouteData.Values["1controller"]); + } + + [Fact] + public async Task Match_Success_OptionalParameter_ValueProvided() + { + // Arrange + var route = CreateRoute("{controller}/{action}.{format?}", new { action = "Index" }); + var context = CreateRouteContext("/Home/Create.xml"); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.NotNull(context.Handler); + Assert.Equal(3, context.RouteData.Values.Count); + Assert.Equal("Home", context.RouteData.Values["controller"]); + Assert.Equal("Create", context.RouteData.Values["action"]); + Assert.Equal("xml", context.RouteData.Values["format"]); + } + + [Fact] + public async Task Match_Success_OptionalParameter_ValueNotProvided() + { + // Arrange + var route = CreateRoute("{controller}/{action}.{format?}", new { action = "Index" }); + var context = CreateRouteContext("/Home/Create"); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.NotNull(context.Handler); + Assert.Equal(2, context.RouteData.Values.Count); + Assert.Equal("Home", context.RouteData.Values["controller"]); + Assert.Equal("Create", context.RouteData.Values["action"]); + } + + [Fact] + public async Task Match_Success_OptionalParameter_DefaultValue() + { + // Arrange + var route = CreateRoute("{controller}/{action}.{format?}", new { action = "Index", format = "xml" }); + var context = CreateRouteContext("/Home/Create"); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.NotNull(context.Handler); + Assert.Equal(3, context.RouteData.Values.Count); + Assert.Equal("Home", context.RouteData.Values["controller"]); + Assert.Equal("Create", context.RouteData.Values["action"]); + Assert.Equal("xml", context.RouteData.Values["format"]); + } + + [Fact] + public async Task Match_Success_OptionalParameter_EndsWithDot() + { + // Arrange + var route = CreateRoute("{controller}/{action}.{format?}", new { action = "Index" }); + var context = CreateRouteContext("/Home/Create."); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.Null(context.Handler); + } + + private static RouteContext CreateRouteContext(string requestPath, ILoggerFactory factory = null) + { + if (factory == null) + { + factory = NullLoggerFactory.Instance; + } + + var request = new Mock(MockBehavior.Strict); + request.SetupGet(r => r.Path).Returns(requestPath); + + var context = new Mock(MockBehavior.Strict); + context.Setup(m => m.RequestServices.GetService(typeof(ILoggerFactory))) + .Returns(factory); + context.SetupGet(c => c.Request).Returns(request.Object); + + return new RouteContext(context.Object); + } + + [Fact] + public void GetVirtualPath_Success() + { + // Arrange + var route = CreateRoute("{controller}"); + var context = CreateVirtualPathContext(new { controller = "Home" }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.Equal("/Home", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } + + [Fact] + public void GetVirtualPath_Fail() + { + // Arrange + var route = CreateRoute("{controller}/{action}"); + var context = CreateVirtualPathContext(new { controller = "Home" }); + + // Act + var path = route.GetVirtualPath(context); + + // Assert + Assert.Null(path); + } + + [Fact] + public void GetVirtualPath_EncodesValues() + { + // Arrange + var route = CreateRoute("{controller}/{action}"); + var context = CreateVirtualPathContext( + new { name = "name with %special #characters" }, + new { controller = "Home", action = "Index" }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.Equal("/Home/Index?name=name%20with%20%25special%20%23characters", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } + + [Fact] + public void GetVirtualPath_AlwaysUsesDefaultUrlEncoder() + { + // Arrange + var nameRouteValue = "name with %special #characters Jörn"; + var expected = "/Home/Index?name=" + UrlEncoder.Default.Encode(nameRouteValue); + var services = new ServiceCollection(); + services.AddSingleton(NullLoggerFactory.Instance); + services.AddSingleton(); + services.AddRouting(); + // This test encoder should not be used by Routing and should always use the default one. + services.AddSingleton(new UrlTestEncoder()); + var httpContext = new DefaultHttpContext + { + RequestServices = services.BuildServiceProvider(), + }; + + var context = new VirtualPathContext( + httpContext, + values: new RouteValueDictionary(new { name = nameRouteValue }), + ambientValues: new RouteValueDictionary(new { controller = "Home", action = "Index" })); + + var route = CreateRoute("{controller}/{action}"); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.Equal(expected, pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } + + [Fact] + public void GetVirtualPath_ForListOfStrings() + { + // Arrange + var route = CreateRoute("{controller}/{action}"); + var context = CreateVirtualPathContext( + new { color = new List { "red", "green", "blue" } }, + new { controller = "Home", action = "Index" }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.Equal("/Home/Index?color=red&color=green&color=blue", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } + + [Fact] + public void GetVirtualPath_ForListOfInts() + { + // Arrange + var route = CreateRoute("{controller}/{action}"); + var context = CreateVirtualPathContext( + new { items = new List { 10, 20, 30 } }, + new { controller = "Home", action = "Index" }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.Equal("/Home/Index?items=10&items=20&items=30", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } + + [Fact] + public void GetVirtualPath_ForList_Empty() + { + // Arrange + var route = CreateRoute("{controller}/{action}"); + var context = CreateVirtualPathContext( + new { color = new List { } }, + new { controller = "Home", action = "Index" }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.Equal("/Home/Index", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } + + [Fact] + public void GetVirtualPath_ForList_StringWorkaround() + { + // Arrange + var route = CreateRoute("{controller}/{action}"); + var context = CreateVirtualPathContext( + new { page = 1, color = new List { "red", "green", "blue" }, message = "textfortest" }, + new { controller = "Home", action = "Index" }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.Equal("/Home/Index?page=1&color=red&color=green&color=blue&message=textfortest", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } + + [Theory] + [MemberData(nameof(DataTokensTestData))] + public void GetVirtualPath_ReturnsDataTokens_WhenTargetReturnsVirtualPathData( + RouteValueDictionary dataTokens) + { + // Arrange + var path = "/TestPath"; + + var target = new Mock(MockBehavior.Strict); + target + .Setup(r => r.GetVirtualPath(It.IsAny())) + .Returns(() => new VirtualPathData(target.Object, path, dataTokens)); + + var routeDataTokens = + new RouteValueDictionary() { { "ThisShouldBeIgnored", "" } }; + + var route = CreateRoute( + target.Object, + "{controller}", + defaults: null, + dataTokens: routeDataTokens); + var context = CreateVirtualPathContext(new { controller = path }); + + var expectedDataTokens = dataTokens ?? new RouteValueDictionary(); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.NotNull(pathData); + Assert.Same(target.Object, pathData.Router); + Assert.Equal(path, pathData.VirtualPath); + Assert.NotNull(pathData.DataTokens); + + Assert.DoesNotContain(routeDataTokens.First().Key, pathData.DataTokens.Keys); + + Assert.Equal(expectedDataTokens.Count, pathData.DataTokens.Count); + foreach (var dataToken in expectedDataTokens) + { + Assert.True(pathData.DataTokens.ContainsKey(dataToken.Key)); + Assert.Equal(dataToken.Value, pathData.DataTokens[dataToken.Key]); + } + } + + [Theory] + [MemberData(nameof(DataTokensTestData))] + public void GetVirtualPath_ReturnsDataTokens_WhenTargetReturnsNullVirtualPathData( + RouteValueDictionary dataTokens) + { + // Arrange + var path = "/TestPath"; + + var target = new Mock(MockBehavior.Strict); + target + .Setup(r => r.GetVirtualPath(It.IsAny())) + .Returns(() => null); + + var route = CreateRoute( + target.Object, + "{controller}", + defaults: null, + dataTokens: dataTokens); + var context = CreateVirtualPathContext(new { controller = path }); + + var expectedDataTokens = dataTokens ?? new RouteValueDictionary(); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.NotNull(pathData); + Assert.Same(route, pathData.Router); + Assert.Equal(path, pathData.VirtualPath); + Assert.NotNull(pathData.DataTokens); + + Assert.Equal(expectedDataTokens.Count, pathData.DataTokens.Count); + foreach (var dataToken in expectedDataTokens) + { + Assert.True(pathData.DataTokens.ContainsKey(dataToken.Key)); + Assert.Equal(dataToken.Value, pathData.DataTokens[dataToken.Key]); + } + } + + [Fact] + public void GetVirtualPath_ValuesRejectedByHandler_StillGeneratesPath() + { + // Arrange + var route = CreateRoute("{controller}", handleRequest: false); + var context = CreateVirtualPathContext(new { controller = "Home" }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.Equal("/Home", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } + + [Fact] + public void GetVirtualPath_Success_AmbientValues() + { + // Arrange + var route = CreateRoute("{controller}/{action}"); + var context = CreateVirtualPathContext(new { action = "Index" }, new { controller = "Home" }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.Equal("/Home/Index", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } + + [Fact] + public void RouteGenerationRejectsConstraints() + { + // Arrange + var context = CreateVirtualPathContext(new { p1 = "abcd" }); + + var route = CreateRoute( + "{p1}/{p2}", + new { p2 = "catchall" }, + true, + new RouteValueDictionary(new { p2 = "\\d{4}" })); + + // Act + var virtualPath = route.GetVirtualPath(context); + + // Assert + Assert.Null(virtualPath); + } + + [Fact] + public void RouteGenerationAcceptsConstraints() + { + // Arrange + var context = CreateVirtualPathContext(new { p1 = "hello", p2 = "1234" }); + + var route = CreateRoute( + "{p1}/{p2}", + new { p2 = "catchall" }, + true, + new RouteValueDictionary(new { p2 = "\\d{4}" })); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.NotNull(pathData); + Assert.Equal("/hello/1234", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } + + [Fact] + public void RouteWithCatchAllRejectsConstraints() + { + // Arrange + var context = CreateVirtualPathContext(new { p1 = "abcd" }); + + var route = CreateRoute( + "{p1}/{*p2}", + new { p2 = "catchall" }, + true, + new RouteValueDictionary(new { p2 = "\\d{4}" })); + + // Act + var virtualPath = route.GetVirtualPath(context); + + // Assert + Assert.Null(virtualPath); + } + + [Fact] + public void RouteWithCatchAllAcceptsConstraints() + { + // Arrange + var context = CreateVirtualPathContext(new { p1 = "hello", p2 = "1234" }); + + var route = CreateRoute( + "{p1}/{*p2}", + new { p2 = "catchall" }, + true, + new RouteValueDictionary(new { p2 = "\\d{4}" })); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.NotNull(pathData); + Assert.Equal("/hello/1234", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } + + [Fact] + public void GetVirtualPathWithNonParameterConstraintReturnsUrlWithoutQueryString() + { + // Arrange + var context = CreateVirtualPathContext(new { p1 = "hello", p2 = "1234" }); + + var target = new Mock(); + target + .Setup( + e => e.Match( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns(true) + .Verifiable(); + + var route = CreateRoute( + "{p1}/{p2}", + new { p2 = "catchall" }, + true, + new RouteValueDictionary(new { p2 = target.Object })); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.NotNull(pathData); + Assert.Equal("/hello/1234", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + + target.VerifyAll(); + } + + // Any ambient values from the current request should be visible to constraint, even + // if they have nothing to do with the route generating a link + [Fact] + public void GetVirtualPath_ConstraintsSeeAmbientValues() + { + // Arrange + var constraint = new CapturingConstraint(); + var route = CreateRoute( + template: "slug/{controller}/{action}", + defaults: null, + handleRequest: true, + constraints: new { c = constraint }); + + var context = CreateVirtualPathContext( + values: new { action = "Store" }, + ambientValues: new { Controller = "Home", action = "Blog", extra = "42" }); + + var expectedValues = new RouteValueDictionary( + new { controller = "Home", action = "Store", extra = "42" }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.Equal("/slug/Home/Store", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + + Assert.Equal(expectedValues, constraint.Values); + } + + // Non-parameter default values from the routing generating a link are not in the 'values' + // collection when constraints are processed. + [Fact] + public void GetVirtualPath_ConstraintsDontSeeDefaults_WhenTheyArentParameters() + { + // Arrange + var constraint = new CapturingConstraint(); + var route = CreateRoute( + template: "slug/{controller}/{action}", + defaults: new { otherthing = "17" }, + handleRequest: true, + constraints: new { c = constraint }); + + var context = CreateVirtualPathContext( + values: new { action = "Store" }, + ambientValues: new { Controller = "Home", action = "Blog" }); + + var expectedValues = new RouteValueDictionary( + new { controller = "Home", action = "Store" }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.Equal("/slug/Home/Store", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + + Assert.Equal(expectedValues, constraint.Values); + } + + // Default values are visible to the constraint when they are used to fill a parameter. + [Fact] + public void GetVirtualPath_ConstraintsSeesDefault_WhenThereItsAParamter() + { + // Arrange + var constraint = new CapturingConstraint(); + var route = CreateRoute( + template: "slug/{controller}/{action}", + defaults: new { action = "Index" }, + handleRequest: true, + constraints: new { c = constraint }); + + var context = CreateVirtualPathContext( + values: new { controller = "Shopping" }, + ambientValues: new { Controller = "Home", action = "Blog" }); + + var expectedValues = new RouteValueDictionary( + new { controller = "Shopping", action = "Index" }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.Equal("/slug/Shopping", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + + Assert.Equal(expectedValues, constraint.Values); + } + + // Default values from the routing generating a link are in the 'values' collection when + // constraints are processed - IFF they are specified as values or ambient values. + [Fact] + public void GetVirtualPath_ConstraintsSeeDefaults_IfTheyAreSpecifiedOrAmbient() + { + // Arrange + var constraint = new CapturingConstraint(); + var route = CreateRoute( + template: "slug/{controller}/{action}", + defaults: new { otherthing = "17", thirdthing = "13" }, + handleRequest: true, + constraints: new { c = constraint }); + + var context = CreateVirtualPathContext( + values: new { action = "Store", thirdthing = "13" }, + ambientValues: new { Controller = "Home", action = "Blog", otherthing = "17" }); + + var expectedValues = new RouteValueDictionary( + new { controller = "Home", action = "Store", otherthing = "17", thirdthing = "13" }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.Equal("/slug/Home/Store", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + + Assert.Equal(expectedValues.OrderBy(kvp => kvp.Key), constraint.Values.OrderBy(kvp => kvp.Key)); + } + + [Fact] + public void GetVirtualPath_InlineConstraints_Success() + { + // Arrange + var route = CreateRoute("{controller}/{action}/{id:int}"); + var context = CreateVirtualPathContext( + values: new { action = "Index", controller = "Home", id = 4 }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.Equal("/Home/Index/4", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } + + [Fact] + public void GetVirtualPath_InlineConstraints_NonMatchingvalue() + { + // Arrange + var route = CreateRoute("{controller}/{action}/{id:int}"); + var context = CreateVirtualPathContext( + values: new { action = "Index", controller = "Home", id = "asf" }); + + // Act + var path = route.GetVirtualPath(context); + + // Assert + Assert.Null(path); + } + + [Fact] + public void GetVirtualPath_InlineConstraints_OptionalParameter_ValuePresent() + { + // Arrange + var route = CreateRoute("{controller}/{action}/{id:int?}"); + var context = CreateVirtualPathContext( + values: new { action = "Index", controller = "Home", id = 98 }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.Equal("/Home/Index/98", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } + + [Fact] + public void GetVirtualPath_InlineConstraints_OptionalParameter_ValueNotPresent() + { + // Arrange + var route = CreateRoute("{controller}/{action}/{id:int?}"); + var context = CreateVirtualPathContext( + values: new { action = "Index", controller = "Home" }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.Equal("/Home/Index", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } + + [Fact] + public void GetVirtualPath_InlineConstraints_OptionalParameter_ValuePresent_ConstraintFails() + { + // Arrange + var route = CreateRoute("{controller}/{action}/{id:int?}"); + var context = CreateVirtualPathContext( + values: new { action = "Index", controller = "Home", id = "sdfd" }); + + // Act + var path = route.GetVirtualPath(context); + + // Assert + Assert.Null(path); + } + + [Fact] + public void GetVirtualPath_InlineConstraints_CompositeInlineConstraint() + { + // Arrange + var route = CreateRoute("{controller}/{action}/{id:int:range(1,20)}"); + var context = CreateVirtualPathContext( + values: new { action = "Index", controller = "Home", id = 14 }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.Equal("/Home/Index/14", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } + + [Fact] + public void GetVirtualPath_InlineConstraints_CompositeConstraint_FromConstructor() + { + // Arrange + var constraint = new MaxLengthRouteConstraint(20); + var route = CreateRoute( + template: "{controller}/{action}/{name:alpha}", + defaults: null, + handleRequest: true, + constraints: new { name = constraint }); + + var context = CreateVirtualPathContext( + values: new { action = "Index", controller = "Home", name = "products" }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.Equal("/Home/Index/products", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } + + [Fact] + public void GetVirtualPath_OptionalParameter_ParameterPresentInValues() + { + // Arrange + var route = CreateRoute( + template: "{controller}/{action}/{name}.{format?}", + defaults: null, + handleRequest: true, + constraints: null); + + var context = CreateVirtualPathContext( + values: new { action = "Index", controller = "Home", name = "products", format = "xml" }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.Equal("/Home/Index/products.xml", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } + + [Fact] + public void GetVirtualPath_OptionalParameter_ParameterNotPresentInValues() + { + // Arrange + var route = CreateRoute( + template: "{controller}/{action}/{name}.{format?}", + defaults: null, + handleRequest: true, + constraints: null); + + var context = CreateVirtualPathContext( + values: new { action = "Index", controller = "Home", name = "products" }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.Equal("/Home/Index/products", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } + + [Fact] + public void GetVirtualPath_OptionalParameter_ParameterPresentInValuesAndDefaults() + { + // Arrange + var route = CreateRoute( + template: "{controller}/{action}/{name}.{format?}", + defaults: new { format = "json" }, + handleRequest: true, + constraints: null); + + var context = CreateVirtualPathContext( + values: new { action = "Index", controller = "Home", name = "products", format = "xml" }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.Equal("/Home/Index/products.xml", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } + + [Fact] + public void GetVirtualPath_OptionalParameter_ParameterNotPresentInValues_PresentInDefaults() + { + // Arrange + var route = CreateRoute( + template: "{controller}/{action}/{name}.{format?}", + defaults: new { format = "json" }, + handleRequest: true, + constraints: null); + + var context = CreateVirtualPathContext( + values: new { action = "Index", controller = "Home", name = "products" }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.Equal("/Home/Index/products", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } + + [Fact] + public void GetVirtualPath_OptionalParameter_ParameterNotPresentInTemplate_PresentInValues() + { + // Arrange + var route = CreateRoute( + template: "{controller}/{action}/{name}", + defaults: null, + handleRequest: true, + constraints: null); + + var context = CreateVirtualPathContext( + values: new { action = "Index", controller = "Home", name = "products", format = "json" }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.Equal("/Home/Index/products?format=json", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } + + [Fact] + public void GetVirtualPath_OptionalParameter_FollowedByDotAfterSlash_ParameterPresent() + { + // Arrange + var route = CreateRoute( + template: "{controller}/{action}/.{name?}", + defaults: null, + handleRequest: true, + constraints: null); + + var context = CreateVirtualPathContext( + values: new { action = "Index", controller = "Home", name = "products" }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.Equal("/Home/Index/.products", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } + + [Fact] + public void GetVirtualPath_OptionalParameter_FollowedByDotAfterSlash_ParameterNotPresent() + { + // Arrange + var route = CreateRoute( + template: "{controller}/{action}/.{name?}", + defaults: null, + handleRequest: true, + constraints: null); + + var context = CreateVirtualPathContext( + values: new { action = "Index", controller = "Home" }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.Equal("/Home/Index/", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } + + [Fact] + public void GetVirtualPath_OptionalParameter_InSimpleSegment() + { + // Arrange + var route = CreateRoute( + template: "{controller}/{action}/{name?}", + defaults: null, + handleRequest: true, + constraints: null); + + var context = CreateVirtualPathContext( + values: new { action = "Index", controller = "Home" }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.Equal("/Home/Index", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } + + [Fact] + public void GetVirtualPath_TwoOptionalParameters_OneValueFromAmbientValues() + { + // Arrange + var route = CreateRoute( + template: "a/{b=15}/{c?}/{d?}", + defaults: null, + handleRequest: true, + constraints: null); + + var context = CreateVirtualPathContext( + values: new { }, + ambientValues: new { c = "17" }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.NotNull(pathData); + Assert.Equal("/a/15/17", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } + + + [Fact] + public void GetVirtualPath_OptionalParameterAfterDefault_OneValueFromAmbientValues() + { + // Arrange + var route = CreateRoute( + template: "a/{b=15}/{c?}", + defaults: null, + handleRequest: true, + constraints: null); + + var context = CreateVirtualPathContext( + values: new { }, + ambientValues: new { c = "17" }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.NotNull(pathData); + Assert.Equal("/a/15/17", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } + + [Fact] + public void GetVirtualPath_TwoOptionalParametersAfterDefault_OneValueFromAmbientValues() + { + // Arrange + var route = CreateRoute( + template: "a/{b=15}/{c?}/{d?}", + defaults: null, + handleRequest: true, + constraints: null); + + var context = CreateVirtualPathContext( + values: new { }, + ambientValues: new { c = "17" }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.NotNull(pathData); + Assert.Equal("/a/15/17", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } + + [Fact] + public void GetVirtualPath_TwoOptionalParametersAfterDefault_LastValueFromAmbientValues() + { + // Arrange + var route = CreateRoute( + template: "a/{b=15}/{c?}/{d?}", + defaults: null, + handleRequest: true, + constraints: null); + + var context = CreateVirtualPathContext( + values: new { }, + ambientValues: new { d = "17" }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.NotNull(pathData); + Assert.Equal("/a", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } + + private static VirtualPathContext CreateVirtualPathContext(object values) + { + return CreateVirtualPathContext(new RouteValueDictionary(values), null); + } + + private static VirtualPathContext CreateVirtualPathContext(object values, object ambientValues) + { + return CreateVirtualPathContext(new RouteValueDictionary(values), new RouteValueDictionary(ambientValues)); + } + + private static VirtualPathContext CreateVirtualPathContext( + RouteValueDictionary values, + RouteValueDictionary ambientValues) + { + var services = new ServiceCollection(); + services.AddSingleton(NullLoggerFactory.Instance); + services.AddSingleton(); + services.AddRouting(); + + var context = new DefaultHttpContext + { + RequestServices = services.BuildServiceProvider(), + }; + + return new VirtualPathContext(context, ambientValues, values); + } + + private static VirtualPathContext CreateVirtualPathContext(string routeName) + { + return new VirtualPathContext(null, null, null, routeName); + } + + public static IEnumerable DataTokens + { + get + { + yield return new object[] { + new Dictionary { { "key1", "data1" }, { "key2", 13 } }, + new Dictionary { { "key1", "data1" }, { "key2", 13 } }, + }; + yield return new object[] { + new RouteValueDictionary { { "key1", "data1" }, { "key2", 13 } }, + new Dictionary { { "key1", "data1" }, { "key2", 13 } }, + }; + yield return new object[] { + new object(), + new Dictionary(), + }; + yield return new object[] { + null, + new Dictionary() + }; + yield return new object[] { + new { key1 = "data1", key2 = 13 }, + new Dictionary { { "key1", "data1" }, { "key2", 13 } }, + }; + } + } + + [Theory] + [MemberData(nameof(DataTokens))] + public void RegisteringRoute_WithDataTokens_AbleToAddTheRoute(object dataToken, + IDictionary expectedDictionary) + { + // Arrange + var routeBuilder = CreateRouteBuilder(); + + // Act + routeBuilder.MapRoute("mockName", + "{controller}/{action}", + defaults: null, + constraints: null, + dataTokens: dataToken); + + // Assert + var templateRoute = (Route)routeBuilder.Routes[0]; + + // Assert + Assert.Equal(expectedDictionary.Count, templateRoute.DataTokens.Count); + foreach (var expectedKey in expectedDictionary.Keys) + { + Assert.True(templateRoute.DataTokens.ContainsKey(expectedKey)); + Assert.Equal(expectedDictionary[expectedKey], templateRoute.DataTokens[expectedKey]); + } + } + + [Fact] + public void RegisteringRouteWithInvalidConstraints_Throws() + { + // Arrange + var routeBuilder = CreateRouteBuilder(); + + // Assert + var expectedMessage = "An error occurred while creating the route with name 'mockName' and template" + + " '{controller}/{action}'."; + + var exception = ExceptionAssert.Throws( + () => routeBuilder.MapRoute("mockName", + "{controller}/{action}", + defaults: null, + constraints: new { controller = "a.*", action = 17 }), + expectedMessage); + + expectedMessage = "The constraint entry 'action' - '17' on the route '{controller}/{action}' " + + "must have a string value or be of a type which implements '" + + typeof(IRouteConstraint) + "'."; + Assert.NotNull(exception.InnerException); + Assert.Equal(expectedMessage, exception.InnerException.Message); + } + + [Fact] + public void RegisteringRouteWithTwoConstraints() + { + // Arrange + var routeBuilder = CreateRouteBuilder(); + + var mockConstraint = new Mock().Object; + + routeBuilder.MapRoute("mockName", + "{controller}/{action}", + defaults: null, + constraints: new { controller = "a.*", action = mockConstraint }); + + var constraints = ((Route)routeBuilder.Routes[0]).Constraints; + + // Assert + Assert.Equal(2, constraints.Count); + Assert.IsType(constraints["controller"]); + Assert.Equal(mockConstraint, constraints["action"]); + } + + [Fact] + public void RegisteringRouteWithOneInlineConstraintAndOneUsingConstraintArgument() + { + // Arrange + var routeBuilder = CreateRouteBuilder(); + + // Act + routeBuilder.MapRoute("mockName", + "{controller}/{action}/{id:int}", + defaults: null, + constraints: new { id = "1*" }); + + // Assert + var constraints = ((Route)routeBuilder.Routes[0]).Constraints; + Assert.Equal(1, constraints.Count); + var constraint = (CompositeRouteConstraint)constraints["id"]; + Assert.IsType(constraint); + Assert.IsType(constraint.Constraints.ElementAt(0)); + Assert.IsType(constraint.Constraints.ElementAt(1)); + } + + [Fact] + public void RegisteringRoute_WithOneInlineConstraint_AddsItToConstraintCollection() + { + // Arrange + var routeBuilder = CreateRouteBuilder(); + + // Act + routeBuilder.MapRoute("mockName", + "{controller}/{action}/{id:int}", + defaults: null, + constraints: null); + + // Assert + var constraints = ((Route)routeBuilder.Routes[0]).Constraints; + Assert.Equal(1, constraints.Count); + Assert.IsType(constraints["id"]); + } + + [Fact] + public void RegisteringRouteWithRouteName_WithNullDefaults_AddsTheRoute() + { + // Arrange + var routeBuilder = CreateRouteBuilder(); + + routeBuilder.MapRoute(name: "RouteName", template: "{controller}/{action}", defaults: null); + + // Act + var name = ((Route)routeBuilder.Routes[0]).Name; + + // Assert + Assert.Equal("RouteName", name); + } + + [Fact] + public void RegisteringRouteWithRouteName_WithNullDefaultsAndConstraints_AddsTheRoute() + { + // Arrange + var routeBuilder = CreateRouteBuilder(); + + routeBuilder.MapRoute(name: "RouteName", + template: "{controller}/{action}", + defaults: null, + constraints: null); + + // Act + var name = ((Route)routeBuilder.Routes[0]).Name; + + // Assert + Assert.Equal("RouteName", name); + } + + [Theory] + [InlineData("///")] + [InlineData("/a//")] + [InlineData("/a/b//")] + [InlineData("//b//")] + [InlineData("///c")] + [InlineData("///c/")] + public async Task RouteAsync_MultipleOptionalParameters_WithEmptyIntermediateSegmentsDoesNotMatch(string url) + { + // Arrange + var builder = CreateRouteBuilder(); + + builder.MapRoute(name: null, + template: "{controller?}/{action?}/{id?}", + defaults: null, + constraints: null); + + var route = builder.Build(); + + var context = CreateRouteContext(url); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.Null(context.Handler); + } + + // DataTokens test data for TemplateRoute.GetVirtualPath + public static IEnumerable DataTokensTestData + { + get + { + yield return new object[] { null }; + yield return new object[] { new RouteValueDictionary() }; + yield return new object[] { new RouteValueDictionary() { { "tokenKeyA", "tokenValueA" } } }; + } + } + + private static IRouteBuilder CreateRouteBuilder() + { + var services = new ServiceCollection(); + services.AddSingleton(_inlineConstraintResolver); + services.AddSingleton(); + + var applicationBuilder = Mock.Of(); + applicationBuilder.ApplicationServices = services.BuildServiceProvider(); + + var routeBuilder = new RouteBuilder(applicationBuilder); + routeBuilder.DefaultHandler = new RouteHandler(NullHandler); + return routeBuilder; + } + + private static Route CreateRoute(string routeName, string template, bool handleRequest = true) + { + return new Route( + CreateTarget(handleRequest), + routeName, + template, + defaults: null, + constraints: null, + dataTokens: null, + inlineConstraintResolver: _inlineConstraintResolver); + } + + private static Route CreateRoute(string template, bool handleRequest = true) + { + return new Route(CreateTarget(handleRequest), template, _inlineConstraintResolver); + } + + private static Route CreateRoute( + string template, + object defaults, + bool handleRequest = true, + object constraints = null, + object dataTokens = null) + { + return new Route( + CreateTarget(handleRequest), + template, + new RouteValueDictionary(defaults), + new RouteValueDictionary(constraints), + new RouteValueDictionary(dataTokens), + _inlineConstraintResolver); + } + + private static Route CreateRoute(IRouter target, string template) + { + return new Route( + target, + template, + new RouteValueDictionary(), + constraints: null, + dataTokens: null, + inlineConstraintResolver: _inlineConstraintResolver); + } + + private static Route CreateRoute( + IRouter target, + string template, + object defaults, + RouteValueDictionary dataTokens = null) + { + return new Route( + target, + template, + new RouteValueDictionary(defaults), + constraints: null, + dataTokens: dataTokens, + inlineConstraintResolver: _inlineConstraintResolver); + } + + private static IRouter CreateTarget(bool handleRequest = true) + { + var target = new Mock(MockBehavior.Strict); + target + .Setup(e => e.GetVirtualPath(It.IsAny())) + .Returns(rc => null); + + target + .Setup(e => e.RouteAsync(It.IsAny())) + .Callback((c) => c.Handler = handleRequest ? NullHandler : null) + .Returns(Task.FromResult(null)); + + return target.Object; + } + + private static IInlineConstraintResolver GetInlineConstraintResolver() + { + var routeOptions = new Mock>(); + routeOptions + .SetupGet(o => o.Value) + .Returns(new RouteOptions()); + + return new DefaultInlineConstraintResolver(routeOptions.Object); + } + + private class CapturingConstraint : IRouteConstraint + { + public IDictionary Values { get; private set; } + + public bool Match( + HttpContext httpContext, + IRouter route, + string routeKey, + RouteValueDictionary values, + RouteDirection routeDirection) + { + Values = new RouteValueDictionary(values); + return true; + } + } + } +} diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RouterMiddlewareTest.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RouterMiddlewareTest.cs new file mode 100644 index 0000000000..464a389b44 --- /dev/null +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RouterMiddlewareTest.cs @@ -0,0 +1,106 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging.Testing; +using Xunit; + +namespace Microsoft.AspNetCore.Routing +{ + public class RouterMiddlewareTest + { + [Fact] + public async void Invoke_LogsCorrectValues_WhenNotHandled() + { + // Arrange + var expectedMessage = "Request did not match any routes."; + var isHandled = false; + + var sink = new TestSink( + TestSink.EnableWithTypeName, + TestSink.EnableWithTypeName); + var loggerFactory = new TestLoggerFactory(sink, enabled: true); + + var httpContext = new DefaultHttpContext(); + httpContext.RequestServices = new ServiceProvider(); + + RequestDelegate next = (c) => + { + return Task.FromResult(null); + }; + + var router = new TestRouter(isHandled); + var middleware = new RouterMiddleware(next, loggerFactory, router); + + // Act + await middleware.Invoke(httpContext); + + // Assert + Assert.Empty(sink.Scopes); + var write = Assert.Single(sink.Writes); + Assert.Equal(expectedMessage, write.State?.ToString()); + } + + [Fact] + public async void Invoke_DoesNotLog_WhenHandled() + { + // Arrange + var isHandled = true; + + var sink = new TestSink( + TestSink.EnableWithTypeName, + TestSink.EnableWithTypeName); + var loggerFactory = new TestLoggerFactory(sink, enabled: true); + + var httpContext = new DefaultHttpContext(); + httpContext.RequestServices = new ServiceProvider(); + + RequestDelegate next = (c) => + { + return Task.FromResult(null); + }; + + var router = new TestRouter(isHandled); + var middleware = new RouterMiddleware(next, loggerFactory, router); + + // Act + await middleware.Invoke(httpContext); + + // Assert + Assert.Empty(sink.Scopes); + Assert.Empty(sink.Writes); + } + + private class TestRouter : IRouter + { + private bool _isHandled; + + public TestRouter(bool isHandled) + { + _isHandled = isHandled; + } + + public VirtualPathData GetVirtualPath(VirtualPathContext context) + { + return new VirtualPathData(this, ""); + } + + public Task RouteAsync(RouteContext context) + { + context.Handler = _isHandled ? (RequestDelegate)((c) => Task.FromResult(0)) : null; + return Task.FromResult(null); + } + } + + private class ServiceProvider : IServiceProvider + { + public object GetService(Type serviceType) + { + throw new NotImplementedException(); + } + } + } +} diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RoutingBuilderExtensionsTest.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RoutingBuilderExtensionsTest.cs new file mode 100644 index 0000000000..9499a6cdeb --- /dev/null +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RoutingBuilderExtensionsTest.cs @@ -0,0 +1,115 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Builder.Internal; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Builder +{ + public class RoutingBuilderExtensionsTest + { + [Fact] + public void UseRouter_IRouter_ThrowsWithoutCallingAddRouting() + { + // Arrange + var app = new ApplicationBuilder(Mock.Of()); + + // Act + var ex = Assert.Throws(() => app.UseRouter(Mock.Of())); + + // Assert + Assert.Equal( + "Unable to find the required services. " + + "Please add all the required services by calling 'IServiceCollection.AddRouting' " + + "inside the call to 'ConfigureServices(...)' in the application startup code.", + ex.Message); + } + + [Fact] + public void UseRouter_Action_ThrowsWithoutCallingAddRouting() + { + // Arrange + var app = new ApplicationBuilder(Mock.Of()); + + // Act + var ex = Assert.Throws(() => app.UseRouter(b => { })); + + // Assert + Assert.Equal( + "Unable to find the required services. " + + "Please add all the required services by calling 'IServiceCollection.AddRouting' " + + "inside the call to 'ConfigureServices(...)' in the application startup code.", + ex.Message); + } + + [Fact] + public async Task UseRouter_IRouter_CallsRoute() + { + // Arrange + var services = CreateServices(); + + var app = new ApplicationBuilder(services); + + var router = new Mock(MockBehavior.Strict); + router + .Setup(r => r.RouteAsync(It.IsAny())) + .Returns(Task.FromResult(0)) + .Verifiable(); + + app.UseRouter(router.Object); + + var appFunc = app.Build(); + + // Act + await appFunc(new DefaultHttpContext()); + + // Assert + router.Verify(); + } + + [Fact] + public async Task UseRouter_Action_CallsRoute() + { + // Arrange + var services = CreateServices(); + + var app = new ApplicationBuilder(services); + + var router = new Mock(MockBehavior.Strict); + router + .Setup(r => r.RouteAsync(It.IsAny())) + .Returns(Task.FromResult(0)) + .Verifiable(); + + app.UseRouter(b => + { + b.Routes.Add(router.Object); + }); + + var appFunc = app.Build(); + + // Act + await appFunc(new DefaultHttpContext()); + + // Assert + router.Verify(); + } + + private IServiceProvider CreateServices() + { + var services = new ServiceCollection(); + + services.AddLogging(); + services.AddOptions(); + services.AddRouting(); + + return services.BuildServiceProvider(); + } + } +} diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Template/RoutePrecedenceTests.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Template/RoutePrecedenceTests.cs new file mode 100644 index 0000000000..7dd04454e2 --- /dev/null +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Template/RoutePrecedenceTests.cs @@ -0,0 +1,121 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Routing.Template +{ + public class RoutePrecedenceTests + { + [Theory] + [InlineData("Employees/{id}", "Employees/{employeeId}")] + [InlineData("abc", "def")] + [InlineData("{x:alpha}", "{x:int}")] + public void ComputeMatched_IsEqual(string xTemplate, string yTemplate) + { + // Arrange & Act + var xPrededence = ComputeMatched(xTemplate); + var yPrededence = ComputeMatched(yTemplate); + + // Assert + Assert.Equal(xPrededence, yPrededence); + } + + [Theory] + [InlineData("Employees/{id}", "Employees/{employeeId}")] + [InlineData("abc", "def")] + [InlineData("{x:alpha}", "{x:int}")] + public void ComputeGenerated_IsEqual(string xTemplate, string yTemplate) + { + // Arrange & Act + var xPrededence = ComputeGenerated(xTemplate); + var yPrededence = ComputeGenerated(yTemplate); + + // Assert + Assert.Equal(xPrededence, yPrededence); + } + + [Theory] + [InlineData("abc", "a{x}")] + [InlineData("abc", "{x}c")] + [InlineData("abc", "{x:int}")] + [InlineData("abc", "{x}")] + [InlineData("abc", "{*x}")] + [InlineData("{x:int}", "{x}")] + [InlineData("{x:int}", "{*x}")] + [InlineData("a{x}", "{x}")] + [InlineData("{x}c", "{x}")] + [InlineData("a{x}", "{*x}")] + [InlineData("{x}c", "{*x}")] + [InlineData("{x}", "{*x}")] + [InlineData("{*x:maxlength(10)}", "{*x}")] + [InlineData("abc/def", "abc/{x:int}")] + [InlineData("abc/def", "abc/{x}")] + [InlineData("abc/def", "abc/{*x}")] + [InlineData("abc/{x:int}", "abc/{x}")] + [InlineData("abc/{x:int}", "abc/{*x}")] + [InlineData("abc/{x}", "abc/{*x}")] + [InlineData("{x}/{y:int}", "{x}/{y}")] + public void ComputeMatched_IsLessThan(string xTemplate, string yTemplate) + { + // Arrange & Act + var xPrededence = ComputeMatched(xTemplate); + var yPrededence = ComputeMatched(yTemplate); + + // Assert + Assert.True(xPrededence < yPrededence); + } + + [Theory] + [InlineData("abc", "a{x}")] + [InlineData("abc", "{x}c")] + [InlineData("abc", "{x:int}")] + [InlineData("abc", "{x}")] + [InlineData("abc", "{*x}")] + [InlineData("{x:int}", "{x}")] + [InlineData("{x:int}", "{*x}")] + [InlineData("a{x}", "{x}")] + [InlineData("{x}c", "{x}")] + [InlineData("a{x}", "{*x}")] + [InlineData("{x}c", "{*x}")] + [InlineData("{x}", "{*x}")] + [InlineData("{*x:maxlength(10)}", "{*x}")] + [InlineData("abc/def", "abc/{x:int}")] + [InlineData("abc/def", "abc/{x}")] + [InlineData("abc/def", "abc/{*x}")] + [InlineData("abc/{x:int}", "abc/{x}")] + [InlineData("abc/{x:int}", "abc/{*x}")] + [InlineData("abc/{x}", "abc/{*x}")] + [InlineData("{x}/{y:int}", "{x}/{y}")] + public void ComputeGenerated_IsGreaterThan(string xTemplate, string yTemplate) + { + // Arrange & Act + var xPrecedence = ComputeGenerated(xTemplate); + var yPrecedence = ComputeGenerated(yTemplate); + + // Assert + Assert.True(xPrecedence > yPrecedence); + } + + private static decimal ComputeMatched(string template) + { + return Compute(template, RoutePrecedence.ComputeInbound); + } + private static decimal ComputeGenerated(string template) + { + return Compute(template, RoutePrecedence.ComputeOutbound); + } + + private static decimal Compute(string template, Func func) + { + var options = new Mock>(); + options.SetupGet(o => o.Value).Returns(new RouteOptions()); + + var parsed = TemplateParser.Parse(template); + return func(parsed); + } + } +} diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Template/TemplateBinderTests.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Template/TemplateBinderTests.cs new file mode 100644 index 0000000000..d4dacaaa6b --- /dev/null +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Template/TemplateBinderTests.cs @@ -0,0 +1,1266 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Encodings.Web; +using Microsoft.AspNetCore.Routing.Internal; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.ObjectPool; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.AspNetCore.Routing.Template.Tests +{ + public class TemplateBinderTests + { + private readonly IInlineConstraintResolver _inlineConstraintResolver = GetInlineConstraintResolver(); + + public static TheoryData EmptyAndNullDefaultValues => + new TheoryData + { + { + "Test/{val1}/{val2}", + new RouteValueDictionary(new {val1 = "", val2 = ""}), + new RouteValueDictionary(new {val2 = "SomeVal2"}), + null + }, + { + "Test/{val1}/{val2}", + new RouteValueDictionary(new {val1 = "", val2 = ""}), + new RouteValueDictionary(new {val1 = "a"}), + "/Test/a" + }, + { + "Test/{val1}/{val2}/{val3}", + new RouteValueDictionary(new {val1 = "", val3 = ""}), + new RouteValueDictionary(new {val2 = "a"}), + null + }, + { + "Test/{val1}/{val2}", + new RouteValueDictionary(new {val1 = "", val2 = ""}), + new RouteValueDictionary(new {val1 = "a", val2 = "b"}), + "/Test/a/b" + }, + { + "Test/{val1}/{val2}/{val3}", + new RouteValueDictionary(new {val1 = "", val2 = "", val3 = ""}), + new RouteValueDictionary(new {val1 = "a", val2 = "b", val3 = "c"}), + "/Test/a/b/c" + }, + { + "Test/{val1}/{val2}/{val3}", + new RouteValueDictionary(new {val1 = "", val2 = "", val3 = ""}), + new RouteValueDictionary(new {val1 = "a", val2 = "b"}), + "/Test/a/b" + }, + { + "Test/{val1}/{val2}/{val3}", + new RouteValueDictionary(new {val1 = "", val2 = "", val3 = ""}), + new RouteValueDictionary(new {val1 = "a"}), + "/Test/a" + }, + { + "Test/{val1}", + new RouteValueDictionary(new {val1 = "42", val2 = "", val3 = ""}), + new RouteValueDictionary(), + "/Test" + }, + { + "Test/{val1}/{val2}/{val3}", + new RouteValueDictionary(new {val1 = "42", val2 = (string)null, val3 = (string)null}), + new RouteValueDictionary(), + "/Test" + }, + { + "Test/{val1}/{val2}/{val3}/{val4}", + new RouteValueDictionary(new {val1 = "21", val2 = "", val3 = "", val4 = ""}), + new RouteValueDictionary(new {val1 = "42", val2 = "11", val3 = "", val4 = ""}), + "/Test/42/11" + }, + { + "Test/{val1}/{val2}/{val3}", + new RouteValueDictionary(new {val1 = "21", val2 = "", val3 = ""}), + new RouteValueDictionary(new {val1 = "42"}), + "/Test/42" + }, + { + "Test/{val1}/{val2}/{val3}/{val4}", + new RouteValueDictionary(new {val1 = "21", val2 = "", val3 = "", val4 = ""}), + new RouteValueDictionary(new {val1 = "42", val2 = "11"}), + "/Test/42/11" + }, + { + "Test/{val1}/{val2}/{val3}", + new RouteValueDictionary(new {val1 = "21", val2 = (string)null, val3 = (string)null}), + new RouteValueDictionary(new {val1 = "42"}), + "/Test/42" + }, + { + "Test/{val1}/{val2}/{val3}/{val4}", + new RouteValueDictionary(new {val1 = "21", val2 = (string)null, val3 = (string)null, val4 = (string)null}), + new RouteValueDictionary(new {val1 = "42", val2 = "11"}), + "/Test/42/11" + }, + }; + + [Theory] + [MemberData(nameof(EmptyAndNullDefaultValues))] + public void Binding_WithEmptyAndNull_DefaultValues( + string template, + RouteValueDictionary defaults, + RouteValueDictionary values, + string expected) + { + // Arrange + var binder = new TemplateBinder( + UrlEncoder.Default, + new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy()), + TemplateParser.Parse(template), + defaults); + + // Act & Assert + var result = binder.GetValues(ambientValues: null, values: values); + if (result == null) + { + if (expected == null) + { + return; + } + else + { + Assert.NotNull(result); + } + } + + var boundTemplate = binder.BindValues(result.AcceptedValues); + if (expected == null) + { + Assert.Null(boundTemplate); + } + else + { + Assert.NotNull(boundTemplate); + Assert.Equal(expected, boundTemplate); + } + } + + [Fact] + public void GetVirtualPathWithMultiSegmentParamsOnBothEndsMatches() + { + RunTest( + "language/{lang}-{region}", + null, + new RouteValueDictionary(new { lang = "en", region = "US" }), + new RouteValueDictionary(new { lang = "xx", region = "yy" }), + "/language/xx-yy"); + } + + [Fact] + public void GetVirtualPathWithMultiSegmentParamsOnLeftEndMatches() + { + RunTest( + "language/{lang}-{region}a", + null, + new RouteValueDictionary(new { lang = "en", region = "US" }), + new RouteValueDictionary(new { lang = "xx", region = "yy" }), + "/language/xx-yya"); + } + + [Fact] + public void GetVirtualPathWithMultiSegmentParamsOnRightEndMatches() + { + RunTest( + "language/a{lang}-{region}", + null, + new RouteValueDictionary(new { lang = "en", region = "US" }), + new RouteValueDictionary(new { lang = "xx", region = "yy" }), + "/language/axx-yy"); + } + + public static TheoryData OptionalParamValues => + new TheoryData + { + // defaults + // ambient values + // values + { + "Test/{val1}/{val2}.{val3?}", + new RouteValueDictionary(new {val1 = "someval1", val2 = "someval2"}), + new RouteValueDictionary(new {val3 = "someval3"}), + new RouteValueDictionary(new {val3 = "someval3"}), + "/Test/someval1/someval2.someval3" + }, + { + "Test/{val1}/{val2}.{val3?}", + new RouteValueDictionary(new {val1 = "someval1", val2 = "someval2"}), + new RouteValueDictionary(new {val3 = "someval3a"}), + new RouteValueDictionary(new {val3 = "someval3v"}), + "/Test/someval1/someval2.someval3v" + }, + { + "Test/{val1}/{val2}.{val3?}", + new RouteValueDictionary(new {val1 = "someval1", val2 = "someval2"}), + new RouteValueDictionary(new {val3 = "someval3a"}), + new RouteValueDictionary(), + "/Test/someval1/someval2.someval3a" + }, + { + "Test/{val1}/{val2}.{val3?}", + new RouteValueDictionary(new {val1 = "someval1", val2 = "someval2"}), + new RouteValueDictionary(), + new RouteValueDictionary(new {val3 = "someval3v"}), + "/Test/someval1/someval2.someval3v" + }, + { + "Test/{val1}/{val2}.{val3?}", + new RouteValueDictionary(new {val1 = "someval1", val2 = "someval2"}), + new RouteValueDictionary(), + new RouteValueDictionary(), + "/Test/someval1/someval2" + }, + { + "Test/{val1}.{val2}.{val3}.{val4?}", + new RouteValueDictionary(new {val1 = "someval1", val2 = "someval2" }), + new RouteValueDictionary(), + new RouteValueDictionary(new {val4 = "someval4", val3 = "someval3" }), + "/Test/someval1.someval2." + + "someval3.someval4" + }, + { + "Test/{val1}.{val2}.{val3}.{val4?}", + new RouteValueDictionary(new {val1 = "someval1", val2 = "someval2" }), + new RouteValueDictionary(), + new RouteValueDictionary(new {val3 = "someval3" }), + "/Test/someval1.someval2." + + "someval3" + }, + { + "Test/.{val2?}", + new RouteValueDictionary(new { }), + new RouteValueDictionary(), + new RouteValueDictionary(new {val2 = "someval2" }), + "/Test/.someval2" + }, + { + "Test/{val1}.{val2}", + new RouteValueDictionary(new {val1 = "someval1", val2 = "someval2" }), + new RouteValueDictionary(), + new RouteValueDictionary(new {val3 = "someval3" }), + "/Test/someval1.someval2?" + + "val3=someval3" + }, + }; + + [Theory] + [MemberData(nameof(OptionalParamValues))] + public void GetVirtualPathWithMultiSegmentWithOptionalParam( + string template, + RouteValueDictionary defaults, + RouteValueDictionary ambientValues, + RouteValueDictionary values, + string expected) + { + // Arrange + var binder = new TemplateBinder( + UrlEncoder.Default, + new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy()), + TemplateParser.Parse(template), + defaults); + + // Act & Assert + var result = binder.GetValues(ambientValues: ambientValues, values: values); + if (result == null) + { + if (expected == null) + { + return; + } + else + { + Assert.NotNull(result); + } + } + + var boundTemplate = binder.BindValues(result.AcceptedValues); + if (expected == null) + { + Assert.Null(boundTemplate); + } + else + { + Assert.NotNull(boundTemplate); + Assert.Equal(expected, boundTemplate); + } + } + + [Fact] + public void GetVirtualPathWithMultiSegmentParamsOnNeitherEndMatches() + { + RunTest( + "language/a{lang}-{region}a", + null, + new RouteValueDictionary(new { lang = "en", region = "US" }), + new RouteValueDictionary(new { lang = "xx", region = "yy" }), + "/language/axx-yya"); + } + + [Fact] + public void GetVirtualPathWithMultiSegmentParamsOnNeitherEndDoesNotMatch() + { + RunTest( + "language/a{lang}-{region}a", + null, + new RouteValueDictionary(new { lang = "en", region = "US" }), + new RouteValueDictionary(new { lang = "", region = "yy" }), + null); + } + + [Fact] + public void GetVirtualPathWithMultiSegmentParamsOnNeitherEndDoesNotMatch2() + { + RunTest( + "language/a{lang}-{region}a", + null, + new RouteValueDictionary(new { lang = "en", region = "US" }), + new RouteValueDictionary(new { lang = "xx", region = "" }), + null); + } + + [Fact] + public void GetVirtualPathWithSimpleMultiSegmentParamsOnBothEndsMatches() + { + RunTest( + "language/{lang}", + null, + new RouteValueDictionary(new { lang = "en" }), + new RouteValueDictionary(new { lang = "xx" }), + "/language/xx"); + } + + [Fact] + public void GetVirtualPathWithSimpleMultiSegmentParamsOnLeftEndMatches() + { + RunTest( + "language/{lang}-", + null, + new RouteValueDictionary(new { lang = "en" }), + new RouteValueDictionary(new { lang = "xx" }), + "/language/xx-"); + } + + [Fact] + public void GetVirtualPathWithSimpleMultiSegmentParamsOnRightEndMatches() + { + RunTest( + "language/a{lang}", + null, + new RouteValueDictionary(new { lang = "en" }), + new RouteValueDictionary(new { lang = "xx" }), + "/language/axx"); + } + + [Fact] + public void GetVirtualPathWithSimpleMultiSegmentParamsOnNeitherEndMatches() + { + RunTest( + "language/a{lang}a", + null, + new RouteValueDictionary(new { lang = "en" }), + new RouteValueDictionary(new { lang = "xx" }), + "/language/axxa"); + } + + [Fact] + public void GetVirtualPathWithMultiSegmentStandardMvcRouteMatches() + { + RunTest( + "{controller}.mvc/{action}/{id}", + new RouteValueDictionary(new { action = "Index", id = (string)null }), + new RouteValueDictionary(new { controller = "home", action = "list", id = (string)null }), + new RouteValueDictionary(new { controller = "products" }), + "/products.mvc"); + } + + [Fact] + public void GetVirtualPathWithMultiSegmentParamsOnBothEndsWithDefaultValuesMatches() + { + RunTest( + "language/{lang}-{region}", + new RouteValueDictionary(new { lang = "xx", region = "yy" }), + new RouteValueDictionary(new { lang = "en", region = "US" }), + new RouteValueDictionary(new { lang = "zz" }), + "/language/zz-yy"); + } + + [Fact] + public void GetUrlWithDefaultValue() + { + // URL should be found but excluding the 'id' parameter, which has only a default value. + RunTest( + "{controller}/{action}/{id}", + new RouteValueDictionary(new { id = "defaultid" }), + new RouteValueDictionary(new { controller = "home", action = "oldaction" }), + new RouteValueDictionary(new { action = "newaction" }), + "/home/newaction"); + } + + [Fact] + public void GetVirtualPathWithEmptyStringRequiredValueReturnsNull() + { + RunTest( + "foo/{controller}", + null, + new RouteValueDictionary(new { }), + new RouteValueDictionary(new { controller = "" }), + null); + } + + [Fact] + public void GetVirtualPathWithNullRequiredValueReturnsNull() + { + RunTest( + "foo/{controller}", + null, + new RouteValueDictionary(new { }), + new RouteValueDictionary(new { controller = (string)null }), + null); + } + + [Fact] + public void GetVirtualPathWithRequiredValueReturnsPath() + { + RunTest( + "foo/{controller}", + null, + new RouteValueDictionary(new { }), + new RouteValueDictionary(new { controller = "home" }), + "/foo/home"); + } + + [Fact] + public void GetUrlWithNullDefaultValue() + { + // URL should be found but excluding the 'id' parameter, which has only a default value. + RunTest( + "{controller}/{action}/{id}", + new RouteValueDictionary(new { id = (string)null }), + new RouteValueDictionary(new { controller = "home", action = "oldaction", id = (string)null }), + new RouteValueDictionary(new { action = "newaction" }), + "/home/newaction"); + } + + [Fact] + public void GetVirtualPathCanFillInSeparatedParametersWithDefaultValues() + { + RunTest( + "{controller}/{language}-{locale}", + new RouteValueDictionary(new { language = "en", locale = "US" }), + new RouteValueDictionary(), + new RouteValueDictionary(new { controller = "Orders" }), + "/Orders/en-US"); + } + + [Fact] + public void GetVirtualPathWithUnusedNullValueShouldGenerateUrlAndIgnoreNullValue() + { + RunTest( + "{controller}.mvc/{action}/{id}", + new RouteValueDictionary(new { action = "Index", id = "" }), + new RouteValueDictionary(new { controller = "Home", action = "Index", id = "" }), + new RouteValueDictionary(new { controller = "Home", action = "TestAction", id = "1", format = (string)null }), + "/Home.mvc/TestAction/1"); + } + + [Fact] + public void GetUrlWithMissingValuesDoesntMatch() + { + RunTest( + "{controller}/{action}/{id}", + null, + new { controller = "home", action = "oldaction" }, + new { action = "newaction" }, + null); + } + + [Fact] + public void GetUrlWithEmptyRequiredValuesReturnsNull() + { + RunTest( + "{p1}/{p2}/{p3}", + null, + new { p1 = "v1", }, + new { p2 = "", p3 = "" }, + null); + } + + [Fact] + public void GetUrlWithEmptyOptionalValuesReturnsShortUrl() + { + RunTest( + "{p1}/{p2}/{p3}", + new { p2 = "d2", p3 = "d3" }, + new { p1 = "v1", }, + new { p2 = "", p3 = "" }, + "/v1"); + } + + [Fact] + public void GetUrlShouldIgnoreValuesAfterChangedParameter() + { + RunTest( + "{controller}/{action}/{id}", + new { action = "Index", id = (string)null }, + new { controller = "orig", action = "init", id = "123" }, + new { action = "new", }, + "/orig/new"); + } + + [Fact] + public void GetUrlWithNullForMiddleParameterIgnoresRemainingParameters() + { + RunTest( + "UrlGeneration1/{controller}.mvc/{action}/{category}/{year}/{occasion}/{SafeParam}", + new { year = 1995, occasion = "Christmas", action = "Play", SafeParam = "SafeParamValue" }, + new { controller = "UrlRouting", action = "Play", category = "Photos", year = "2008", occasion = "Easter", SafeParam = "SafeParamValue" }, + new { year = (string)null, occasion = "Hola" }, + "/UrlGeneration1/UrlRouting.mvc/Play/" + + "Photos/1995/Hola"); + } + + [Fact] + public void GetUrlWithEmptyStringForMiddleParameterIgnoresRemainingParameters() + { + var ambientValues = new RouteValueDictionary(); + ambientValues.Add("controller", "UrlRouting"); + ambientValues.Add("action", "Play"); + ambientValues.Add("category", "Photos"); + ambientValues.Add("year", "2008"); + ambientValues.Add("occasion", "Easter"); + ambientValues.Add("SafeParam", "SafeParamValue"); + + var values = new RouteValueDictionary(); + values.Add("year", String.Empty); + values.Add("occasion", "Hola"); + + RunTest( + "UrlGeneration1/{controller}.mvc/{action}/{category}/{year}/{occasion}/{SafeParam}", + new RouteValueDictionary(new { year = 1995, occasion = "Christmas", action = "Play", SafeParam = "SafeParamValue" }), + ambientValues, + values, + "/UrlGeneration1/UrlRouting.mvc/" + + "Play/Photos/1995/Hola"); + } + + [Fact] + public void GetUrlWithEmptyStringForMiddleParameterShouldUseDefaultValue() + { + var ambientValues = new RouteValueDictionary(); + ambientValues.Add("Controller", "Test"); + ambientValues.Add("Action", "Fallback"); + ambientValues.Add("param1", "fallback1"); + ambientValues.Add("param2", "fallback2"); + ambientValues.Add("param3", "fallback3"); + + var values = new RouteValueDictionary(); + values.Add("controller", "subtest"); + values.Add("param1", "b"); + + RunTest( + "{controller}.mvc/{action}/{param1}", + new RouteValueDictionary(new { action = "Default" }), + ambientValues, + values, + "/subtest.mvc/Default/b"); + } + + [Fact] + public void GetUrlVerifyEncoding() + { + var values = new RouteValueDictionary(); + values.Add("controller", "#;?:@&=+$,"); + values.Add("action", "showcategory"); + values.Add("id", 123); + values.Add("so?rt", "de?sc"); + values.Add("maxPrice", 100); + + RunTest( + "{controller}.mvc/{action}/{id}", + new RouteValueDictionary(new { controller = "Home" }), + new RouteValueDictionary(new { controller = "home", action = "Index", id = (string)null }), + values, + "/%23;%3F%3A@%26%3D%2B$,.mvc/showcategory/123?so%3Frt=de%3Fsc&maxPrice=100"); + } + + [Fact] + public void GetUrlGeneratesQueryStringForNewValuesAndEscapesQueryString() + { + var values = new RouteValueDictionary(new { controller = "products", action = "showcategory", id = 123, maxPrice = 100 }); + values.Add("so?rt", "de?sc"); + + RunTest( + "{controller}.mvc/{action}/{id}", + new RouteValueDictionary(new { controller = "Home" }), + new RouteValueDictionary(new { controller = "home", action = "Index", id = (string)null }), + values, + "/products.mvc/showcategory/123" + + "?so%3Frt=de%3Fsc&maxPrice=100"); + } + + [Fact] + public void GetUrlGeneratesQueryStringForNewValuesButIgnoresNewValuesThatMatchDefaults() + { + RunTest( + "{controller}.mvc/{action}/{id}", + new RouteValueDictionary(new { controller = "Home", Custom = "customValue" }), + new RouteValueDictionary(new { controller = "Home", action = "Index", id = (string)null }), + new RouteValueDictionary( + new + { + controller = "products", + action = "showcategory", + id = 123, + sort = "desc", + maxPrice = 100, + custom = "customValue" + }), + "/products.mvc/showcategory/123" + + "?sort=desc&maxPrice=100"); + } + + [Fact] + public void GetVirtualPathEncodesParametersAndLiterals() + { + RunTest( + "bl%og/{controller}/he llo/{action}", + null, + new RouteValueDictionary(new { controller = "ho%me", action = "li st" }), + new RouteValueDictionary(), + "/bl%25og/ho%25me/he%20llo/li%20st"); + } + + [Fact] + public void GetVirtualDoesNotEncodeLeadingSlashes() + { + RunTest( + "{controller}/{action}", + null, + new RouteValueDictionary(new { controller = "/home", action = "/my/index" }), + new RouteValueDictionary(), + "/home/%2Fmy%2Findex"); + } + + [Fact] + public void GetUrlWithCatchAllWithValue() + { + RunTest( + "{p1}/{*p2}", + new RouteValueDictionary(new { id = "defaultid" }), + new RouteValueDictionary(new { p1 = "v1" }), + new RouteValueDictionary(new { p2 = "v2a/v2b" }), + "/v1/v2a%2Fv2b"); + } + + [Fact] + public void GetUrlWithCatchAllWithEmptyValue() + { + RunTest( + "{p1}/{*p2}", + new RouteValueDictionary(new { id = "defaultid" }), + new RouteValueDictionary(new { p1 = "v1" }), + new RouteValueDictionary(new { p2 = "" }), + "/v1"); + } + + [Fact] + public void GetUrlWithCatchAllWithNullValue() + { + RunTest( + "{p1}/{*p2}", + new RouteValueDictionary(new { id = "defaultid" }), + new RouteValueDictionary(new { p1 = "v1" }), + new RouteValueDictionary(new { p2 = (string)null }), + "/v1"); + } + + [Fact] + public void GetUrlWithLeadingTildeSlash() + { + RunTest( + "~/foo", + null, + null, + new RouteValueDictionary(new { }), + "/foo"); + } + + [Fact] + public void GetUrlWithLeadingSlash() + { + RunTest( + "/foo", + null, + null, + new RouteValueDictionary(new { }), + "/foo"); + } + + [Fact] + public void TemplateBinder_KeepsExplicitlySuppliedRouteValues_OnFailedRouetMatch() + { + // Arrange + var template = "{area?}/{controller=Home}/{action=Index}/{id?}"; + var binder = new TemplateBinder( + UrlEncoder.Default, + new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy()), + TemplateParser.Parse(template), + defaults: null); + var ambientValues = new RouteValueDictionary(); + var routeValues = new RouteValueDictionary(new { controller = "Test", action = "Index" }); + + // Act + var templateValuesResult = binder.GetValues(ambientValues, routeValues); + var boundTemplate = binder.BindValues(templateValuesResult.AcceptedValues); + + // Assert + Assert.Null(boundTemplate); + Assert.Equal(2, templateValuesResult.CombinedValues.Count); + object routeValue; + Assert.True(templateValuesResult.CombinedValues.TryGetValue("controller", out routeValue)); + Assert.Equal("Test", routeValue?.ToString()); + Assert.True(templateValuesResult.CombinedValues.TryGetValue("action", out routeValue)); + Assert.Equal("Index", routeValue?.ToString()); + } + +#if ROUTE_COLLECTION + + [Fact] + public void GetUrlShouldValidateOnlyAcceptedParametersAndUserDefaultValuesForInvalidatedParameters() + { + // Arrange + var rd = CreateRouteData(); + rd.Values.Add("Controller", "UrlRouting"); + rd.Values.Add("Name", "MissmatchedValidateParams"); + rd.Values.Add("action", "MissmatchedValidateParameters2"); + rd.Values.Add("ValidateParam1", "special1"); + rd.Values.Add("ValidateParam2", "special2"); + + IRouteCollection rc = new DefaultRouteCollection(); + rc.Add(CreateRoute( + "UrlConstraints/Validation.mvc/Input5/{action}/{ValidateParam1}/{ValidateParam2}", + new RouteValueDictionary(new { Controller = "UrlRouting", Name = "MissmatchedValidateParams", ValidateParam2 = "valid" }), + new RouteValueDictionary(new { ValidateParam1 = "valid.*", ValidateParam2 = "valid.*" }))); + + rc.Add(CreateRoute( + "UrlConstraints/Validation.mvc/Input5/{action}/{ValidateParam1}/{ValidateParam2}", + new RouteValueDictionary(new { Controller = "UrlRouting", Name = "MissmatchedValidateParams" }), + new RouteValueDictionary(new { ValidateParam1 = "special.*", ValidateParam2 = "special.*" }))); + + var values = CreateRouteValueDictionary(); + values.Add("Name", "MissmatchedValidateParams"); + values.Add("ValidateParam1", "valid1"); + + // Act + var vpd = rc.GetVirtualPath(GetHttpContext("/app1", "", ""), values); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("/app1/UrlConstraints/Validation.mvc/Input5/MissmatchedValidateParameters2/valid1", vpd.VirtualPath); + } + + [Fact] + public void GetUrlWithRouteThatHasExtensionWithSubsequentDefaultValueIncludesExtensionButNotDefaultValue() + { + // Arrange + var rd = CreateRouteData(); + rd.Values.Add("controller", "Bank"); + rd.Values.Add("action", "MakeDeposit"); + rd.Values.Add("accountId", "7770"); + + IRouteCollection rc = new DefaultRouteCollection(); + rc.Add(CreateRoute( + "{controller}.mvc/Deposit/{accountId}", + new RouteValueDictionary(new { Action = "DepositView" }))); + + // Note: This route was in the original bug, but it turns out that this behavior is incorrect. With the + // recent fix to Route (in this changelist) this route would have been selected since we have values for + // all three required parameters. + //rc.Add(new Route { + // Url = "{controller}.mvc/{action}/{accountId}", + // RouteHandler = new DummyRouteHandler() + //}); + + // This route should be chosen because the requested action is List. Since the default value of the action + // is List then the Action should not be in the URL. However, the file extension should be included since + // it is considered "safe." + rc.Add(CreateRoute( + "{controller}.mvc/{action}", + new RouteValueDictionary(new { Action = "List" }))); + + var values = CreateRouteValueDictionary(); + values.Add("Action", "List"); + + // Act + var vpd = rc.GetVirtualPath(GetHttpContext("/app1", "", ""), values); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("/app1/Bank.mvc", vpd.VirtualPath); + } + + [Fact] + public void GetUrlWithRouteThatHasDifferentControllerCaseShouldStillMatch() + { + // Arrange + var rd = CreateRouteData(); + rd.Values.Add("controller", "Bar"); + rd.Values.Add("action", "bbb"); + rd.Values.Add("id", null); + + IRouteCollection rc = new DefaultRouteCollection(); + rc.Add(CreateRoute("PrettyFooUrl", new RouteValueDictionary(new { controller = "Foo", action = "aaa", id = (string)null }))); + + rc.Add(CreateRoute("PrettyBarUrl", new RouteValueDictionary(new { controller = "Bar", action = "bbb", id = (string)null }))); + + rc.Add(CreateRoute("{controller}/{action}/{id}", new RouteValueDictionary(new { action = "Index", id = (string)null }))); + + var values = CreateRouteValueDictionary(); + values.Add("Action", "aaa"); + values.Add("Controller", "foo"); + + // Act + var vpd = rc.GetVirtualPath(GetHttpContext("/app1", "", ""), values); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("/app1/PrettyFooUrl", vpd.VirtualPath); + } + + [Fact] + public void GetUrlWithNoChangedValuesShouldProduceSameUrl() + { + // Arrange + var rd = CreateRouteData(); + rd.Values.Add("controller", "Home"); + rd.Values.Add("action", "Index"); + rd.Values.Add("id", null); + + IRouteCollection rc = new DefaultRouteCollection(); + rc.Add(CreateRoute("{controller}.mvc/{action}/{id}", new RouteValueDictionary(new { action = "Index", id = (string)null }))); + + rc.Add(CreateRoute("{controller}/{action}/{id}", new RouteValueDictionary(new { action = "Index", id = (string)null }))); + + var values = CreateRouteValueDictionary(); + values.Add("Action", "Index"); + + // Act + var vpd = rc.GetVirtualPath(GetHttpContext("/app1", "", ""), values); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("/app1/Home.mvc", vpd.VirtualPath); + } + + [Fact] + public void GetUrlAppliesConstraintsRulesToChooseRoute() + { + // Arrange + var rd = CreateRouteData(); + rd.Values.Add("controller", "Home"); + rd.Values.Add("action", "Index"); + rd.Values.Add("id", null); + + IRouteCollection rc = new DefaultRouteCollection(); + rc.Add(CreateRoute( + "foo.mvc/{action}", + new RouteValueDictionary(new { controller = "Home" }), + new RouteValueDictionary(new { controller = "Home", action = "Contact", httpMethod = CreateHttpMethodConstraint("get") }))); + + rc.Add(CreateRoute( + "{controller}.mvc/{action}", + new RouteValueDictionary(new { action = "Index" }), + new RouteValueDictionary(new { controller = "Home", action = "(Index|About)", httpMethod = CreateHttpMethodConstraint("post") }))); + + var values = CreateRouteValueDictionary(); + values.Add("Action", "Index"); + + // Act + var vpd = rc.GetVirtualPath(GetHttpContext("/app1", "", ""), values); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("/app1/Home.mvc", vpd.VirtualPath); + } + + [Fact] + public void GetUrlWithValuesThatAreCompletelyDifferentFromTheCurrentRoute() + { + // Arrange + HttpContext context = GetHttpContext("/app", null, null); + IRouteCollection rt = new DefaultRouteCollection(); + rt.Add(CreateRoute("date/{y}/{m}/{d}", null)); + rt.Add(CreateRoute("{controller}/{action}/{id}", null)); + + var rd = CreateRouteData(); + rd.Values.Add("controller", "home"); + rd.Values.Add("action", "dostuff"); + + var values = CreateRouteValueDictionary(); + values.Add("y", "2007"); + values.Add("m", "08"); + values.Add("d", "12"); + + // Act + var vpd = rt.GetVirtualPath(context, values); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("/app/date/2007/08/12", vpd.VirtualPath); + } + + [Fact] + public void GetUrlWithValuesThatAreCompletelyDifferentFromTheCurrentRouteAsSecondRoute() + { + // Arrange + HttpContext context = GetHttpContext("/app", null, null); + + IRouteCollection rt = new DefaultRouteCollection(); + rt.Add(CreateRoute("{controller}/{action}/{id}")); + rt.Add(CreateRoute("date/{y}/{m}/{d}")); + + var rd = CreateRouteData(); + rd.Values.Add("controller", "home"); + rd.Values.Add("action", "dostuff"); + + var values = CreateRouteValueDictionary(); + values.Add("y", "2007"); + values.Add("m", "08"); + values.Add("d", "12"); + + // Act + var vpd = rt.GetVirtualPath(context, values); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("/app/date/2007/08/12", vpd.VirtualPath); + } + + [Fact] + public void GetVirtualPathUsesCurrentValuesNotInRouteToMatch() + { + // Arrange + HttpContext context = GetHttpContext("/app", null, null); + TemplateRoute r1 = CreateRoute( + "ParameterMatching.mvc/{Action}/{product}", + new RouteValueDictionary(new { Controller = "ParameterMatching", product = (string)null }), + null); + + TemplateRoute r2 = CreateRoute( + "{controller}.mvc/{action}", + new RouteValueDictionary(new { Action = "List" }), + new RouteValueDictionary(new { Controller = "Action|Bank|Overridden|DerivedFromAction|OverrideInvokeActionAndExecute|InvalidControllerName|Store|HtmlHelpers|(T|t)est|UrlHelpers|Custom|Parent|Child|TempData|ViewFactory|LocatingViews|AccessingDataInViews|ViewOverrides|ViewMasterPage|InlineCompileError|CustomView" }), + null); + + var rd = CreateRouteData(); + rd.Values.Add("controller", "Bank"); + rd.Values.Add("Action", "List"); + var valuesDictionary = CreateRouteValueDictionary(); + valuesDictionary.Add("action", "AttemptLogin"); + + // Act for first route + var vpd = r1.GetVirtualPath(context, valuesDictionary); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("ParameterMatching.mvc/AttemptLogin", vpd.VirtualPath); + + // Act for second route + vpd = r2.GetVirtualPath(context, valuesDictionary); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("Bank.mvc/AttemptLogin", vpd.VirtualPath); + } + +#endif + +#if DATA_TOKENS + [Fact] + public void GetVirtualPathWithDataTokensCopiesThemFromRouteToVirtualPathData() + { + // Arrange + HttpContext context = GetHttpContext("/app", null, null); + TemplateRoute r = CreateRoute("{controller}/{action}", null, null, new RouteValueDictionary(new { foo = "bar", qux = "quux" })); + + var rd = CreateRouteData(); + rd.Values.Add("controller", "home"); + rd.Values.Add("action", "index"); + var valuesDictionary = CreateRouteValueDictionary(); + + // Act + var vpd = r.GetVirtualPath(context, valuesDictionary); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("home/index", vpd.VirtualPath); + Assert.Equal(r, vpd.Route); + Assert.Equal(2, vpd.DataTokens.Count); + Assert.Equal("bar", vpd.DataTokens["foo"]); + Assert.Equal("quux", vpd.DataTokens["qux"]); + } +#endif + +#if ROUTE_FORMAT_HELPER + + [Fact] + public void UrlWithEscapedOpenCloseBraces() + { + RouteFormatHelper("foo/{{p1}}", "foo/{p1}"); + } + + [Fact] + public void UrlWithEscapedOpenBraceAtTheEnd() + { + RouteFormatHelper("bar{{", "bar{"); + } + + [Fact] + public void UrlWithEscapedOpenBraceAtTheBeginning() + { + RouteFormatHelper("{{bar", "{bar"); + } + + [Fact] + public void UrlWithRepeatedEscapedOpenBrace() + { + RouteFormatHelper("foo{{{{bar", "foo{{bar"); + } + + [Fact] + public void UrlWithEscapedCloseBraceAtTheEnd() + { + RouteFormatHelper("bar}}", "bar}"); + } + + [Fact] + public void UrlWithEscapedCloseBraceAtTheBeginning() + { + RouteFormatHelper("}}bar", "}bar"); + } + + [Fact] + public void UrlWithRepeatedEscapedCloseBrace() + { + RouteFormatHelper("foo}}}}bar", "foo}}bar"); + } + + private static void RouteFormatHelper(string routeUrl, string requestUrl) + { + var defaults = new RouteValueDictionary(new { route = "matched" }); + var r = CreateRoute(routeUrl, defaults, null); + + GetRouteDataHelper(r, requestUrl, defaults); + GetVirtualPathHelper(r, new RouteValueDictionary(), null, Uri.EscapeUriString(requestUrl)); + } + +#endif + +#if CONSTRAINTS + [Fact] + public void GetVirtualPathWithNonParameterConstraintReturnsUrlWithoutQueryString() + { + // DevDiv Bugs 199612: UrlRouting: UrlGeneration should not append parameter to query string if it is a Constraint parameter and not a Url parameter + RunTest( + "{Controller}.mvc/{action}/{end}", + null, + new RouteValueDictionary(new { foo = CreateHttpMethodConstraint("GET") }), + new RouteValueDictionary(), + new RouteValueDictionary(new { controller = "Orders", action = "Index", end = "end", foo = "GET" }), + "Orders.mvc/Index/end"); + } + + [Fact] + public void GetVirtualPathWithValidCustomConstraints() + { + // Arrange + HttpContext context = GetHttpContext("/app", null, null); + CustomConstraintTemplateRoute r = new CustomConstraintTemplateRoute("{controller}/{action}", null, new RouteValueDictionary(new { action = 5 })); + + var rd = CreateRouteData(); + rd.Values.Add("controller", "home"); + rd.Values.Add("action", "index"); + + var valuesDictionary = CreateRouteValueDictionary(); + + // Act + var vpd = r.GetVirtualPath(context, valuesDictionary); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("home/index", vpd.VirtualPath); + Assert.Equal(r, vpd.Route); + Assert.NotNull(r.ConstraintData); + Assert.Equal(5, r.ConstraintData.Constraint); + Assert.Equal("action", r.ConstraintData.ParameterName); + Assert.Equal("index", r.ConstraintData.ParameterValue); + } + + [Fact] + public void GetVirtualPathWithInvalidCustomConstraints() + { + // Arrange + HttpContext context = GetHttpContext("/app", null, null); + CustomConstraintTemplateRoute r = new CustomConstraintTemplateRoute("{controller}/{action}", null, new RouteValueDictionary(new { action = 5 })); + + var rd = CreateRouteData(); + rd.Values.Add("controller", "home"); + rd.Values.Add("action", "list"); + + var valuesDictionary = CreateRouteValueDictionary(); + + // Act + var vpd = r.GetVirtualPath(context, valuesDictionary); + + // Assert + Assert.Null(vpd); + Assert.NotNull(r.ConstraintData); + Assert.Equal(5, r.ConstraintData.Constraint); + Assert.Equal("action", r.ConstraintData.ParameterName); + Assert.Equal("list", r.ConstraintData.ParameterValue); + } + +#endif + + private static void RunTest( + string template, + RouteValueDictionary defaults, + RouteValueDictionary ambientValues, + RouteValueDictionary values, + string expected) + { + // Arrange + var binder = new TemplateBinder( + UrlEncoder.Default, + new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy()), + TemplateParser.Parse(template), + defaults); + + // Act & Assert + var result = binder.GetValues(ambientValues, values); + if (result == null) + { + if (expected == null) + { + return; + } + else + { + Assert.NotNull(result); + } + } + + var boundTemplate = binder.BindValues(result.AcceptedValues); + if (expected == null) + { + Assert.Null(boundTemplate); + } + else + { + Assert.NotNull(boundTemplate); + + // We want to chop off the query string and compare that using an unordered comparison + var expectedParts = new PathAndQuery(expected); + var actualParts = new PathAndQuery(boundTemplate); + + Assert.Equal(expectedParts.Path, actualParts.Path); + + if (expectedParts.Parameters == null) + { + Assert.Null(actualParts.Parameters); + } + else + { + Assert.Equal(expectedParts.Parameters.Count, actualParts.Parameters.Count); + + foreach (var kvp in expectedParts.Parameters) + { + string value; + Assert.True(actualParts.Parameters.TryGetValue(kvp.Key, out value)); + Assert.Equal(kvp.Value, value); + } + } + } + } + + private static void RunTest( + string template, + object defaults, + object ambientValues, + object values, + string expected) + { + RunTest( + template, + new RouteValueDictionary(defaults), + new RouteValueDictionary(ambientValues), + new RouteValueDictionary(values), + expected); + } + + [Theory] + [InlineData(null, null, true)] + [InlineData("blog", null, false)] + [InlineData(null, "store", false)] + [InlineData("Cool", "cool", true)] + [InlineData("Co0l", "cool", false)] + public void RoutePartsEqualTest(object left, object right, bool expected) + { + // Arrange & Act & Assert + if (expected) + { + Assert.True(TemplateBinder.RoutePartsEqual(left, right)); + } + else + { + Assert.False(TemplateBinder.RoutePartsEqual(left, right)); + } + } + + private static IInlineConstraintResolver GetInlineConstraintResolver() + { + var services = new ServiceCollection().AddOptions(); + var serviceProvider = services.BuildServiceProvider(); + var accessor = serviceProvider.GetRequiredService>(); + return new DefaultInlineConstraintResolver(accessor); + } + + private class PathAndQuery + { + public PathAndQuery(string uri) + { + var queryIndex = uri.IndexOf("?", StringComparison.Ordinal); + if (queryIndex == -1) + { + Path = uri; + } + else + { + Path = uri.Substring(0, queryIndex); + + var query = uri.Substring(queryIndex + 1); + Parameters = + query + .Split(new char[] { '&' }, StringSplitOptions.None) + .Select(s => s.Split(new char[] { '=' }, StringSplitOptions.None)) + .ToDictionary(pair => pair[0], pair => pair[1]); + } + } + + public string Path { get; private set; } + + public Dictionary Parameters { get; private set; } + } + } +} diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Template/TemplateMatcherTests.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Template/TemplateMatcherTests.cs new file mode 100644 index 0000000000..1e5eb7c39e --- /dev/null +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Template/TemplateMatcherTests.cs @@ -0,0 +1,1142 @@ +// 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.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.AspNetCore.Routing.Template.Tests +{ + public class TemplateMatcherTests + { + private static IInlineConstraintResolver _inlineConstraintResolver = GetInlineConstraintResolver(); + + [Fact] + public void TryMatch_Success() + { + // Arrange + var matcher = CreateMatcher("{controller}/{action}/{id}"); + + var values = new RouteValueDictionary(); + + // Act + var match = matcher.TryMatch("/Bank/DoAction/123", values); + + // Assert + Assert.True(match); + Assert.Equal("Bank", values["controller"]); + Assert.Equal("DoAction", values["action"]); + Assert.Equal("123", values["id"]); + } + + [Fact] + public void TryMatch_Fails() + { + // Arrange + var matcher = CreateMatcher("{controller}/{action}/{id}"); + + var values = new RouteValueDictionary(); + + // Act + var match = matcher.TryMatch("/Bank/DoAction", values); + + // Assert + Assert.False(match); + } + + [Fact] + public void TryMatch_WithDefaults_Success() + { + // Arrange + var matcher = CreateMatcher("{controller}/{action}/{id}", new { id = "default id" }); + + var values = new RouteValueDictionary(); + + // Act + var match = matcher.TryMatch("/Bank/DoAction", values); + + // Assert + Assert.True(match); + Assert.Equal("Bank", values["controller"]); + Assert.Equal("DoAction", values["action"]); + Assert.Equal("default id", values["id"]); + } + + [Fact] + public void TryMatch_WithDefaults_Fails() + { + // Arrange + var matcher = CreateMatcher("{controller}/{action}/{id}", new { id = "default id" }); + + var values = new RouteValueDictionary(); + + // Act + var match = matcher.TryMatch("/Bank", values); + + // Assert + Assert.False(match); + } + + [Fact] + public void TryMatch_WithLiterals_Success() + { + // Arrange + var matcher = CreateMatcher("moo/{p1}/bar/{p2}", new { p2 = "default p2" }); + + var values = new RouteValueDictionary(); + + // Act + var match = matcher.TryMatch("/moo/111/bar/222", values); + + // Assert + Assert.True(match); + Assert.Equal("111", values["p1"]); + Assert.Equal("222", values["p2"]); + } + + [Fact] + public void TryMatch_RouteWithLiteralsAndDefaults_Success() + { + // Arrange + var matcher = CreateMatcher("moo/{p1}/bar/{p2}", new { p2 = "default p2" }); + + var values = new RouteValueDictionary(); + + // Act + var match = matcher.TryMatch("/moo/111/bar/", values); + + // Assert + Assert.True(match); + Assert.Equal("111", values["p1"]); + Assert.Equal("default p2", values["p2"]); + } + + [Theory] + [InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)}", "/123-456-7890")] // ssn + [InlineData(@"{p1:regex(^\w+\@\w+\.\w+)}", "/asd@assds.com")] // email + [InlineData(@"{p1:regex(([}}])\w+)}", "/}sda")] // Not balanced } + [InlineData(@"{p1:regex(([{{)])\w+)}", "/})sda")] // Not balanced { + public void TryMatch_RegularExpressionConstraint_Valid( + string template, + string path) + { + // Arrange + var matcher = CreateMatcher(template); + + var values = new RouteValueDictionary(); + + // Act + var match = matcher.TryMatch(path, values); + + // Assert + Assert.True(match); + } + + [Theory] + [InlineData("moo/{p1}.{p2?}", "/moo/foo.bar", true, "foo", "bar")] + [InlineData("moo/{p1?}", "/moo/foo", true, "foo", null)] + [InlineData("moo/{p1?}", "/moo", true, null, null)] + [InlineData("moo/{p1}.{p2?}", "/moo/foo", true, "foo", null)] + [InlineData("moo/{p1}.{p2?}", "/moo/foo..bar", true, "foo.", "bar")] + [InlineData("moo/{p1}.{p2?}", "/moo/foo.moo.bar", true, "foo.moo", "bar")] + [InlineData("moo/{p1}.{p2}", "/moo/foo.bar", true, "foo", "bar")] + [InlineData("moo/foo.{p1}.{p2?}", "/moo/foo.moo.bar", true, "moo", "bar")] + [InlineData("moo/foo.{p1}.{p2?}", "/moo/foo.moo", true, "moo", null)] + [InlineData("moo/.{p2?}", "/moo/.foo", true, null, "foo")] + [InlineData("moo/.{p2?}", "/moo", false, null, null)] + [InlineData("moo/{p1}.{p2?}", "/moo/....", true, "..", ".")] + [InlineData("moo/{p1}.{p2?}", "/moo/.bar", true, ".bar", null)] + public void TryMatch_OptionalParameter_FollowedByPeriod_Valid( + string template, + string path, + bool expectedMatch, + string p1, + string p2) + { + // Arrange + var matcher = CreateMatcher(template); + + var values = new RouteValueDictionary(); + + // Act + var match = matcher.TryMatch(path, values); + + // Assert + Assert.Equal(expectedMatch, match); + if (p1 != null) + { + Assert.Equal(p1, values["p1"]); + } + if (p2 != null) + { + Assert.Equal(p2, values["p2"]); + } + } + + [Theory] + [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo.moo.bar", "foo", "moo", "bar")] + [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo.moo", "foo", "moo", null)] + [InlineData("moo/{p1}.{p2}.{p3}.{p4?}", "/moo/foo.moo.bar", "foo", "moo", "bar")] + [InlineData("{p1}.{p2?}/{p3}", "/foo.moo/bar", "foo", "moo", "bar")] + [InlineData("{p1}.{p2?}/{p3}", "/foo/bar", "foo", null, "bar")] + [InlineData("{p1}.{p2?}/{p3}", "/.foo/bar", ".foo", null, "bar")] + [InlineData("{p1}/{p2}/{p3?}", "/foo/bar/baz", "foo", "bar", "baz")] + public void TryMatch_OptionalParameter_FollowedByPeriod_3Parameters_Valid( + string template, + string path, + string p1, + string p2, + string p3) + { + // Arrange + var matcher = CreateMatcher(template); + + var values = new RouteValueDictionary(); + + // Act + var match = matcher.TryMatch(path, values); + + // Assert + Assert.True(match); + Assert.Equal(p1, values["p1"]); + + if (p2 != null) + { + Assert.Equal(p2, values["p2"]); + } + + if (p3 != null) + { + Assert.Equal(p3, values["p3"]); + } + } + + [Theory] + [InlineData("moo/{p1}.{p2?}", "/moo/foo.")] + [InlineData("moo/{p1}.{p2?}", "/moo/.")] + [InlineData("moo/{p1}.{p2}", "/foo.")] + [InlineData("moo/{p1}.{p2}", "/foo")] + [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo.moo.")] + [InlineData("moo/foo.{p2}.{p3?}", "/moo/bar.foo.moo")] + [InlineData("moo/foo.{p2}.{p3?}", "/moo/kungfoo.moo.bar")] + [InlineData("moo/foo.{p2}.{p3?}", "/moo/kungfoo.moo")] + [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo")] + [InlineData("{p1}.{p2?}/{p3}", "/foo./bar")] + [InlineData("moo/.{p2?}", "/moo/.")] + [InlineData("{p1}.{p2}/{p3}", "/.foo/bar")] + public void TryMatch_OptionalParameter_FollowedByPeriod_Invalid(string template, string path) + { + // Arrange + var matcher = CreateMatcher(template); + + var values = new RouteValueDictionary(); + + // Act + var match = matcher.TryMatch(path, values); + + // Assert + Assert.False(match); + } + + [Fact] + public void TryMatch_RouteWithOnlyLiterals_Success() + { + // Arrange + var matcher = CreateMatcher("moo/bar"); + + var values = new RouteValueDictionary(); + + // Act + var match = matcher.TryMatch("/moo/bar", values); + + // Assert + Assert.True(match); + Assert.Empty(values); + } + + [Fact] + public void TryMatch_RouteWithOnlyLiterals_Fails() + { + // Arrange + var matcher = CreateMatcher("moo/bars"); + + var values = new RouteValueDictionary(); + + // Act + var match = matcher.TryMatch("/moo/bar", values); + + // Assert + Assert.False(match); + } + + [Fact] + public void TryMatch_RouteWithExtraSeparators_Success() + { + // Arrange + var matcher = CreateMatcher("moo/bar"); + + var values = new RouteValueDictionary(); + + // Act + var match = matcher.TryMatch("/moo/bar/", values); + + // Assert + Assert.True(match); + Assert.Empty(values); + } + + [Fact] + public void TryMatch_UrlWithExtraSeparators_Success() + { + // Arrange + var matcher = CreateMatcher("moo/bar/"); + + var values = new RouteValueDictionary(); + + // Act + var match = matcher.TryMatch("/moo/bar", values); + + // Assert + Assert.True(match); + Assert.Empty(values); + } + + [Fact] + public void TryMatch_RouteWithParametersAndExtraSeparators_Success() + { + // Arrange + var matcher = CreateMatcher("{p1}/{p2}/"); + + var values = new RouteValueDictionary(); + + // Act + var match = matcher.TryMatch("/moo/bar", values); + + // Assert + Assert.True(match); + Assert.Equal("moo", values["p1"]); + Assert.Equal("bar", values["p2"]); + } + + [Fact] + public void TryMatch_RouteWithDifferentLiterals_Fails() + { + // Arrange + var matcher = CreateMatcher("{p1}/{p2}/baz"); + + var values = new RouteValueDictionary(); + + // Act + var match = matcher.TryMatch("/moo/bar/boo", values); + + // Assert + Assert.False(match); + } + + [Fact] + public void TryMatch_LongerUrl_Fails() + { + // Arrange + var matcher = CreateMatcher("{p1}"); + + var values = new RouteValueDictionary(); + + // Act + var match = matcher.TryMatch("/moo/bar", values); + + // Assert + Assert.False(match); + } + + [Fact] + public void TryMatch_SimpleFilename_Success() + { + // Arrange + var matcher = CreateMatcher("DEFAULT.ASPX"); + + var values = new RouteValueDictionary(); + + // Act + var match = matcher.TryMatch("/default.aspx", values); + + // Assert + Assert.True(match); + } + + [Theory] + [InlineData("{prefix}x{suffix}", "/xxxxxxxxxx")] + [InlineData("{prefix}xyz{suffix}", "/xxxxyzxyzxxxxxxyz")] + [InlineData("{prefix}xyz{suffix}", "/abcxxxxyzxyzxxxxxxyzxx")] + [InlineData("{prefix}xyz{suffix}", "/xyzxyzxyzxyzxyz")] + [InlineData("{prefix}xyz{suffix}", "/xyzxyzxyzxyzxyz1")] + [InlineData("{prefix}xyz{suffix}", "/xyzxyzxyz")] + [InlineData("{prefix}aa{suffix}", "/aaaaa")] + [InlineData("{prefix}aaa{suffix}", "/aaaaa")] + public void TryMatch_RouteWithComplexSegment_Success(string template, string path) + { + var matcher = CreateMatcher(template); + + var values = new RouteValueDictionary(); + + // Act + var match = matcher.TryMatch(path, values); + + // Assert + Assert.True(match); + } + + [Fact] + public void TryMatch_RouteWithExtraDefaultValues_Success() + { + // Arrange + var matcher = CreateMatcher("{p1}/{p2}", new { p2 = (string)null, foo = "bar" }); + + var values = new RouteValueDictionary(); + + // Act + var match = matcher.TryMatch("/v1", values); + + // Assert + Assert.True(match); + Assert.Equal(3, values.Count); + Assert.Equal("v1", values["p1"]); + Assert.Null(values["p2"]); + Assert.Equal("bar", values["foo"]); + } + + [Fact] + public void TryMatch_PrettyRouteWithExtraDefaultValues_Success() + { + // Arrange + var matcher = CreateMatcher( + "date/{y}/{m}/{d}", + new { controller = "blog", action = "showpost", m = (string)null, d = (string)null }); + + var values = new RouteValueDictionary(); + + // Act + var match = matcher.TryMatch("/date/2007/08", values); + + // Assert + Assert.True(match); + Assert.Equal(5, values.Count); + Assert.Equal("blog", values["controller"]); + Assert.Equal("showpost", values["action"]); + Assert.Equal("2007", values["y"]); + Assert.Equal("08", values["m"]); + Assert.Null(values["d"]); + } + + [Fact] + public void TryMatch_WithMultiSegmentParamsOnBothEndsMatches() + { + RunTest( + "language/{lang}-{region}", + "/language/en-US", + null, + new RouteValueDictionary(new { lang = "en", region = "US" })); + } + + [Fact] + public void TryMatch_WithMultiSegmentParamsOnLeftEndMatches() + { + RunTest( + "language/{lang}-{region}a", + "/language/en-USa", + null, + new RouteValueDictionary(new { lang = "en", region = "US" })); + } + + [Fact] + public void TryMatch_WithMultiSegmentParamsOnRightEndMatches() + { + RunTest( + "language/a{lang}-{region}", + "/language/aen-US", + null, + new RouteValueDictionary(new { lang = "en", region = "US" })); + } + + [Fact] + public void TryMatch_WithMultiSegmentParamsOnNeitherEndMatches() + { + RunTest( + "language/a{lang}-{region}a", + "/language/aen-USa", + null, + new RouteValueDictionary(new { lang = "en", region = "US" })); + } + + [Fact] + public void TryMatch_WithMultiSegmentParamsOnNeitherEndDoesNotMatch() + { + RunTest( + "language/a{lang}-{region}a", + "/language/a-USa", + null, + null); + } + + [Fact] + public void TryMatch_WithMultiSegmentParamsOnNeitherEndDoesNotMatch2() + { + RunTest( + "language/a{lang}-{region}a", + "/language/aen-a", + null, + null); + } + + [Fact] + public void TryMatch_WithSimpleMultiSegmentParamsOnBothEndsMatches() + { + RunTest( + "language/{lang}", + "/language/en", + null, + new RouteValueDictionary(new { lang = "en" })); + } + + [Fact] + public void TryMatch_WithSimpleMultiSegmentParamsOnBothEndsTrailingSlashDoesNotMatch() + { + RunTest( + "language/{lang}", + "/language/", + null, + null); + } + + [Fact] + public void TryMatch_WithSimpleMultiSegmentParamsOnBothEndsDoesNotMatch() + { + RunTest( + "language/{lang}", + "/language", + null, + null); + } + + [Fact] + public void TryMatch_WithSimpleMultiSegmentParamsOnLeftEndMatches() + { + RunTest( + "language/{lang}-", + "/language/en-", + null, + new RouteValueDictionary(new { lang = "en" })); + } + + [Fact] + public void TryMatch_WithSimpleMultiSegmentParamsOnRightEndMatches() + { + RunTest( + "language/a{lang}", + "/language/aen", + null, + new RouteValueDictionary(new { lang = "en" })); + } + + [Fact] + public void TryMatch_WithSimpleMultiSegmentParamsOnNeitherEndMatches() + { + RunTest( + "language/a{lang}a", + "/language/aena", + null, + new RouteValueDictionary(new { lang = "en" })); + } + + [Fact] + public void TryMatch_WithMultiSegmentStandamatchMvcRouteMatches() + { + RunTest( + "{controller}.mvc/{action}/{id}", + "/home.mvc/index", + new RouteValueDictionary(new { action = "Index", id = (string)null }), + new RouteValueDictionary(new { controller = "home", action = "index", id = (string)null })); + } + + [Fact] + public void TryMatch_WithMultiSegmentParamsOnBothEndsWithDefaultValuesMatches() + { + RunTest( + "language/{lang}-{region}", + "/language/-", + new RouteValueDictionary(new { lang = "xx", region = "yy" }), + null); + } + + [Fact] + public void TryMatch_WithUrlWithMultiSegmentWithRepeatedDots() + { + RunTest( + "{Controller}..mvc/{id}/{Param1}", + "/Home..mvc/123/p1", + null, + new RouteValueDictionary(new { Controller = "Home", id = "123", Param1 = "p1" })); + } + + [Fact] + public void TryMatch_WithUrlWithTwoRepeatedDots() + { + RunTest( + "{Controller}.mvc/../{action}", + "/Home.mvc/../index", + null, + new RouteValueDictionary(new { Controller = "Home", action = "index" })); + } + + [Fact] + public void TryMatch_WithUrlWithThreeRepeatedDots() + { + RunTest( + "{Controller}.mvc/.../{action}", + "/Home.mvc/.../index", + null, + new RouteValueDictionary(new { Controller = "Home", action = "index" })); + } + + [Fact] + public void TryMatch_WithUrlWithManyRepeatedDots() + { + RunTest( + "{Controller}.mvc/../../../{action}", + "/Home.mvc/../../../index", + null, + new RouteValueDictionary(new { Controller = "Home", action = "index" })); + } + + [Fact] + public void TryMatch_WithUrlWithExclamationPoint() + { + RunTest( + "{Controller}.mvc!/{action}", + "/Home.mvc!/index", + null, + new RouteValueDictionary(new { Controller = "Home", action = "index" })); + } + + [Fact] + public void TryMatch_WithUrlWithStartingDotDotSlash() + { + RunTest( + "../{Controller}.mvc", + "/../Home.mvc", + null, + new RouteValueDictionary(new { Controller = "Home" })); + } + + [Fact] + public void TryMatch_WithUrlWithStartingBackslash() + { + RunTest( + @"\{Controller}.mvc", + @"/\Home.mvc", + null, + new RouteValueDictionary(new { Controller = "Home" })); + } + + [Fact] + public void TryMatch_WithUrlWithBackslashSeparators() + { + RunTest( + @"{Controller}.mvc\{id}\{Param1}", + @"/Home.mvc\123\p1", + null, + new RouteValueDictionary(new { Controller = "Home", id = "123", Param1 = "p1" })); + } + + [Fact] + public void TryMatch_WithUrlWithParenthesesLiterals() + { + RunTest( + @"(Controller).mvc", + @"/(Controller).mvc", + null, + new RouteValueDictionary()); + } + + [Fact] + public void TryMatch_WithUrlWithTrailingSlashSpace() + { + RunTest( + @"Controller.mvc/ ", + @"/Controller.mvc/ ", + null, + new RouteValueDictionary()); + } + + [Fact] + public void TryMatch_WithUrlWithTrailingSpace() + { + RunTest( + @"Controller.mvc ", + @"/Controller.mvc ", + null, + new RouteValueDictionary()); + } + + [Fact] + public void TryMatch_WithCatchAllCapturesDots() + { + // DevDiv Bugs 189892: UrlRouting: Catch all parameter cannot capture url segments that contain the "." + RunTest( + "Home/ShowPilot/{missionId}/{*name}", + "/Home/ShowPilot/777/12345./foobar", + new RouteValueDictionary(new + { + controller = "Home", + action = "ShowPilot", + missionId = (string)null, + name = (string)null + }), + new RouteValueDictionary(new { controller = "Home", action = "ShowPilot", missionId = "777", name = "12345./foobar" })); + } + + [Fact] + public void TryMatch_RouteWithCatchAll_MatchesMultiplePathSegments() + { + // Arrange + var matcher = CreateMatcher("{p1}/{*p2}"); + + var values = new RouteValueDictionary(); + + // Act + var match = matcher.TryMatch("/v1/v2/v3", values); + + // Assert + Assert.True(match); + Assert.Equal(2, values.Count); + Assert.Equal("v1", values["p1"]); + Assert.Equal("v2/v3", values["p2"]); + } + + [Fact] + public void TryMatch_RouteWithCatchAll_MatchesTrailingSlash() + { + // Arrange + var matcher = CreateMatcher("{p1}/{*p2}"); + + var values = new RouteValueDictionary(); + + // Act + var match = matcher.TryMatch("/v1/", values); + + // Assert + Assert.True(match); + Assert.Equal(2, values.Count); + Assert.Equal("v1", values["p1"]); + Assert.Null(values["p2"]); + } + + [Fact] + public void TryMatch_RouteWithCatchAll_MatchesEmptyContent() + { + // Arrange + var matcher = CreateMatcher("{p1}/{*p2}"); + + var values = new RouteValueDictionary(); + + // Act + var match = matcher.TryMatch("/v1", values); + + // Assert + Assert.True(match); + Assert.Equal(2, values.Count); + Assert.Equal("v1", values["p1"]); + Assert.Null(values["p2"]); + } + + [Fact] + public void TryMatch_RouteWithCatchAll_MatchesEmptyContent_DoesNotReplaceExistingRouteValue() + { + // Arrange + var matcher = CreateMatcher("{p1}/{*p2}"); + + var values = new RouteValueDictionary(new { p2 = "hello" }); + + // Act + var match = matcher.TryMatch("/v1", values); + + // Assert + Assert.True(match); + Assert.Equal(2, values.Count); + Assert.Equal("v1", values["p1"]); + Assert.Equal("hello", values["p2"]); + } + + [Fact] + public void TryMatch_RouteWithCatchAll_UsesDefaultValueForEmptyContent() + { + // Arrange + var matcher = CreateMatcher("{p1}/{*p2}", new { p2 = "catchall" }); + + var values = new RouteValueDictionary(new { p2 = "overridden" }); + + // Act + var match = matcher.TryMatch("/v1", values); + + // Assert + Assert.True(match); + Assert.Equal(2, values.Count); + Assert.Equal("v1", values["p1"]); + Assert.Equal("catchall", values["p2"]); + } + + [Fact] + public void TryMatch_RouteWithCatchAll_IgnoresDefaultValueForNonEmptyContent() + { + // Arrange + var matcher = CreateMatcher("{p1}/{*p2}", new { p2 = "catchall" }); + + var values = new RouteValueDictionary(new { p2 = "overridden" }); + + // Act + var match = matcher.TryMatch("/v1/hello/whatever", values); + + // Assert + Assert.True(match); + Assert.Equal(2, values.Count); + Assert.Equal("v1", values["p1"]); + Assert.Equal("hello/whatever", values["p2"]); + } + + [Fact] + public void TryMatch_DoesNotMatchOnlyLeftLiteralMatch() + { + // DevDiv Bugs 191180: UrlRouting: Wrong template getting matched if a url segment is a substring of the requested url + RunTest( + "foo", + "/fooBAR", + null, + null); + } + + [Fact] + public void TryMatch_DoesNotMatchOnlyRightLiteralMatch() + { + // DevDiv Bugs 191180: UrlRouting: Wrong template getting matched if a url segment is a substring of the requested url + RunTest( + "foo", + "/BARfoo", + null, + null); + } + + [Fact] + public void TryMatch_DoesNotMatchMiddleLiteralMatch() + { + // DevDiv Bugs 191180: UrlRouting: Wrong template getting matched if a url segment is a substring of the requested url + RunTest( + "foo", + "/BARfooBAR", + null, + null); + } + + [Fact] + public void TryMatch_DoesMatchesExactLiteralMatch() + { + // DevDiv Bugs 191180: UrlRouting: Wrong template getting matched if a url segment is a substring of the requested url + RunTest( + "foo", + "/foo", + null, + new RouteValueDictionary()); + } + + [Fact] + public void TryMatch_WithWeimatchParameterNames() + { + RunTest( + "foo/{ }/{.!$%}/{dynamic.data}/{op.tional}", + "/foo/space/weimatch/omatcherid", + new RouteValueDictionary() { { " ", "not a space" }, { "op.tional", "default value" }, { "ran!dom", "va@lue" } }, + new RouteValueDictionary() { { " ", "space" }, { ".!$%", "weimatch" }, { "dynamic.data", "omatcherid" }, { "op.tional", "default value" }, { "ran!dom", "va@lue" } }); + } + + [Fact] + public void TryMatch_DoesNotMatchRouteWithLiteralSeparatomatchefaultsButNoValue() + { + RunTest( + "{controller}/{language}-{locale}", + "/foo", + new RouteValueDictionary(new { language = "en", locale = "US" }), + null); + } + + [Fact] + public void TryMatch_DoesNotMatchesRouteWithLiteralSeparatomatchefaultsAndLeftValue() + { + RunTest( + "{controller}/{language}-{locale}", + "/foo/xx-", + new RouteValueDictionary(new { language = "en", locale = "US" }), + null); + } + + [Fact] + public void TryMatch_DoesNotMatchesRouteWithLiteralSeparatomatchefaultsAndRightValue() + { + RunTest( + "{controller}/{language}-{locale}", + "/foo/-yy", + new RouteValueDictionary(new { language = "en", locale = "US" }), + null); + } + + [Fact] + public void TryMatch_MatchesRouteWithLiteralSeparatomatchefaultsAndValue() + { + RunTest( + "{controller}/{language}-{locale}", + "/foo/xx-yy", + new RouteValueDictionary(new { language = "en", locale = "US" }), + new RouteValueDictionary { { "language", "xx" }, { "locale", "yy" }, { "controller", "foo" } }); + } + + [Fact] + public void TryMatch_SetsOptionalParameter() + { + // Arrange + var route = CreateMatcher("{controller}/{action?}"); + var url = "/Home/Index"; + + var values = new RouteValueDictionary(); + + // Act + var match = route.TryMatch(url, values); + + // Assert + Assert.True(match); + Assert.Equal(2, values.Count); + Assert.Equal("Home", values["controller"]); + Assert.Equal("Index", values["action"]); + } + + [Fact] + public void TryMatch_DoesNotSetOptionalParameter() + { + // Arrange + var route = CreateMatcher("{controller}/{action?}"); + var url = "/Home"; + + var values = new RouteValueDictionary(); + + // Act + var match = route.TryMatch(url, values); + + // Assert + Assert.True(match); + Assert.Single(values); + Assert.Equal("Home", values["controller"]); + Assert.False(values.ContainsKey("action")); + } + + [Fact] + public void TryMatch_DoesNotSetOptionalParameter_EmptyString() + { + // Arrange + var route = CreateMatcher("{controller?}"); + var url = ""; + + var values = new RouteValueDictionary(); + + // Act + var match = route.TryMatch(url, values); + + // Assert + Assert.True(match); + Assert.Empty(values); + Assert.False(values.ContainsKey("controller")); + } + + [Fact] + public void TryMatch__EmptyRouteWith_EmptyString() + { + // Arrange + var route = CreateMatcher(""); + var url = ""; + + var values = new RouteValueDictionary(); + + // Act + var match = route.TryMatch(url, values); + + // Assert + Assert.True(match); + Assert.Empty(values); + } + + [Fact] + public void TryMatch_MultipleOptionalParameters() + { + // Arrange + var route = CreateMatcher("{controller}/{action?}/{id?}"); + var url = "/Home/Index"; + + var values = new RouteValueDictionary(); + + // Act + var match = route.TryMatch(url, values); + + // Assert + Assert.True(match); + Assert.Equal(2, values.Count); + Assert.Equal("Home", values["controller"]); + Assert.Equal("Index", values["action"]); + Assert.False(values.ContainsKey("id")); + } + + [Theory] + [InlineData("///")] + [InlineData("/a//")] + [InlineData("/a/b//")] + [InlineData("//b//")] + [InlineData("///c")] + [InlineData("///c/")] + public void TryMatch_MultipleOptionalParameters_WithEmptyIntermediateSegmentsDoesNotMatch(string url) + { + // Arrange + var route = CreateMatcher("{controller?}/{action?}/{id?}"); + + var values = new RouteValueDictionary(); + + // Act + var match = route.TryMatch(url, values); + + // Assert + Assert.False(match); + } + + [Theory] + [InlineData("")] + [InlineData("/")] + [InlineData("/a")] + [InlineData("/a/")] + [InlineData("/a/b")] + [InlineData("/a/b/")] + [InlineData("/a/b/c")] + [InlineData("/a/b/c/")] + public void TryMatch_MultipleOptionalParameters_WithIncrementalOptionalValues(string url) + { + // Arrange + var route = CreateMatcher("{controller?}/{action?}/{id?}"); + + var values = new RouteValueDictionary(); + + // Act + var match = route.TryMatch(url, values); + + // Assert + Assert.True(match); + } + + [Theory] + [InlineData("///")] + [InlineData("////")] + [InlineData("/a//")] + [InlineData("/a///")] + [InlineData("//b/")] + [InlineData("//b//")] + [InlineData("///c")] + [InlineData("///c/")] + public void TryMatch_MultipleParameters_WithEmptyValues(string url) + { + // Arrange + var route = CreateMatcher("{controller}/{action}/{id}"); + + var values = new RouteValueDictionary(); + + // Act + var match = route.TryMatch(url, values); + + // Assert + Assert.False(match); + } + + [Theory] + [InlineData("/a/b/c//")] + [InlineData("/a/b/c/////")] + public void TryMatch_CatchAllParameters_WithEmptyValuesAtTheEnd(string url) + { + // Arrange + var route = CreateMatcher("{controller}/{action}/{*id}"); + + var values = new RouteValueDictionary(); + + // Act + var match = route.TryMatch(url, values); + + // Assert + Assert.True(match); + } + + [Theory] + [InlineData("/a/b//")] + [InlineData("/a/b///c")] + public void TryMatch_CatchAllParameters_WithEmptyValues(string url) + { + // Arrange + var route = CreateMatcher("{controller}/{action}/{*id}"); + + var values = new RouteValueDictionary(); + + // Act + var match = route.TryMatch(url, values); + + // Assert + Assert.False(match); + } + + private TemplateMatcher CreateMatcher(string template, object defaults = null) + { + return new TemplateMatcher( + TemplateParser.Parse(template), + new RouteValueDictionary(defaults)); + } + + private static void RunTest( + string template, + string path, + RouteValueDictionary defaults, + IDictionary expected) + { + // Arrange + var matcher = new TemplateMatcher( + TemplateParser.Parse(template), + defaults ?? new RouteValueDictionary()); + + var values = new RouteValueDictionary(); + + // Act + var match = matcher.TryMatch(new PathString(path), values); + + // Assert + if (expected == null) + { + Assert.False(match); + } + else + { + Assert.True(match); + Assert.Equal(expected.Count, values.Count); + foreach (string key in values.Keys) + { + Assert.Equal(expected[key], values[key]); + } + } + } + + private static IInlineConstraintResolver GetInlineConstraintResolver() + { + var services = new ServiceCollection().AddOptions(); + var serviceProvider = services.BuildServiceProvider(); + var accessor = serviceProvider.GetRequiredService>(); + return new DefaultInlineConstraintResolver(accessor); + } + } +} diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Template/TemplateParserTests.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Template/TemplateParserTests.cs new file mode 100644 index 0000000000..7f9f9b8b40 --- /dev/null +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Template/TemplateParserTests.cs @@ -0,0 +1,922 @@ +// 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.Testing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Routing.Template.Tests +{ + public class TemplateRouteParserTests + { + [Fact] + public void Parse_SingleLiteral() + { + // Arrange + var template = "cool"; + + var expected = new RouteTemplate(template, new List()); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool")); + + // Act + var actual = TemplateParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new TemplateEqualityComparer()); + } + + [Fact] + public void Parse_SingleParameter() + { + // Arrange + var template = "{p}"; + + var expected = new RouteTemplate(template, new List()); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[0].Parts.Add( + TemplatePart.CreateParameter("p", false, false, defaultValue: null, inlineConstraints: null)); + expected.Parameters.Add(expected.Segments[0].Parts[0]); + + // Act + var actual = TemplateParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new TemplateEqualityComparer()); + } + + [Fact] + public void Parse_OptionalParameter() + { + // Arrange + var template = "{p?}"; + + var expected = new RouteTemplate(template, new List()); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[0].Parts.Add( + TemplatePart.CreateParameter("p", false, true, defaultValue: null, inlineConstraints: null)); + expected.Parameters.Add(expected.Segments[0].Parts[0]); + + // Act + var actual = TemplateParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new TemplateEqualityComparer()); + } + + [Fact] + public void Parse_MultipleLiterals() + { + // Arrange + var template = "cool/awesome/super"; + + var expected = new RouteTemplate(template, new List()); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool")); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[1].Parts.Add(TemplatePart.CreateLiteral("awesome")); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[2].Parts.Add(TemplatePart.CreateLiteral("super")); + + // Act + var actual = TemplateParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new TemplateEqualityComparer()); + } + + [Fact] + public void Parse_MultipleParameters() + { + // Arrange + var template = "{p1}/{p2}/{*p3}"; + + var expected = new RouteTemplate(template, new List()); + + expected.Segments.Add(new TemplateSegment()); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", + false, + false, + defaultValue: null, + inlineConstraints: null)); + expected.Parameters.Add(expected.Segments[0].Parts[0]); + + expected.Segments.Add(new TemplateSegment()); + expected.Segments[1].Parts.Add(TemplatePart.CreateParameter("p2", + false, + false, + defaultValue: null, + inlineConstraints: null)); + expected.Parameters.Add(expected.Segments[1].Parts[0]); + + expected.Segments.Add(new TemplateSegment()); + expected.Segments[2].Parts.Add(TemplatePart.CreateParameter("p3", + true, + false, + defaultValue: null, + inlineConstraints: null)); + expected.Parameters.Add(expected.Segments[2].Parts[0]); + + // Act + var actual = TemplateParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new TemplateEqualityComparer()); + } + + [Fact] + public void Parse_ComplexSegment_LP() + { + // Arrange + var template = "cool-{p1}"; + + var expected = new RouteTemplate(template, new List()); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool-")); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", + false, + false, + defaultValue: null, + inlineConstraints: null)); + expected.Parameters.Add(expected.Segments[0].Parts[1]); + + // Act + var actual = TemplateParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new TemplateEqualityComparer()); + } + + [Fact] + public void Parse_ComplexSegment_PL() + { + // Arrange + var template = "{p1}-cool"; + + var expected = new RouteTemplate(template, new List()); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", + false, + false, + defaultValue: null, + inlineConstraints: null)); + expected.Parameters.Add(expected.Segments[0].Parts[0]); + expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool-")); + + // Act + var actual = TemplateParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new TemplateEqualityComparer()); + } + + [Fact] + public void Parse_ComplexSegment_PLP() + { + // Arrange + var template = "{p1}-cool-{p2}"; + + var expected = new RouteTemplate(template, new List()); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", + false, + false, + defaultValue: null, + inlineConstraints: null)); + expected.Parameters.Add(expected.Segments[0].Parts[0]); + expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool-")); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p2", + false, + false, + defaultValue: null, + inlineConstraints: null)); + expected.Parameters.Add(expected.Segments[0].Parts[2]); + + // Act + var actual = TemplateParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new TemplateEqualityComparer()); + } + + [Fact] + public void Parse_ComplexSegment_LPL() + { + // Arrange + var template = "cool-{p1}-awesome"; + + var expected = new RouteTemplate(template, new List()); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool-")); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", + false, + false, + defaultValue: null, + inlineConstraints: null)); + expected.Parameters.Add(expected.Segments[0].Parts[1]); + expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("-awesome")); + + // Act + var actual = TemplateParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new TemplateEqualityComparer()); + } + + [Fact] + public void Parse_ComplexSegment_OptionalParameterFollowingPeriod() + { + // Arrange + var template = "{p1}.{p2?}"; + + var expected = new RouteTemplate(template, new List()); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", + false, + false, + defaultValue: null, + inlineConstraints: null)); + expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral(".")); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p2", + false, + true, + defaultValue: null, + inlineConstraints: null)); + + expected.Parameters.Add(expected.Segments[0].Parts[0]); + expected.Parameters.Add(expected.Segments[0].Parts[2]); + + // Act + var actual = TemplateParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new TemplateEqualityComparer()); + } + + [Fact] + public void Parse_ComplexSegment_ParametersFollowingPeriod() + { + // Arrange + var template = "{p1}.{p2}"; + + var expected = new RouteTemplate(template, new List()); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", + false, + false, + defaultValue: null, + inlineConstraints: null)); + expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral(".")); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p2", + false, + false, + defaultValue: null, + inlineConstraints: null)); + + expected.Parameters.Add(expected.Segments[0].Parts[0]); + expected.Parameters.Add(expected.Segments[0].Parts[2]); + + // Act + var actual = TemplateParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new TemplateEqualityComparer()); + } + + [Fact] + public void Parse_ComplexSegment_OptionalParameterFollowingPeriod_ThreeParameters() + { + // Arrange + var template = "{p1}.{p2}.{p3?}"; + + var expected = new RouteTemplate(template, new List()); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", + false, + false, + defaultValue: null, + inlineConstraints: null)); + expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral(".")); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p2", + false, + false, + defaultValue: null, + inlineConstraints: null)); + + expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral(".")); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p3", + false, + true, + defaultValue: null, + inlineConstraints: null)); + + + expected.Parameters.Add(expected.Segments[0].Parts[0]); + expected.Parameters.Add(expected.Segments[0].Parts[2]); + expected.Parameters.Add(expected.Segments[0].Parts[4]); + + // Act + var actual = TemplateParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new TemplateEqualityComparer()); + } + + [Fact] + public void Parse_ComplexSegment_ThreeParametersSeperatedByPeriod() + { + // Arrange + var template = "{p1}.{p2}.{p3}"; + + var expected = new RouteTemplate(template, new List()); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", + false, + false, + defaultValue: null, + inlineConstraints: null)); + expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral(".")); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p2", + false, + false, + defaultValue: null, + inlineConstraints: null)); + + expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral(".")); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p3", + false, + false, + defaultValue: null, + inlineConstraints: null)); + + + expected.Parameters.Add(expected.Segments[0].Parts[0]); + expected.Parameters.Add(expected.Segments[0].Parts[2]); + expected.Parameters.Add(expected.Segments[0].Parts[4]); + + // Act + var actual = TemplateParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new TemplateEqualityComparer()); + } + + [Fact] + public void Parse_ComplexSegment_OptionalParameterFollowingPeriod_MiddleSegment() + { + // Arrange + var template = "{p1}.{p2?}/{p3}"; + + var expected = new RouteTemplate(template, new List()); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", + false, + false, + defaultValue: null, + inlineConstraints: null)); + expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral(".")); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p2", + false, + true, + defaultValue: null, + inlineConstraints: null)); + + expected.Parameters.Add(expected.Segments[0].Parts[0]); + expected.Parameters.Add(expected.Segments[0].Parts[2]); + + expected.Segments.Add(new TemplateSegment()); + expected.Segments[1].Parts.Add(TemplatePart.CreateParameter("p3", + false, + false, + null, + null)); + expected.Parameters.Add(expected.Segments[1].Parts[0]); + // Act + var actual = TemplateParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new TemplateEqualityComparer()); + } + + [Fact] + public void Parse_ComplexSegment_OptionalParameterFollowingPeriod_LastSegment() + { + // Arrange + var template = "{p1}/{p2}.{p3?}"; + + var expected = new RouteTemplate(template, new List()); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", + false, + false, + defaultValue: null, + inlineConstraints: null)); + + expected.Segments.Add(new TemplateSegment()); + expected.Segments[1].Parts.Add(TemplatePart.CreateParameter("p2", + false, + false, + defaultValue: null, + inlineConstraints: null)); + expected.Segments[1].Parts.Add(TemplatePart.CreateLiteral(".")); + expected.Segments[1].Parts.Add(TemplatePart.CreateParameter("p3", + false, + true, + null, + null)); + expected.Parameters.Add(expected.Segments[0].Parts[0]); + expected.Parameters.Add(expected.Segments[1].Parts[0]); + expected.Parameters.Add(expected.Segments[1].Parts[2]); + + // Act + var actual = TemplateParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new TemplateEqualityComparer()); + } + + [Fact] + public void Parse_ComplexSegment_OptionalParameterFollowingPeriod_PeriodAfterSlash() + { + // Arrange + var template = "{p2}/.{p3?}"; + + var expected = new RouteTemplate(template, new List()); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p2", + false, + false, + defaultValue: null, + inlineConstraints: null)); + + expected.Segments.Add(new TemplateSegment()); + expected.Segments[1].Parts.Add(TemplatePart.CreateLiteral(".")); + expected.Segments[1].Parts.Add(TemplatePart.CreateParameter("p3", + false, + true, + null, + null)); + expected.Parameters.Add(expected.Segments[0].Parts[0]); + expected.Parameters.Add(expected.Segments[1].Parts[1]); + + // Act + var actual = TemplateParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new TemplateEqualityComparer()); + } + + [Theory] + [InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)}", @"regex(^\d{3}-\d{3}-\d{4}$)")] // ssn + [InlineData(@"{p1:regex(^\d{{1,2}}\/\d{{1,2}}\/\d{{4}}$)}", @"regex(^\d{1,2}\/\d{1,2}\/\d{4}$)")] // date + [InlineData(@"{p1:regex(^\w+\@\w+\.\w+)}", @"regex(^\w+\@\w+\.\w+)")] // email + [InlineData(@"{p1:regex(([}}])\w+)}", @"regex(([}])\w+)")] // Not balanced } + [InlineData(@"{p1:regex(([{{(])\w+)}", @"regex(([{(])\w+)")] // Not balanced { + public void Parse_RegularExpressions(string template, string constraint) + { + // Arrange + var expected = new RouteTemplate(template, new List()); + expected.Segments.Add(new TemplateSegment()); + var c = new InlineConstraint(constraint); + expected.Segments[0].Parts.Add( + TemplatePart.CreateParameter("p1", + false, + false, + defaultValue: null, + inlineConstraints: new List { c })); + expected.Parameters.Add(expected.Segments[0].Parts[0]); + + // Act + var actual = TemplateParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new TemplateEqualityComparer()); + } + + [Theory] + [InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}}$)}")] // extra } + [InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)}}")] // extra } at the end + [InlineData(@"{{p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)}")] // extra { at the begining + [InlineData(@"{p1:regex(([}])\w+}")] // Not escaped } + [InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{{4}$)}")] // Not escaped } + [InlineData(@"{p1:regex(abc)")] + public void Parse_RegularExpressions_Invalid(string template) + { + // Act and Assert + ExceptionAssert.Throws( + () => TemplateParser.Parse(template), + "There is an incomplete parameter in the route template. Check that each '{' character has a matching " + + "'}' character." + Environment.NewLine + "Parameter name: routeTemplate"); + } + + [Theory] + [InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{{{4}}$)}")] // extra { + [InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{4}}$)}")] // Not escaped { + public void Parse_RegularExpressions_Unescaped(string template) + { + // Act and Assert + ExceptionAssert.Throws( + () => TemplateParser.Parse(template), + "In a route parameter, '{' and '}' must be escaped with '{{' and '}}'." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Theory] + [InlineData("{p1}.{p2?}.{p3}", "p2", ".")] + [InlineData("{p1?}{p2}", "p1", "p2")] + [InlineData("{p1?}{p2?}", "p1", "p2")] + [InlineData("{p1}.{p2?})", "p2", ")")] + [InlineData("{foorb?}-bar-{z}", "foorb", "-bar-")] + public void Parse_ComplexSegment_OptionalParameter_NotTheLastPart( + string template, + string parameter, + string invalid) + { + // Act and Assert + ExceptionAssert.Throws( + () => TemplateParser.Parse(template), + "An optional parameter must be at the end of the segment. In the segment '" + template + + "', optional parameter '" + parameter + "' is followed by '" + invalid + "'." + + Environment.NewLine + "Parameter name: routeTemplate"); + } + + [Theory] + [InlineData("{p1}-{p2?}", "-")] + [InlineData("{p1}..{p2?}", "..")] + [InlineData("..{p2?}", "..")] + [InlineData("{p1}.abc.{p2?}", ".abc.")] + [InlineData("{p1}{p2?}", "{p1}")] + public void Parse_ComplexSegment_OptionalParametersSeperatedByPeriod_Invalid(string template, string parameter) + { + // Act and Assert + ExceptionAssert.Throws( + () => TemplateParser.Parse(template), + "In the segment '"+ template +"', the optional parameter 'p2' is preceded by an invalid " + + "segment '" + parameter +"'. Only a period (.) can precede an optional parameter." + + Environment.NewLine + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_WithRepeatedParameter() + { + var ex = ExceptionAssert.Throws( + () => TemplateParser.Parse("{Controller}.mvc/{id}/{controller}"), + "The route parameter name 'controller' appears more than one time in the route template." + + Environment.NewLine + "Parameter name: routeTemplate"); + } + + [Theory] + [InlineData("123{a}abc{")] + [InlineData("123{a}abc}")] + [InlineData("xyz}123{a}abc}")] + [InlineData("{{p1}")] + [InlineData("{p1}}")] + [InlineData("p1}}p2{")] + public void InvalidTemplate_WithMismatchedBraces(string template) + { + ExceptionAssert.Throws( + () => TemplateParser.Parse(template), + @"There is an incomplete parameter in the route template. Check that each '{' character has a " + + "matching '}' character." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_CannotHaveCatchAllInMultiSegment() + { + ExceptionAssert.Throws( + () => TemplateParser.Parse("123{a}abc{*moo}"), + "A path segment that contains more than one section, such as a literal section or a parameter, " + + "cannot contain a catch-all parameter." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_CannotHaveMoreThanOneCatchAll() + { + ExceptionAssert.Throws( + () => TemplateParser.Parse("{*p1}/{*p2}"), + "A catch-all parameter can only appear as the last segment of the route template." + + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_CannotHaveMoreThanOneCatchAllInMultiSegment() + { + ExceptionAssert.Throws( + () => TemplateParser.Parse("{*p1}abc{*p2}"), + "A path segment that contains more than one section, such as a literal section or a parameter, " + + "cannot contain a catch-all parameter." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_CannotHaveCatchAllWithNoName() + { + ExceptionAssert.Throws( + () => TemplateParser.Parse("foo/{*}"), + "The route parameter name '' is invalid. Route parameter names must be non-empty and cannot" + + " contain these characters: '{', '}', '/'. The '?' character marks a parameter as optional," + + " and can occur only at the end of the parameter. The '*' character marks a parameter as catch-all," + + " and can occur only at the start of the parameter." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Theory] + [InlineData("{**}", "*")] + [InlineData("{a*}", "a*")] + [InlineData("{*a*}", "a*")] + [InlineData("{*a*:int}", "a*")] + [InlineData("{*a*=5}", "a*")] + [InlineData("{*a*b=5}", "a*b")] + [InlineData("{p1?}.{p2/}/{p3}", "p2/")] + [InlineData("{p{{}", "p{")] + [InlineData("{p}}}", "p}")] + [InlineData("{p/}", "p/")] + public void ParseRouteParameter_ThrowsIf_ParameterContainsSpecialCharacters( + string template, + string parameterName) + { + // Arrange + var expectedMessage = "The route parameter name '" + parameterName + "' is invalid. Route parameter " + + "names must be non-empty and cannot contain these characters: '{', '}', '/'. The '?' character " + + "marks a parameter as optional, and can occur only at the end of the parameter. The '*' character " + + "marks a parameter as catch-all, and can occur only at the start of the parameter."; + + // Act & Assert + ExceptionAssert.Throws( + () => TemplateParser.Parse(template), expectedMessage + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Theory] + [InlineData("/foo")] + [InlineData("~/foo")] + public void ValidTemplate_CanStartWithSlashOrTildeSlash(string routeTemplate) + { + // Arrange & Act + var template = TemplateParser.Parse(routeTemplate); + + // Assert + Assert.Equal(routeTemplate, template.TemplateText); + } + + [Fact] + public void InvalidTemplate_CannotHaveConsecutiveOpenBrace() + { + ExceptionAssert.Throws( + () => TemplateParser.Parse("foo/{{p1}"), + "There is an incomplete parameter in the route template. Check that each '{' character has a " + + "matching '}' character." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_CannotHaveConsecutiveCloseBrace() + { + ExceptionAssert.Throws( + () => TemplateParser.Parse("foo/{p1}}"), + "There is an incomplete parameter in the route template. Check that each '{' character has a " + + "matching '}' character." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_SameParameterTwiceThrows() + { + ExceptionAssert.Throws( + () => TemplateParser.Parse("{aaa}/{AAA}"), + "The route parameter name 'AAA' appears more than one time in the route template." + + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_SameParameterTwiceAndOneCatchAllThrows() + { + ExceptionAssert.Throws( + () => TemplateParser.Parse("{aaa}/{*AAA}"), + "The route parameter name 'AAA' appears more than one time in the route template." + + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_InvalidParameterNameWithCloseBracketThrows() + { + ExceptionAssert.Throws( + () => TemplateParser.Parse("{a}/{aa}a}/{z}"), + "There is an incomplete parameter in the route template. Check that each '{' character has a " + + "matching '}' character." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_InvalidParameterNameWithOpenBracketThrows() + { + ExceptionAssert.Throws( + () => TemplateParser.Parse("{a}/{a{aa}/{z}"), + "In a route parameter, '{' and '}' must be escaped with '{{' and '}}'." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_InvalidParameterNameWithEmptyNameThrows() + { + ExceptionAssert.Throws( + () => TemplateParser.Parse("{a}/{}/{z}"), + "The route parameter name '' is invalid. Route parameter names must be non-empty and cannot" + + " contain these characters: '{', '}', '/'. The '?' character marks a parameter as optional, and" + + " can occur only at the end of the parameter. The '*' character marks a parameter as catch-all," + + " and can occur only at the start of the parameter." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_InvalidParameterNameWithQuestionThrows() + { + ExceptionAssert.Throws( + () => TemplateParser.Parse("{Controller}.mvc/{?}"), + "The route parameter name '' is invalid. Route parameter names must be non-empty and cannot" + + " contain these characters: '{', '}', '/'. The '?' character marks a parameter as optional, and" + + " can occur only at the end of the parameter. The '*' character marks a parameter as catch-all," + + " and can occur only at the start of the parameter." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_ConsecutiveSeparatorsSlashSlashThrows() + { + ExceptionAssert.Throws( + () => TemplateParser.Parse("{a}//{z}"), + "The route template separator character '/' cannot appear consecutively. It must be separated by " + + "either a parameter or a literal value." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_WithCatchAllNotAtTheEndThrows() + { + ExceptionAssert.Throws( + () => TemplateParser.Parse("foo/{p1}/{*p2}/{p3}"), + "A catch-all parameter can only appear as the last segment of the route template." + + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_RepeatedParametersThrows() + { + ExceptionAssert.Throws( + () => TemplateParser.Parse("foo/aa{p1}{p2}"), + "A path segment cannot contain two consecutive parameters. They must be separated by a '/' or by " + + "a literal string." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_CannotStartWithTilde() + { + ExceptionAssert.Throws( + () => TemplateParser.Parse("~foo"), + "The route template cannot start with a '~' character unless followed by a '/'." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_CannotContainQuestionMark() + { + ExceptionAssert.Throws( + () => TemplateParser.Parse("foor?bar"), + "The literal section 'foor?bar' is invalid. Literal sections cannot contain the '?' character." + + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_ParameterCannotContainQuestionMark_UnlessAtEnd() + { + ExceptionAssert.Throws( + () => TemplateParser.Parse("{foor?b}"), + "The route parameter name 'foor?b' is invalid. Route parameter names must be non-empty and cannot" + + " contain these characters: '{', '}', '/'. The '?' character marks a parameter as optional, and" + + " can occur only at the end of the parameter. The '*' character marks a parameter as catch-all," + + " and can occur only at the start of the parameter." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_CatchAllMarkedOptional() + { + ExceptionAssert.Throws( + () => TemplateParser.Parse("{a}/{*b?}"), + "A catch-all parameter cannot be marked optional." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + private class TemplateEqualityComparer : IEqualityComparer + { + public bool Equals(RouteTemplate x, RouteTemplate y) + { + if (x == null && y == null) + { + return true; + } + else if (x == null || y == null) + { + return false; + } + else + { + if (!string.Equals(x.TemplateText, y.TemplateText, StringComparison.Ordinal)) + { + return false; + } + + if (x.Segments.Count != y.Segments.Count) + { + return false; + } + + for (int i = 0; i < x.Segments.Count; i++) + { + if (x.Segments[i].Parts.Count != y.Segments[i].Parts.Count) + { + return false; + } + + for (int j = 0; j < x.Segments[i].Parts.Count; j++) + { + if (!Equals(x.Segments[i].Parts[j], y.Segments[i].Parts[j])) + { + return false; + } + } + } + + if (x.Parameters.Count != y.Parameters.Count) + { + return false; + } + + for (int i = 0; i < x.Parameters.Count; i++) + { + if (!Equals(x.Parameters[i], y.Parameters[i])) + { + return false; + } + } + + return true; + } + } + + private bool Equals(TemplatePart x, TemplatePart y) + { + if (x.IsLiteral != y.IsLiteral || + x.IsParameter != y.IsParameter || + x.IsCatchAll != y.IsCatchAll || + x.IsOptional != y.IsOptional || + !String.Equals(x.Name, y.Name, StringComparison.Ordinal) || + !String.Equals(x.Name, y.Name, StringComparison.Ordinal) || + (x.InlineConstraints == null && y.InlineConstraints != null) || + (x.InlineConstraints != null && y.InlineConstraints == null)) + { + return false; + } + + if (x.InlineConstraints == null && y.InlineConstraints == null) + { + return true; + } + + if (x.InlineConstraints.Count() != y.InlineConstraints.Count()) + { + return false; + } + + foreach (var xconstraint in x.InlineConstraints) + { + if (!y.InlineConstraints.Any( + c => string.Equals(c.Constraint, xconstraint.Constraint))) + { + return false; + } + } + + return true; + } + + public int GetHashCode(RouteTemplate obj) + { + throw new NotImplementedException(); + } + } + } +} diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/TemplateParserDefaultValuesTests.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/TemplateParserDefaultValuesTests.cs new file mode 100644 index 0000000000..96c126fd30 --- /dev/null +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/TemplateParserDefaultValuesTests.cs @@ -0,0 +1,149 @@ +// 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.Builder; +using Microsoft.AspNetCore.Routing.Internal; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Routing.Tests +{ + public class TemplateParserDefaultValuesTests + { + private static IInlineConstraintResolver _inlineConstraintResolver = GetInlineConstraintResolver(); + + [Fact] + public void InlineDefaultValueSpecified_InlineValueIsUsed() + { + // Arrange & Act + var routeBuilder = CreateRouteBuilder(); + + // Act + routeBuilder.MapRoute("mockName", + "{controller}/{action}/{id:int=12}", + defaults: null, + constraints: null); + + // Assert + var defaults = ((Route)routeBuilder.Routes[0]).Defaults; + Assert.Equal("12", defaults["id"]); + } + + [Theory] + [InlineData(@"{controller}/{action}/{p1:regex(([}}])\w+)=}}asd}", "}asd")] + [InlineData(@"{p1:regex(^\d{{1,2}}\/\d{{1,2}}\/\d{{4}}$)=12/12/1234}", @"12/12/1234")] + public void InlineDefaultValueSpecified_WithSpecialCharacters(string template, string value) + { + // Arrange & Act + var routeBuilder = CreateRouteBuilder(); + + // Act + routeBuilder.MapRoute("mockName", + template, + defaults: null, + constraints: null); + + // Assert + var defaults = ((Route)routeBuilder.Routes[0]).Defaults; + Assert.Equal(value, defaults["p1"]); + } + + [Fact] + public void ExplicitDefaultValueSpecified_WithInlineDefaultValue_Throws() + { + // Arrange + var routeBuilder = CreateRouteBuilder(); + + // Act & Assert + var ex = Assert.Throws( + () => routeBuilder.MapRoute("mockName", + "{controller}/{action}/{id:int=12}", + defaults: new { id = 13 }, + constraints: null)); + + var message = "An error occurred while creating the route with name 'mockName' and template" + + " '{controller}/{action}/{id:int=12}'."; + Assert.Equal(message, ex.Message); + + Assert.NotNull(ex.InnerException); + message = "The route parameter 'id' has both an inline default value and an explicit default" + + " value specified. A route parameter cannot contain an inline default value when" + + " a default value is specified explicitly. Consider removing one of them."; + Assert.Equal(message, ex.InnerException.Message); + } + + [Fact] + public void EmptyDefaultValue_WithOptionalParameter_Throws() + { + // Arrange + var routeBuilder = CreateRouteBuilder(); + + // Act & Assert + var ex = Assert.Throws( + () => routeBuilder.MapRoute("mockName", + "{controller}/{action}/{id:int=?}", + defaults: new { id = 13 }, + constraints: null)); + + var message = "An error occurred while creating the route with name 'mockName' and template" + + " '{controller}/{action}/{id:int=?}'."; + Assert.Equal(message, ex.Message); + + Assert.NotNull(ex.InnerException); + message = "An optional parameter cannot have default value." + Environment.NewLine + + "Parameter name: routeTemplate"; + Assert.Equal(message, ex.InnerException.Message); + } + + [Fact] + public void NonEmptyDefaultValue_WithOptionalParameter_Throws() + { + // Arrange + var routeBuilder = CreateRouteBuilder(); + + // Act & Assert + var ex = Assert.Throws(() => + { + routeBuilder.MapRoute( + "mockName", + "{controller}/{action}/{id:int=12?}", + defaults: new { id = 13 }, + constraints: null); + }); + + var message = "An error occurred while creating the route with name 'mockName' and template" + + " '{controller}/{action}/{id:int=12?}'."; + Assert.Equal(message, ex.Message); + + Assert.NotNull(ex.InnerException); + message = "An optional parameter cannot have default value." + Environment.NewLine + + "Parameter name: routeTemplate"; + Assert.Equal(message, ex.InnerException.Message); + } + + private static IRouteBuilder CreateRouteBuilder() + { + var services = new ServiceCollection(); + services.AddSingleton(_inlineConstraintResolver); + services.AddSingleton(); + + var applicationBuilder = Mock.Of(); + applicationBuilder.ApplicationServices = services.BuildServiceProvider(); + + var routeBuilder = new RouteBuilder(applicationBuilder); + routeBuilder.DefaultHandler = Mock.Of(); + return routeBuilder; + } + + private static IInlineConstraintResolver GetInlineConstraintResolver() + { + var services = new ServiceCollection().AddOptions(); + var serviceProvider = services.BuildServiceProvider(); + var accessor = serviceProvider.GetRequiredService>(); + return new DefaultInlineConstraintResolver(accessor); + } + } +} diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Tree/TreeRouteBuilderTest.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Tree/TreeRouteBuilderTest.cs new file mode 100644 index 0000000000..2645e78439 --- /dev/null +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Tree/TreeRouteBuilderTest.cs @@ -0,0 +1,266 @@ +// 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.Text.Encodings.Web; +using Microsoft.AspNetCore.Routing.Internal; +using Microsoft.AspNetCore.Routing.Template; +using Microsoft.AspNetCore.Testing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.ObjectPool; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Routing.Tree +{ + public class TreeRouteBuilderTest + { + [Fact] + public void TreeRouter_BuildThrows_RoutesWithTheSameNameAndDifferentTemplates() + { + // Arrange + var builder = CreateBuilder(); + + var message = "Two or more routes named 'Get_Products' have different templates."; + + builder.MapOutbound( + Mock.Of(), + TemplateParser.Parse("api/Products"), + new RouteValueDictionary(), + "Get_Products", + order: 0); + + builder.MapOutbound( + Mock.Of(), + TemplateParser.Parse("Products/Index"), + new RouteValueDictionary(), + "Get_Products", + order: 0); + + // Act & Assert + ExceptionAssert.ThrowsArgument(() => + { + builder.Build(); + }, "linkGenerationEntries", message); + } + + [Fact] + public void TreeRouter_BuildDoesNotThrow_RoutesWithTheSameNameAndSameTemplates() + { + // Arrange + var builder = CreateBuilder(); + + builder.MapOutbound( + Mock.Of(), + TemplateParser.Parse("api/Products"), + new RouteValueDictionary(), + "Get_Products", + order: 0); + + builder.MapOutbound( + Mock.Of(), + TemplateParser.Parse("api/products"), + new RouteValueDictionary(), + "Get_Products", + order: 0); + + // Act & Assert (does not throw) + builder.Build(); + } + + [Fact] + public void TreeRouter_BuildDoesNotAddIntermediateMatchingNodes_ForRoutesWithIntermediateParametersWithDefaultValues() + { + // Arrange + var builder = CreateBuilder(); + + builder.MapInbound( + Mock.Of(), + TemplateParser.Parse("a/{b=3}/c"), + "Intermediate", + order: 0); + + // Act + var tree = builder.Build(); + + // Assert + Assert.NotNull(tree); + Assert.NotNull(tree.MatchingTrees); + var matchingTree = Assert.Single(tree.MatchingTrees); + + var firstSegment = Assert.Single(matchingTree.Root.Literals); + Assert.Equal("a", firstSegment.Key); + Assert.NotNull(firstSegment.Value.Parameters); + + var secondSegment = firstSegment.Value.Parameters; + Assert.Empty(secondSegment.Matches); + + var thirdSegment = Assert.Single(secondSegment.Literals); + Assert.Equal("c", thirdSegment.Key); + Assert.Single(thirdSegment.Value.Matches); + } + + [Fact] + public void TreeRouter_BuildDoesNotAddIntermediateMatchingNodes_ForRoutesWithMultipleIntermediateParametersWithDefaultOrOptionalValues() + { + // Arrange + var builder = CreateBuilder(); + + builder.MapInbound( + Mock.Of(), + TemplateParser.Parse("a/{b=3}/c/{d?}/e/{*f}"), + "Intermediate", + order: 0); + + // Act + var tree = builder.Build(); + + // Assert + Assert.NotNull(tree); + Assert.NotNull(tree.MatchingTrees); + var matchingTree = Assert.Single(tree.MatchingTrees); + + var firstSegment = Assert.Single(matchingTree.Root.Literals); + Assert.Equal("a", firstSegment.Key); + Assert.NotNull(firstSegment.Value.Parameters); + + var secondSegment = firstSegment.Value.Parameters; + Assert.Empty(secondSegment.Matches); + + var thirdSegment = Assert.Single(secondSegment.Literals); + Assert.Equal("c", thirdSegment.Key); + Assert.Empty(thirdSegment.Value.Matches); + + var fourthSegment = thirdSegment.Value.Parameters; + Assert.NotNull(fourthSegment); + Assert.Empty(fourthSegment.Matches); + + var fifthSegment = Assert.Single(fourthSegment.Literals); + Assert.Equal("e", fifthSegment.Key); + Assert.Single(fifthSegment.Value.Matches); + + var sixthSegment = fifthSegment.Value.CatchAlls; + Assert.NotNull(sixthSegment); + Assert.Single(sixthSegment.Matches); + } + + [Fact] + public void TreeRouter_BuildDoesNotAddIntermediateMatchingNodes_ForRoutesWithIntermediateParametersWithOptionalValues() + { + // Arrange + var builder = CreateBuilder(); + + builder.MapInbound( + Mock.Of(), + TemplateParser.Parse("a/{b?}/c"), + "Intermediate", + order: 0); + + // Act + var tree = builder.Build(); + + // Assert + Assert.NotNull(tree); + Assert.NotNull(tree.MatchingTrees); + var matchingTree = Assert.Single(tree.MatchingTrees); + + var firstSegment = Assert.Single(matchingTree.Root.Literals); + Assert.Equal("a", firstSegment.Key); + Assert.NotNull(firstSegment.Value.Parameters); + + var secondSegment = firstSegment.Value.Parameters; + Assert.Empty(secondSegment.Matches); + + var thirdSegment = Assert.Single(secondSegment.Literals); + Assert.Equal("c", thirdSegment.Key); + Assert.Single(thirdSegment.Value.Matches); + } + + [Fact] + public void TreeRouter_BuildDoesNotAddIntermediateMatchingNodes_ForRoutesWithIntermediateParametersWithConstrainedDefaultValues() + { + // Arrange + var builder = CreateBuilder(); + + builder.MapInbound( + Mock.Of(), + TemplateParser.Parse("a/{b:int=3}/c"), + "Intermediate", + order: 0); + + // Act + var tree = builder.Build(); + + // Assert + Assert.NotNull(tree); + Assert.NotNull(tree.MatchingTrees); + var matchingTree = Assert.Single(tree.MatchingTrees); + + var firstSegment = Assert.Single(matchingTree.Root.Literals); + Assert.Equal("a", firstSegment.Key); + Assert.NotNull(firstSegment.Value.ConstrainedParameters); + + var secondSegment = firstSegment.Value.ConstrainedParameters; + Assert.Empty(secondSegment.Matches); + + var thirdSegment = Assert.Single(secondSegment.Literals); + Assert.Equal("c", thirdSegment.Key); + Assert.Single(thirdSegment.Value.Matches); + } + + [Fact] + public void TreeRouter_BuildDoesNotAddIntermediateMatchingNodes_ForRoutesWithIntermediateParametersWithConstrainedOptionalValues() + { + // Arrange + var builder = CreateBuilder(); + + builder.MapInbound( + Mock.Of(), + TemplateParser.Parse("a/{b:int?}/c"), + "Intermediate", + order: 0); + + // Act + var tree = builder.Build(); + + // Assert + Assert.NotNull(tree); + Assert.NotNull(tree.MatchingTrees); + var matchingTree = Assert.Single(tree.MatchingTrees); + + var firstSegment = Assert.Single(matchingTree.Root.Literals); + Assert.Equal("a", firstSegment.Key); + Assert.NotNull(firstSegment.Value.ConstrainedParameters); + + var secondSegment = firstSegment.Value.ConstrainedParameters; + Assert.Empty(secondSegment.Matches); + + var thirdSegment = Assert.Single(secondSegment.Literals); + Assert.Equal("c", thirdSegment.Key); + Assert.Single(thirdSegment.Value.Matches); + } + + private static TreeRouteBuilder CreateBuilder() + { + var objectPoolProvider = new DefaultObjectPoolProvider(); + var objectPolicy = new UriBuilderContextPooledObjectPolicy(); + var objectPool = objectPoolProvider.Create(objectPolicy); + + var constraintResolver = GetInlineConstraintResolver(); + var builder = new TreeRouteBuilder( + NullLoggerFactory.Instance, + objectPool, + constraintResolver); + return builder; + } + + private static IInlineConstraintResolver GetInlineConstraintResolver() + { + var services = new ServiceCollection().AddOptions(); + var serviceProvider = services.BuildServiceProvider(); + var accessor = serviceProvider.GetRequiredService>(); + return new DefaultInlineConstraintResolver(accessor); + } + } +} diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Tree/TreeRouterTest.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Tree/TreeRouterTest.cs new file mode 100644 index 0000000000..66d15c5314 --- /dev/null +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Tree/TreeRouterTest.cs @@ -0,0 +1,2209 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing.Internal; +using Microsoft.AspNetCore.Routing.Template; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.ObjectPool; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Routing.Tree +{ + public class TreeRouterTest + { + private static readonly RequestDelegate NullHandler = (c) => Task.FromResult(0); + + private static ObjectPool Pool = new DefaultObjectPoolProvider().Create( + new UriBuilderContextPooledObjectPolicy()); + + [Fact] + public async Task TreeRouter_RouteAsync_MatchesCatchAllRoutesWithDefaults_UsingObsoleteConstructo() + { + // Arrange + var routes = new[] { + "{parameter1=1}/{parameter2=2}/{parameter3=3}/{*parameter4=4}", + }; + var url = "/a/b/c"; + var routeValues = new[] { "a", "b", "c", "4" }; + + var expectedRouteGroup = CreateRouteGroup(0, "{parameter1=1}/{parameter2=2}/{parameter3=3}/{*parameter4=4}"); + var routeValueKeys = new[] { "parameter1", "parameter2", "parameter3", "parameter4" }; + var expectedRouteValues = new RouteValueDictionary(); + for (int i = 0; i < routeValueKeys.Length; i++) + { + expectedRouteValues.Add(routeValueKeys[i], routeValues[i]); + } + + var builder = CreateBuilderUsingObsoleteConstructor(); + + // We setup the route entries in reverse order of precedence to ensure that when we + // try to route the request, the route with a higher precedence gets tried first. + foreach (var template in routes.Reverse()) + { + MapInboundEntry(builder, template); + } + + var route = builder.Build(); + + var context = CreateRouteContext(url); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.Equal(expectedRouteGroup, context.RouteData.Values["test_route_group"]); + foreach (var entry in expectedRouteValues) + { + var data = Assert.Single(context.RouteData.Values, v => v.Key == entry.Key); + Assert.Equal(entry.Value, data.Value); + } + } + + [Fact] + public async Task TreeRouter_RouteAsync_DoesNotMatchRoutesWithIntermediateDefaultRouteValues_UsingObsoleteConstructor() + { + // Arrange + var url = "/a/b"; + + var builder = CreateBuilderUsingObsoleteConstructor(); + + MapInboundEntry(builder, "a/b/{parameter3=3}/d"); + + var route = builder.Build(); + + var context = CreateRouteContext(url); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.Null(context.Handler); + } + + [Theory] + [InlineData("template/5", "template/{parameter:int}")] + [InlineData("template/5", "template/{parameter}")] + [InlineData("template/5", "template/{*parameter:int}")] + [InlineData("template/5", "template/{*parameter}")] + [InlineData("template/{parameter}", "template/{parameter:alpha}")] // constraint doesn't match + [InlineData("template/{parameter:int}", "template/{parameter}")] + [InlineData("template/{parameter:int}", "template/{*parameter:int}")] + [InlineData("template/{parameter:int}", "template/{*parameter}")] + [InlineData("template/{parameter}", "template/{*parameter:int}")] + [InlineData("template/{parameter}", "template/{*parameter}")] + [InlineData("template/{*parameter:int}", "template/{*parameter}")] + public async Task TreeRouter_RouteAsync_RespectsPrecedence( + string firstTemplate, + string secondTemplate) + { + // Arrange + var expectedRouteGroup = CreateRouteGroup(0, firstTemplate); + + var builder = CreateBuilder(); + + // We setup the route entries in reverse order of precedence to ensure that when we + // try to route the request, the route with a higher precedence gets tried first. + MapInboundEntry(builder, secondTemplate); + MapInboundEntry(builder, firstTemplate); + + var route = builder.Build(); + + var context = CreateRouteContext("/template/5"); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.Equal(expectedRouteGroup, context.RouteData.Values["test_route_group"]); + } + + [Theory] + [InlineData("/", "")] + [InlineData("/Literal1", "Literal1")] + [InlineData("/Literal1/Literal2", "Literal1/Literal2")] + [InlineData("/Literal1/Literal2/Literal3", "Literal1/Literal2/Literal3")] + [InlineData("/Literal1/Literal2/Literal3/4", "Literal1/Literal2/Literal3/{*constrainedCatchAll:int}")] + [InlineData("/Literal1/Literal2/Literal3/Literal4", "Literal1/Literal2/Literal3/{*catchAll}")] + [InlineData("/1", "{constrained1:int}")] + [InlineData("/1/2", "{constrained1:int}/{constrained2:int}")] + [InlineData("/1/2/3", "{constrained1:int}/{constrained2:int}/{constrained3:int}")] + [InlineData("/1/2/3/4", "{constrained1:int}/{constrained2:int}/{constrained3:int}/{*constrainedCatchAll:int}")] + [InlineData("/1/2/3/CatchAll4", "{constrained1:int}/{constrained2:int}/{constrained3:int}/{*catchAll}")] + [InlineData("/parameter1", "{parameter1}")] + [InlineData("/parameter1/parameter2", "{parameter1}/{parameter2}")] + [InlineData("/parameter1/parameter2/parameter3", "{parameter1}/{parameter2}/{parameter3}")] + [InlineData("/parameter1/parameter2/parameter3/4", "{parameter1}/{parameter2}/{parameter3}/{*constrainedCatchAll:int}")] + [InlineData("/parameter1/parameter2/parameter3/CatchAll4", "{parameter1}/{parameter2}/{parameter3}/{*catchAll}")] + public async Task TreeRouter_RouteAsync_MatchesRouteWithTheRightLength(string url, string expected) + { + // Arrange + var routes = new[] { + "", + "Literal1", + "Literal1/Literal2", + "Literal1/Literal2/Literal3", + "Literal1/Literal2/Literal3/{*constrainedCatchAll:int}", + "Literal1/Literal2/Literal3/{*catchAll}", + "{constrained1:int}", + "{constrained1:int}/{constrained2:int}", + "{constrained1:int}/{constrained2:int}/{constrained3:int}", + "{constrained1:int}/{constrained2:int}/{constrained3:int}/{*constrainedCatchAll:int}", + "{constrained1:int}/{constrained2:int}/{constrained3:int}/{*catchAll}", + "{parameter1}", + "{parameter1}/{parameter2}", + "{parameter1}/{parameter2}/{parameter3}", + "{parameter1}/{parameter2}/{parameter3}/{*constrainedCatchAll:int}", + "{parameter1}/{parameter2}/{parameter3}/{*catchAll}", + }; + + var expectedRouteGroup = CreateRouteGroup(0, expected); + + var builder = CreateBuilder(); + + // We setup the route entries in reverse order of precedence to ensure that when we + // try to route the request, the route with a higher precedence gets tried first. + foreach (var template in routes.Reverse()) + { + MapInboundEntry(builder, template); + } + + var route = builder.Build(); + + var context = CreateRouteContext(url); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.Equal(expectedRouteGroup, context.RouteData.Values["test_route_group"]); + } + + public static TheoryData MatchesRoutesWithDefaultsData => + new TheoryData + { + { "/", new object[] { "1", "2", "3", "4" } }, + { "/a", new object[] { "a", "2", "3", "4" } }, + { "/a/b", new object[] { "a", "b", "3", "4" } }, + { "/a/b/c", new object[] { "a", "b", "c", "4" } }, + { "/a/b/c/d", new object[] { "a", "b", "c", "d" } } + }; + + [Theory] + [MemberData(nameof(MatchesRoutesWithDefaultsData))] + public async Task TreeRouter_RouteAsync_MatchesRoutesWithDefaults(string url, object[] routeValues) + { + // Arrange + var routes = new[] { + "{parameter1=1}/{parameter2=2}/{parameter3=3}/{parameter4=4}", + }; + + var expectedRouteGroup = CreateRouteGroup(0, "{parameter1=1}/{parameter2=2}/{parameter3=3}/{parameter4=4}"); + var routeValueKeys = new[] { "parameter1", "parameter2", "parameter3", "parameter4" }; + var expectedRouteValues = new RouteValueDictionary(); + for (int i = 0; i < routeValueKeys.Length; i++) + { + expectedRouteValues.Add(routeValueKeys[i], routeValues[i]); + } + + var builder = CreateBuilder(); + + // We setup the route entries in reverse order of precedence to ensure that when we + // try to route the request, the route with a higher precedence gets tried first. + foreach (var template in routes.Reverse()) + { + MapInboundEntry(builder, template); + } + + var route = builder.Build(); + + var context = CreateRouteContext(url); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.Equal(expectedRouteGroup, context.RouteData.Values["test_route_group"]); + foreach (var entry in expectedRouteValues) + { + var data = Assert.Single(context.RouteData.Values, v => v.Key == entry.Key); + Assert.Equal(entry.Value, data.Value); + } + } + + public static TheoryData MatchesConstrainedRoutesWithDefaultsData => + new TheoryData + { + { "/", new object[] { "1", "2", "3", "4" } }, + { "/10", new object[] { "10", "2", "3", "4" } }, + { "/10/11", new object[] { "10", "11", "3", "4" } }, + { "/10/11/12", new object[] { "10", "11", "12", "4" } }, + { "/10/11/12/13", new object[] { "10", "11", "12", "13" } } + }; + + [Theory] + [MemberData(nameof(MatchesConstrainedRoutesWithDefaultsData))] + public async Task TreeRouter_RouteAsync_MatchesConstrainedRoutesWithDefaults(string url, object[] routeValues) + { + // Arrange + var routes = new[] { + "{parameter1:int=1}/{parameter2:int=2}/{parameter3:int=3}/{parameter4:int=4}", + }; + + var expectedRouteGroup = CreateRouteGroup(0, "{parameter1:int=1}/{parameter2:int=2}/{parameter3:int=3}/{parameter4:int=4}"); + var routeValueKeys = new[] { "parameter1", "parameter2", "parameter3", "parameter4" }; + var expectedRouteValues = new RouteValueDictionary(); + for (int i = 0; i < routeValueKeys.Length; i++) + { + expectedRouteValues.Add(routeValueKeys[i], routeValues[i]); + } + + var builder = CreateBuilder(); + + // We setup the route entries in reverse order of precedence to ensure that when we + // try to route the request, the route with a higher precedence gets tried first. + foreach (var template in routes.Reverse()) + { + MapInboundEntry(builder, template); + } + + var route = builder.Build(); + + var context = CreateRouteContext(url); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.Equal(expectedRouteGroup, context.RouteData.Values["test_route_group"]); + foreach (var entry in expectedRouteValues) + { + var data = Assert.Single(context.RouteData.Values, v => v.Key == entry.Key); + Assert.Equal(entry.Value, data.Value); + } + } + + [Fact] + public async Task TreeRouter_RouteAsync_MatchesCatchAllRoutesWithDefaults() + { + // Arrange + var routes = new[] { + "{parameter1=1}/{parameter2=2}/{parameter3=3}/{*parameter4=4}", + }; + var url = "/a/b/c"; + var routeValues = new[] { "a", "b", "c", "4" }; + + var expectedRouteGroup = CreateRouteGroup(0, "{parameter1=1}/{parameter2=2}/{parameter3=3}/{*parameter4=4}"); + var routeValueKeys = new[] { "parameter1", "parameter2", "parameter3", "parameter4" }; + var expectedRouteValues = new RouteValueDictionary(); + for (int i = 0; i < routeValueKeys.Length; i++) + { + expectedRouteValues.Add(routeValueKeys[i], routeValues[i]); + } + + var builder = CreateBuilder(); + + // We setup the route entries in reverse order of precedence to ensure that when we + // try to route the request, the route with a higher precedence gets tried first. + foreach (var template in routes.Reverse()) + { + MapInboundEntry(builder, template); + } + + var route = builder.Build(); + + var context = CreateRouteContext(url); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.Equal(expectedRouteGroup, context.RouteData.Values["test_route_group"]); + foreach (var entry in expectedRouteValues) + { + var data = Assert.Single(context.RouteData.Values, v => v.Key == entry.Key); + Assert.Equal(entry.Value, data.Value); + } + } + + [Fact] + public async Task TreeRouter_RouteAsync_DoesNotMatchRoutesWithIntermediateDefaultRouteValues() + { + // Arrange + var url = "/a/b"; + + var builder = CreateBuilder(); + + MapInboundEntry(builder, "a/b/{parameter3=3}/d"); + + var route = builder.Build(); + + var context = CreateRouteContext(url); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.Null(context.Handler); + } + + [Theory] + [InlineData("a/{b=3}/c/{d?}/e/{*f}", "/a")] + [InlineData("a/{b=3}/c/{d?}/e/{*f}", "/a/b")] + [InlineData("a/{b=3}/c/{d?}/e/{*f}", "/a/b/c")] + [InlineData("a/{b=3}/c/{d?}/e/{*f}", "/a/b/c/d")] + public async Task TreeRouter_RouteAsync_DoesNotMatchRoutesWithMultipleIntermediateDefaultOrOptionalRouteValues(string template, string url) + { + // Arrange + var builder = CreateBuilder(); + + MapInboundEntry(builder, template); + + var route = builder.Build(); + + var context = CreateRouteContext(url); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.Null(context.Handler); + } + + [Theory] + [InlineData("a/{b=3}/c/{d?}/e/{*f}", "/a/b/c/d/e")] + [InlineData("a/{b=3}/c/{d?}/e/{*f}", "/a/b/c/d/e/f")] + public async Task RouteAsync_MatchRoutesWithMultipleIntermediateDefaultOrOptionalRouteValues_WhenAllIntermediateValuesAreProvided(string template, string url) + { + // Arrange + var builder = CreateBuilder(); + + MapInboundEntry(builder, template); + + var route = builder.Build(); + + var context = CreateRouteContext(url); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.NotNull(context.Handler); + } + + [Fact] + public async Task TreeRouter_RouteAsync_DoesNotMatchShorterUrl() + { + // Arrange + var routes = new[] { + "Literal1/Literal2/Literal3", + }; + + var builder = CreateBuilder(); + + // We setup the route entries in reverse order of precedence to ensure that when we + // try to route the request, the route with a higher precedence gets tried first. + foreach (var template in routes.Reverse()) + { + MapInboundEntry(builder, template); + } + + var route = builder.Build(); + + var context = CreateRouteContext("/Literal1"); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.Null(context.Handler); + } + + [Theory] + [InlineData("template/5", "template/{parameter:int}")] + [InlineData("template/5", "template/{parameter}")] + [InlineData("template/5", "template/{*parameter:int}")] + [InlineData("template/5", "template/{*parameter}")] + [InlineData("template/{parameter:int}", "template/{parameter}")] + [InlineData("template/{parameter:int}", "template/{*parameter:int}")] + [InlineData("template/{parameter:int}", "template/{*parameter}")] + [InlineData("template/{parameter}", "template/{*parameter:int}")] + [InlineData("template/{parameter}", "template/{*parameter}")] + [InlineData("template/{*parameter:int}", "template/{*parameter}")] + public async Task TreeRouter_RouteAsync_RespectsOrderOverPrecedence( + string firstTemplate, + string secondTemplate) + { + // Arrange + var expectedRouteGroup = CreateRouteGroup(0, secondTemplate); + + var builder = CreateBuilder(); + + // We setup the route entries with a lower relative order and higher relative precedence + // first to ensure that when we try to route the request, the route with the higher + // relative order gets tried first. + MapInboundEntry(builder, firstTemplate, order: 1); + MapInboundEntry(builder, secondTemplate, order: 0); + + var route = builder.Build(); + + var context = CreateRouteContext("/template/5"); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.Equal(expectedRouteGroup, context.RouteData.Values["test_route_group"]); + } + + [Theory] + [InlineData("///")] + [InlineData("/a//")] + [InlineData("/a/b//")] + [InlineData("//b//")] + [InlineData("///c")] + [InlineData("///c/")] + public async Task TryMatch_MultipleOptionalParameters_WithEmptyIntermediateSegmentsDoesNotMatch(string url) + { + // Arrange + var builder = CreateBuilder(); + + MapInboundEntry(builder, "{controller?}/{action?}/{id?}"); + + var route = builder.Build(); + + var context = CreateRouteContext(url); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.Null(context.Handler); + } + + [Theory] + [InlineData("")] + [InlineData("/")] + [InlineData("/a")] + [InlineData("/a/")] + [InlineData("/a/b")] + [InlineData("/a/b/")] + [InlineData("/a/b/c")] + [InlineData("/a/b/c/")] + public async Task TryMatch_MultipleOptionalParameters_WithIncrementalOptionalValues(string url) + { + // Arrange + var builder = CreateBuilder(); + + MapInboundEntry(builder, "{controller?}/{action?}/{id?}"); + + var route = builder.Build(); + + var context = CreateRouteContext(url); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.NotNull(context.Handler); + } + + [Theory] + [InlineData("///")] + [InlineData("////")] + [InlineData("/a//")] + [InlineData("/a///")] + [InlineData("//b/")] + [InlineData("//b//")] + [InlineData("///c")] + [InlineData("///c/")] + public async Task TryMatch_MultipleParameters_WithEmptyValues(string url) + { + // Arrange + var builder = CreateBuilder(); + + MapInboundEntry(builder, "{controller}/{action}/{id}"); + + var route = builder.Build(); + + var context = CreateRouteContext(url); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.Null(context.Handler); + } + + [Theory] + [InlineData("/a/b/c//")] + [InlineData("/a/b/c/////")] + public async Task TryMatch_CatchAllParameters_WithEmptyValuesAtTheEnd(string url) + { + // Arrange + var builder = CreateBuilder(); + + MapInboundEntry(builder, "{controller}/{action}/{*id}"); + + var route = builder.Build(); + + var context = CreateRouteContext(url); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.NotNull(context.Handler); + } + + [Theory] + [InlineData("/a/b//")] + [InlineData("/a/b///c")] + public async Task TryMatch_CatchAllParameters_WithEmptyValues(string url) + { + // Arrange + var builder = CreateBuilder(); + + MapInboundEntry(builder, "{controller}/{action}/{*id}"); + + var route = builder.Build(); + + var context = CreateRouteContext(url); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.Null(context.Handler); + } + + [Theory] + [InlineData("{*path}", "/a", "a")] + [InlineData("{*path}", "/a/b/c", "a/b/c")] + [InlineData("a/{*path}", "/a/b", "b")] + [InlineData("a/{*path}", "/a/b/c/d", "b/c/d")] + [InlineData("a/{*path:regex(10/20/30)}", "/a/10/20/30", "10/20/30")] + public async Task TreeRouter_RouteAsync_MatchesWildCard_ForLargerPathSegments( + string template, + string requestPath, + string expectedResult) + { + // Arrange + var builder = CreateBuilder(); + MapInboundEntry(builder, template); + var route = builder.Build(); + + var context = CreateRouteContext(requestPath); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.NotNull(context.Handler); + Assert.Equal(expectedResult, context.RouteData.Values["path"]); + } + + [Theory] + [InlineData("a/{*path}", "/a")] + [InlineData("a/{*path}", "/a/")] + public async Task TreeRouter_RouteAsync_MatchesCatchAll_NullValue( + string template, + string requestPath) + { + // Arrange + var builder = CreateBuilder(); + MapInboundEntry(builder, template); + var route = builder.Build(); + + var context = CreateRouteContext(requestPath); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.NotNull(context.Handler); + Assert.Null(context.RouteData.Values["path"]); + } + + [Theory] + [InlineData("a/{*path}", "/a")] + [InlineData("a/{*path}", "/a/")] + public async Task TreeRouter_RouteAsync_MatchesCatchAll_NullValue_DoesNotReplaceExistingValue( + string template, + string requestPath) + { + // Arrange + var builder = CreateBuilder(); + MapInboundEntry(builder, template); + var route = builder.Build(); + + var context = CreateRouteContext(requestPath); + context.RouteData.Values["path"] = "existing-value"; + + // Act + await route.RouteAsync(context); + + // Assert + Assert.NotNull(context.Handler); + Assert.Equal("existing-value", context.RouteData.Values["path"]); + } + + [Theory] + [InlineData("a/{*path=default}", "/a")] + [InlineData("a/{*path=default}", "/a/")] + public async Task TreeRouter_RouteAsync_MatchesCatchAll_UsesDefaultValue( + string template, + string requestPath) + { + // Arrange + var builder = CreateBuilder(); + MapInboundEntry(builder, template); + var route = builder.Build(); + + var context = CreateRouteContext(requestPath); + context.RouteData.Values["path"] = "existing-value"; + + // Act + await route.RouteAsync(context); + + // Assert + Assert.NotNull(context.Handler); + Assert.Equal("default", context.RouteData.Values["path"]); + } + + [Theory] + [InlineData("template/5")] + [InlineData("template/{parameter:int}")] + [InlineData("template/{parameter}")] + [InlineData("template/{*parameter:int}")] + [InlineData("template/{*parameter}")] + public async Task TreeRouter_RouteAsync_RespectsOrder(string template) + { + // Arrange + var expectedRouteGroup = CreateRouteGroup(0, template); + + var builder = CreateBuilder(); + + // We setup the route entries with a lower relative order first to ensure that when + // we try to route the request, the route with the higher relative order gets tried first. + MapInboundEntry(builder, template, order: 1); + MapInboundEntry(builder, template, order: 0); + + var route = builder.Build(); + + var context = CreateRouteContext("/template/5"); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.Equal(expectedRouteGroup, context.RouteData.Values["test_route_group"]); + } + + [Theory] + [InlineData("template/{first:int}", "template/{second:int}")] + [InlineData("template/{first}", "template/{second}")] + [InlineData("template/{*first:int}", "template/{*second:int}")] + [InlineData("template/{*first}", "template/{*second}")] + public async Task TreeRouter_RouteAsync_EnsuresStableOrdering(string first, string second) + { + // Arrange + var expectedRouteGroup = CreateRouteGroup(0, first); + + var builder = CreateBuilder(); + + // We setup the route entries with a lower relative template order first to ensure that when + // we try to route the request, the route with the higher template order gets tried first. + MapInboundEntry(builder, first); + MapInboundEntry(builder, second); + + var route = builder.Build(); + + var context = CreateRouteContext("/template/5"); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.Equal(expectedRouteGroup, context.RouteData.Values["test_route_group"]); + } + + [Theory] + [InlineData("template/{parameter:int}", "/template/5", true)] + [InlineData("template/{parameter:int?}", "/template/5", true)] + [InlineData("template/{parameter:int?}", "/template", true)] + [InlineData("template/{parameter:int?}", "/template/qwer", false)] + public async Task TreeRouter_WithOptionalInlineConstraint( + string template, + string request, + bool expectedResult) + { + // Arrange + var expectedRouteGroup = CreateRouteGroup(0, template); + + var builder = CreateBuilder(); + MapInboundEntry(builder, template); + var route = builder.Build(); + + var context = CreateRouteContext(request); + + // Act + await route.RouteAsync(context); + + // Assert + if (expectedResult) + { + Assert.NotNull(context.Handler); + Assert.Equal(expectedRouteGroup, context.RouteData.Values["test_route_group"]); + } + else + { + Assert.Null(context.Handler); + } + } + + [Theory] + [InlineData("moo/{p1}.{p2?}", "/moo/foo.bar", "foo", "bar", null)] + [InlineData("moo/{p1?}", "/moo/foo", "foo", null, null)] + [InlineData("moo/{p1?}", "/moo", null, null, null)] + [InlineData("moo/{p1}.{p2?}", "/moo/foo", "foo", null, null)] + [InlineData("moo/{p1}.{p2?}", "/moo/foo..bar", "foo.", "bar", null)] + [InlineData("moo/{p1}.{p2?}", "/moo/foo.moo.bar", "foo.moo", "bar", null)] + [InlineData("moo/{p1}.{p2}", "/moo/foo.bar", "foo", "bar", null)] + [InlineData("moo/foo.{p1}.{p2?}", "/moo/foo.moo.bar", "moo", "bar", null)] + [InlineData("moo/foo.{p1}.{p2?}", "/moo/foo.moo", "moo", null, null)] + [InlineData("moo/.{p2?}", "/moo/.foo", null, "foo", null)] + [InlineData("moo/{p1}.{p2?}", "/moo/....", "..", ".", null)] + [InlineData("moo/{p1}.{p2?}", "/moo/.bar", ".bar", null, null)] + [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo.moo.bar", "foo", "moo", "bar")] + [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo.moo", "foo", "moo", null)] + [InlineData("moo/{p1}.{p2}.{p3}.{p4?}", "/moo/foo.moo.bar", "foo", "moo", "bar")] + [InlineData("{p1}.{p2?}/{p3}", "/foo.moo/bar", "foo", "moo", "bar")] + [InlineData("{p1}.{p2?}/{p3}", "/foo/bar", "foo", null, "bar")] + [InlineData("{p1}.{p2?}/{p3}", "/.foo/bar", ".foo", null, "bar")] + public async Task TreeRouter_WithOptionalCompositeParameter_Valid( + string template, + string request, + string p1, + string p2, + string p3) + { + // Arrange + var expectedRouteGroup = CreateRouteGroup(0, template); + + var builder = CreateBuilder(); + MapInboundEntry(builder, template); + var route = builder.Build(); + + var context = CreateRouteContext(request); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.NotNull(context.Handler); + if (p1 != null) + { + Assert.Equal(p1, context.RouteData.Values["p1"]); + } + if (p2 != null) + { + Assert.Equal(p2, context.RouteData.Values["p2"]); + } + if (p3 != null) + { + Assert.Equal(p3, context.RouteData.Values["p3"]); + } + } + + [Theory] + [InlineData("moo/{p1}.{p2?}", "/moo/foo.")] + [InlineData("moo/{p1}.{p2?}", "/moo/.")] + [InlineData("moo/{p1}.{p2}", "/foo.")] + [InlineData("moo/{p1}.{p2}", "/foo")] + [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo.moo.")] + [InlineData("moo/foo.{p2}.{p3?}", "/moo/bar.foo.moo")] + [InlineData("moo/foo.{p2}.{p3?}", "/moo/kungfoo.moo.bar")] + [InlineData("moo/foo.{p2}.{p3?}", "/moo/kungfoo.moo")] + [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo")] + [InlineData("{p1}.{p2?}/{p3}", "/foo./bar")] + [InlineData("moo/.{p2?}", "/moo/.")] + [InlineData("{p1}.{p2}/{p3}", "/.foo/bar")] + public async Task TreeRouter_WithOptionalCompositeParameter_Invalid( + string template, + string request) + { + // Arrange + var expectedRouteGroup = CreateRouteGroup(0, template); + + var builder = CreateBuilder(); + MapInboundEntry(builder, template); + var route = builder.Build(); + + var context = CreateRouteContext(request); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.Null(context.Handler); + } + + [Theory] + [InlineData("template", "{*url:alpha}", "/template?url=dingo&id=5")] + [InlineData("{*url:alpha}", "{*url}", "/dingo?id=5")] + [InlineData("{id}", "{*url}", "/5?url=dingo")] + [InlineData("{id}", "{*url:alpha}", "/5?url=dingo")] + [InlineData("{id:int}", "{id}", "/5?url=dingo")] + [InlineData("{id}", "{id:alpha}/{url}", "/5?url=dingo")] // constraint doesn't match + [InlineData("template/api/{*url}", "template/api", "/template/api/dingo?id=5")] + [InlineData("template/api", "template/{*url}", "/template/api?url=dingo&id=5")] + [InlineData("template/api", "template/api{id}location", "/template/api?url=dingo&id=5")] + [InlineData("template/api{id}location", "template/{id:int}", "/template/api5location?url=dingo")] + public void TreeRouter_GenerateLink(string firstTemplate, string secondTemplate, string expectedPath) + { + // Arrange + var values = new Dictionary + { + {"url", "dingo" }, + {"id", 5 } + }; + + var route = CreateTreeRouter(firstTemplate, secondTemplate); + var context = CreateVirtualPathContext( + values: values, + ambientValues: null); + + // Act + var result = route.GetVirtualPath(context); + + // Assert + Assert.NotNull(result); + Assert.Equal(expectedPath, result.VirtualPath); + Assert.Same(route, result.Router); + Assert.Empty(result.DataTokens); + } + + [Fact] + public void TreeRouter_GenerateLink_LongerTemplateWithDefaultIsMoreSpecific() + { + // Arrange + var firstTemplate = "template"; + var secondTemplate = "template/{parameter:int=1003}"; + + var route = CreateTreeRouter(firstTemplate, secondTemplate); + var context = CreateVirtualPathContext( + values: null, + ambientValues: null); + + // Act + var result = route.GetVirtualPath(context); + + // Assert + Assert.NotNull(result); + // The Binder binds to /template + Assert.Equal("/template", result.VirtualPath); + Assert.Same(route, result.Router); + Assert.Empty(result.DataTokens); + } + + [Theory] + [InlineData("template/{parameter:int=5}", "template", "/template/5")] + [InlineData("template/{parameter}", "template", "/template/5")] + [InlineData("template/{parameter}/{id}", "template/{parameter}", "/template/5/1234")] + public void TreeRouter_GenerateLink_OrderingAgnostic( + string firstTemplate, + string secondTemplate, + string expectedPath) + { + // Arrange + var route = CreateTreeRouter(firstTemplate, secondTemplate); + var parameter = 5; + var id = 1234; + var values = new Dictionary + { + { nameof(parameter) , parameter}, + { nameof(id), id } + }; + var context = CreateVirtualPathContext( + values: null, + ambientValues: values); + + // Act + var result = route.GetVirtualPath(context); + + // Assert + Assert.NotNull(result); + Assert.Equal(expectedPath, result.VirtualPath); + Assert.Same(route, result.Router); + Assert.Empty(result.DataTokens); + } + + [Theory] + [InlineData("template", "template/{parameter}", "/template/5")] + [InlineData("template/{parameter}", "template/{parameter}/{id}", "/template/5/1234")] + [InlineData("template", "template/{parameter:int=5}", "/template/5")] + public void TreeRouter_GenerateLink_UseAvailableVariables( + string firstTemplate, + string secondTemplate, + string expectedPath) + { + // Arrange + var route = CreateTreeRouter(firstTemplate, secondTemplate); + var parameter = 5; + var id = 1234; + var values = new Dictionary + { + { nameof(parameter) , parameter}, + { nameof(id), id } + }; + var context = CreateVirtualPathContext( + values: null, + ambientValues: values); + + // Act + var result = route.GetVirtualPath(context); + + // Assert + Assert.NotNull(result); + Assert.Equal(expectedPath, result.VirtualPath); + Assert.Same(route, result.Router); + Assert.Empty(result.DataTokens); + } + + [Theory] + [InlineData("template/5", "template/{parameter:int}")] + [InlineData("template/5", "template/{parameter}")] + [InlineData("template/5", "template/{*parameter:int}")] + [InlineData("template/5", "template/{*parameter}")] + [InlineData("template/{parameter:int}", "template/{parameter}")] + [InlineData("template/{parameter:int}", "template/{*parameter:int}")] + [InlineData("template/{parameter:int}", "template/{*parameter}")] + [InlineData("template/{parameter}", "template/{*parameter:int}")] + [InlineData("template/{parameter}", "template/{*parameter}")] + [InlineData("template/{*parameter:int}", "template/{*parameter}")] + public void TreeRouter_GenerateLink_RespectsPrecedence(string firstTemplate, string secondTemplate) + { + // Arrange + var builder = CreateBuilder(); + + // We setup the route entries in reverse order of precedence to ensure that when we + // try to generate a link, the route with a higher precedence gets tried first. + MapOutboundEntry(builder, secondTemplate); + MapOutboundEntry(builder, firstTemplate); + + var route = builder.Build(); + + var context = CreateVirtualPathContext(values: null, ambientValues: new { parameter = 5 }); + + // Act + var result = route.GetVirtualPath(context); + + // Assert + Assert.NotNull(result); + Assert.Equal("/template/5", result.VirtualPath); + Assert.Same(route, result.Router); + Assert.Empty(result.DataTokens); + } + + [Theory] + [InlineData("template/{parameter:int}", "/template/5", 5)] + [InlineData("template/{parameter:int?}", "/template/5", 5)] + [InlineData("template/{parameter:int?}", "/template", null)] + [InlineData("template/{parameter:int?}", null, "asdf")] + [InlineData("template/{parameter:alpha?}", "/template/asdf", "asdf")] + [InlineData("template/{parameter:alpha?}", "/template", null)] + [InlineData("template/{parameter:int:range(1,20)?}", "/template", null)] + [InlineData("template/{parameter:int:range(1,20)?}", "/template/5", 5)] + [InlineData("template/{parameter:int:range(1,20)?}", null, 21)] + public void TreeRouter_GenerateLink_OptionalInlineParameter( + string template, + string expectedPath, + object parameter) + { + // Arrange + var builder = CreateBuilder(); + MapOutboundEntry(builder, template); + var route = builder.Build(); + + VirtualPathContext context; + if (parameter != null) + { + context = CreateVirtualPathContext(values: null, ambientValues: new { parameter = parameter }); + } + else + { + context = CreateVirtualPathContext(values: null, ambientValues: null); + } + + // Act + var result = route.GetVirtualPath(context); + + // Assert + if (expectedPath == null) + { + Assert.Null(result); + } + else + { + Assert.NotNull(result); + Assert.Equal(expectedPath, result.VirtualPath); + Assert.Same(route, result.Router); + Assert.Empty(result.DataTokens); + } + } + + [Theory] + [InlineData("template/5", "template/{parameter:int}")] + [InlineData("template/5", "template/{parameter}")] + [InlineData("template/5", "template/{*parameter:int}")] + [InlineData("template/5", "template/{*parameter}")] + [InlineData("template/{parameter:int}", "template/{parameter}")] + [InlineData("template/{parameter:int}", "template/{*parameter:int}")] + [InlineData("template/{parameter:int}", "template/{*parameter}")] + [InlineData("template/{parameter}", "template/{*parameter:int}")] + [InlineData("template/{parameter}", "template/{*parameter}")] + [InlineData("template/{*parameter:int}", "template/{*parameter}")] + public void TreeRouter_GenerateLink_RespectsOrderOverPrecedence(string firstTemplate, string secondTemplate) + { + // Arrange + var builder = CreateBuilder(); + + // We setup the route entries with a lower relative order and higher relative precedence + // first to ensure that when we try to generate a link, the route with the higher + // relative order gets tried first. + MapOutboundEntry(builder, firstTemplate, order: 1); + MapOutboundEntry(builder, secondTemplate, order: 0); + + var route = builder.Build(); + + var context = CreateVirtualPathContext(null, ambientValues: new { parameter = 5 }); + + // Act + var result = route.GetVirtualPath(context); + + // Assert + Assert.NotNull(result); + Assert.Equal("/template/5", result.VirtualPath); + Assert.Same(route, result.Router); + Assert.Empty(result.DataTokens); + } + + [Theory] + [InlineData("template/5", "template/5")] + [InlineData("template/{first:int}", "template/{second:int}")] + [InlineData("template/{first}", "template/{second}")] + [InlineData("template/{*first:int}", "template/{*second:int}")] + [InlineData("template/{*first}", "template/{*second}")] + public void TreeRouter_GenerateLink_RespectsOrder(string firstTemplate, string secondTemplate) + { + // Arrange + var builder = CreateBuilder(); + + // We setup the route entries with a lower relative order first to ensure that when + // we try to generate a link, the route with the higher relative order gets tried first. + MapOutboundEntry(builder, firstTemplate, requiredValues: null, order: 1); + MapOutboundEntry(builder, secondTemplate, requiredValues: null, order: 0); + + var route = builder.Build(); + + var context = CreateVirtualPathContext(values: null, ambientValues: new { first = 5, second = 5 }); + + // Act + var result = route.GetVirtualPath(context); + + // Assert + Assert.NotNull(result); + Assert.Equal("/template/5", result.VirtualPath); + Assert.Same(route, result.Router); + Assert.Empty(result.DataTokens); + } + + [Theory] + [InlineData("first/5", "second/5")] + [InlineData("first/{first:int}", "second/{second:int}")] + [InlineData("first/{first}", "second/{second}")] + [InlineData("first/{*first:int}", "second/{*second:int}")] + [InlineData("first/{*first}", "second/{*second}")] + public void TreeRouter_GenerateLink_EnsuresStableOrder(string firstTemplate, string secondTemplate) + { + // Arrange + var builder = CreateBuilder(); + + // We setup the route entries with a lower relative template order first to ensure that when + // we try to generate a link, the route with the higher template order gets tried first. + MapOutboundEntry(builder, secondTemplate, requiredValues: null, order: 0); + MapOutboundEntry(builder, firstTemplate, requiredValues: null, order: 0); + + var route = builder.Build(); + + var context = CreateVirtualPathContext(values: null, ambientValues: new { first = 5, second = 5 }); + + // Act + var result = route.GetVirtualPath(context); + + // Assert + Assert.NotNull(result); + Assert.Equal("/first/5", result.VirtualPath); + Assert.Same(route, result.Router); + Assert.Empty(result.DataTokens); + } + + [Fact] + public void TreeRouter_GenerateLink_CreatesLinksForRoutesWithIntermediateDefaultRouteValues() + { + // Arrange + var builder = CreateBuilder(); + + MapOutboundEntry(builder, template: "a/b/{parameter3=3}/d", requiredValues: null, order: 0); + + var route = builder.Build(); + + var context = CreateVirtualPathContext(values: null, ambientValues: null); + + // Act + var result = route.GetVirtualPath(context); + + // Assert + Assert.NotNull(result); + Assert.Equal("/a/b/3/d", result.VirtualPath); + } + + + [Fact] + public void TreeRouter_GeneratesLink_ForMultipleNamedEntriesWithTheSameTemplate() + { + // Arrange + var builder = CreateBuilder(); + + MapOutboundEntry(builder, "Template", name: "NamedEntry", order: 1); + MapOutboundEntry(builder, "TEMPLATE", name: "NamedEntry", order: 2); + + // Act & Assert (does not throw) + builder.Build(); + } + + [Fact] + public void TreeRouter_GenerateLink_WithName() + { + // Arrange + var builder = CreateBuilder(); + + // The named route has a lower order which will ensure that we aren't trying the route as + // if it were an unnamed route. + MapOutboundEntry(builder, "named", requiredValues: null, order: 1, name: "NamedRoute"); + MapOutboundEntry(builder, "unnamed", requiredValues: null, order: 0); + + var route = builder.Build(); + + var context = CreateVirtualPathContext(values: null, ambientValues: null, name: "NamedRoute"); + + // Act + var result = route.GetVirtualPath(context); + + // Assert + Assert.NotNull(result); + Assert.Equal("/named", result.VirtualPath); + Assert.Same(route, result.Router); + Assert.Empty(result.DataTokens); + } + + [Fact] + public void TreeRouter_DoesNotGenerateLink_IfThereIsNoRouteForAGivenName() + { + // Arrange + var builder = CreateBuilder(); + + // The named route has a lower order which will ensure that we aren't trying the route as + // if it were an unnamed route. + MapOutboundEntry(builder, "named", requiredValues: null, order: 1, name: "NamedRoute"); + + // Add an unnamed entry to ensure we don't fall back to generating a link for an unnamed route. + MapOutboundEntry(builder, "unnamed", requiredValues: null, order: 0); + + var route = builder.Build(); + + var context = CreateVirtualPathContext(values: null, ambientValues: null, name: "NonExistingNamedRoute"); + + // Act + var result = route.GetVirtualPath(context); + + // Assert + Assert.Null(result); + } + + [Theory] + [InlineData("template/{parameter:int}", null)] + [InlineData("template/{parameter:int}", "NaN")] + [InlineData("template/{parameter}", null)] + [InlineData("template/{*parameter:int}", null)] + [InlineData("template/{*parameter:int}", "NaN")] + public void TreeRouter_DoesNotGenerateLink_IfValuesDoNotMatchNamedEntry(string template, string value) + { + // Arrange + var builder = CreateBuilder(); + + // The named route has a lower order which will ensure that we aren't trying the route as + // if it were an unnamed route. + MapOutboundEntry(builder, template, requiredValues: null, order: 1, name: "NamedRoute"); + + // Add an unnamed entry to ensure we don't fall back to generating a link for an unnamed route. + MapOutboundEntry(builder, "unnamed", requiredValues: null, order: 0); + + var route = builder.Build(); + + var ambientValues = value == null ? null : new { parameter = value }; + var context = CreateVirtualPathContext(values: null, ambientValues: ambientValues, name: "NamedRoute"); + + // Act + var result = route.GetVirtualPath(context); + + // Assert + Assert.Null(result); + } + + [Theory] + [InlineData("template/{parameter:int}", "5")] + [InlineData("template/{parameter}", "5")] + [InlineData("template/{*parameter:int}", "5")] + [InlineData("template/{*parameter}", "5")] + public void TreeRouter_GeneratesLink_IfValuesMatchNamedEntry(string template, string value) + { + // Arrange + var builder = CreateBuilder(); + + // The named route has a lower order which will ensure that we aren't trying the route as + // if it were an unnamed route. + MapOutboundEntry(builder, template, requiredValues: null, order: 1, name: "NamedRoute"); + + // Add an unnamed entry to ensure we don't fall back to generating a link for an unnamed route. + MapOutboundEntry(builder, "unnamed", requiredValues: null, order: 0); + + var route = builder.Build(); + + var ambientValues = value == null ? null : new { parameter = value }; + var context = CreateVirtualPathContext(values: null, ambientValues: ambientValues, name: "NamedRoute"); + + // Act + var result = route.GetVirtualPath(context); + + // Assert + Assert.NotNull(result); + Assert.Equal("/template/5", result.VirtualPath); + Assert.Same(route, result.Router); + Assert.Empty(result.DataTokens); + } + + [Fact] + public void TreeRouter_GenerateLink_NoRequiredValues() + { + // Arrange + var builder = CreateBuilder(); + MapOutboundEntry(builder, "api/Store", new { }); + var route = builder.Build(); + + var context = CreateVirtualPathContext(new { }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.NotNull(pathData); + Assert.Equal("/api/Store", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } + + [Fact] + public void TreeRouter_GenerateLink_Match() + { + // Arrange + var builder = CreateBuilder(); + MapOutboundEntry(builder, "api/Store", new { action = "Index", controller = "Store" }); + var route = builder.Build(); + + var context = CreateVirtualPathContext(new { action = "Index", controller = "Store" }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.NotNull(pathData); + Assert.Equal("/api/Store", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } + + [Fact] + public void TreeRouter_GenerateLink_NoMatch() + { + // Arrange + var builder = CreateBuilder(); + MapOutboundEntry(builder, "api/Store", new { action = "Details", controller = "Store" }); + var route = builder.Build(); + + var context = CreateVirtualPathContext(new { action = "Index", controller = "Store" }); + + // Act + var path = route.GetVirtualPath(context); + + // Assert + Assert.Null(path); + } + + [Fact] + public void TreeRouter_GenerateLink_Match_WithAmbientValues() + { + // Arrange + var builder = CreateBuilder(); + MapOutboundEntry(builder, "api/Store", new { action = "Index", controller = "Store" }); + var route = builder.Build(); + + var context = CreateVirtualPathContext(new { }, new { action = "Index", controller = "Store" }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.NotNull(pathData); + Assert.Equal("/api/Store", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } + + [Fact] + public void TreeRouter_GenerateLink_Match_HasTwoOptionalParametersWithoutValues() + { + // Arrange + var builder = CreateBuilder(); + MapOutboundEntry(builder, "Customers/SeparatePageModels/{handler?}/{id?}", new { page = "/Customers/SeparatePageModels/Index" }); + var route = builder.Build(); + + var context = CreateVirtualPathContext(new { page = "/Customers/SeparatePageModels/Index" }, new { page = "/Customers/SeparatePageModels/Edit", id = "17" }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.NotNull(pathData); + Assert.Equal("/Customers/SeparatePageModels", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } + + [Fact] + public void TreeRouter_GenerateLink_Match_WithParameters() + { + // Arrange + var builder = CreateBuilder(); + MapOutboundEntry(builder, "api/Store/{action}", new { action = "Index", controller = "Store" }); + var route = builder.Build(); + + var context = CreateVirtualPathContext(new { action = "Index", controller = "Store" }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.NotNull(pathData); + Assert.Equal("/api/Store/Index", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } + + [Fact] + public void TreeRouter_GenerateLink_Match_WithMoreParameters() + { + // Arrange + var builder = CreateBuilder(); + MapOutboundEntry(builder, + "api/{area}/dosomething/{controller}/{action}", + new { action = "Index", controller = "Store", area = "AwesomeCo" }); + + var route = builder.Build(); + + var context = CreateVirtualPathContext( + new { action = "Index", controller = "Store" }, + new { area = "AwesomeCo" }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.NotNull(pathData); + Assert.Equal("/api/AwesomeCo/dosomething/Store/Index", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } + + [Fact] + public void TreeRouter_GenerateLink_Match_WithDefault() + { + // Arrange + var builder = CreateBuilder(); + MapOutboundEntry(builder, "api/Store/{action=Index}", new { action = "Index", controller = "Store" }); + var route = builder.Build(); + + var context = CreateVirtualPathContext(new { action = "Index", controller = "Store" }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.NotNull(pathData); + Assert.Equal("/api/Store", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } + + [Fact] + public void TreeRouter_GenerateLink_Match_WithConstraint() + { + // Arrange + var builder = CreateBuilder(); + MapOutboundEntry(builder, "api/Store/{action}/{id:int}", new { action = "Index", controller = "Store" }); + + var route = builder.Build(); + + var context = CreateVirtualPathContext(new { action = "Index", controller = "Store", id = 5 }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.NotNull(pathData); + Assert.Equal("/api/Store/Index/5", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } + + [Fact] + public void TreeRouter_GenerateLink_NoMatch_WithConstraint() + { + // Arrange + var builder = CreateBuilder(); + MapOutboundEntry(builder, "api/Store/{action}/{id:int}", new { action = "Index", controller = "Store" }); + var route = builder.Build(); + + var next = new StubRouter(); + var context = CreateVirtualPathContext(new { action = "Index", controller = "Store", id = "heyyyy" }); + + // Act + var path = route.GetVirtualPath(context); + + // Assert + Assert.Null(path); + } + + [Fact] + public void TreeRouter_GenerateLink_Match_WithMixedAmbientValues() + { + // Arrange + var builder = CreateBuilder(); + MapOutboundEntry(builder, "api/Store", new { action = "Index", controller = "Store" }); + var route = builder.Build(); + + var context = CreateVirtualPathContext(new { action = "Index" }, new { controller = "Store" }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.NotNull(pathData); + Assert.Equal("/api/Store", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } + + [Fact] + public void TreeRouter_GenerateLink_Match_WithQueryString() + { + // Arrange + var builder = CreateBuilder(); + MapOutboundEntry(builder, "api/Store", new { action = "Index", controller = "Store" }); + var route = builder.Build(); + + var context = CreateVirtualPathContext(new { action = "Index", id = 5 }, new { controller = "Store" }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.NotNull(pathData); + Assert.Equal("/api/Store?id=5", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } + + [Fact] + public void TreeRouter_GenerateLink_RejectedByFirstRoute() + { + // Arrange + var builder = CreateBuilder(); + MapOutboundEntry(builder, "api/Store", new { action = "Index", controller = "Store" }); + MapOutboundEntry(builder, "api2/{controller}", new { action = "Index", controller = "Blog" }); + + var route = builder.Build(); + + var context = CreateVirtualPathContext(new { action = "Index", controller = "Blog" }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.NotNull(pathData); + Assert.Equal("/api2/Blog", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } + + [Fact] + public void TreeRouter_GenerateLink_ToArea() + { + // Arrange + var builder = CreateBuilder(); + var entry1 = MapOutboundEntry(builder, "Help/Store", new { area = "Help", action = "Edit", controller = "Store" }); + entry1.Precedence = 2; + + var entry2 = MapOutboundEntry(builder, "Store", new { area = (string)null, action = "Edit", controller = "Store" }); + entry2.Precedence = 1; + + var route = builder.Build(); + + var context = CreateVirtualPathContext(new { area = "Help", 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_ToArea_PredecedenceReversed() + { + // Arrange + var builder = CreateBuilder(); + var entry1 = MapOutboundEntry(builder, "Help/Store", new { area = "Help", action = "Edit", controller = "Store" }); + entry1.Precedence = 1; + + var entry2 = MapOutboundEntry(builder, "Store", new { area = (string)null, action = "Edit", controller = "Store" }); + entry2.Precedence = 2; + + var route = builder.Build(); + + var context = CreateVirtualPathContext(new { area = "Help", 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_ToArea_WithAmbientValues() + { + // Arrange + var builder = CreateBuilder(); + var entry1 = MapOutboundEntry(builder, "Help/Store", new { area = "Help", action = "Edit", controller = "Store" }); + entry1.Precedence = 2; + + var entry2 = MapOutboundEntry(builder, "Store", new { area = (string)null, action = "Edit", controller = "Store" }); + entry2.Precedence = 1; + + var route = builder.Build(); + + var context = CreateVirtualPathContext( + values: new { action = "Edit", controller = "Store" }, + ambientValues: new { area = "Help" }); + + // 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_OutOfArea_IgnoresAmbientValue() + { + // Arrange + var builder = CreateBuilder(); + var entry1 = MapOutboundEntry(builder, "Help/Store", new { area = "Help", action = "Edit", controller = "Store" }); + entry1.Precedence = 2; + + var entry2 = MapOutboundEntry(builder, "Store", new { area = (string)null, action = "Edit", controller = "Store" }); + entry2.Precedence = 1; + + var route = builder.Build(); + + var context = CreateVirtualPathContext( + values: new { action = "Edit", controller = "Store" }, + ambientValues: new { area = "Blog" }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.NotNull(pathData); + Assert.Equal("/Store", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } + + public static IEnumerable OptionalParamValues + { + get + { + return new object[][] + { + // defaults + // ambient values + // values + new object[] + { + "Test/{val1}/{val2}.{val3?}", + new {val1 = "someval1", val2 = "someval2", val3 = "someval3a"}, + new {val3 = "someval3v"}, + "/Test/someval1/someval2.someval3v", + }, + new object[] + { + "Test/{val1}/{val2}.{val3?}", + new {val3 = "someval3a"}, + new {val1 = "someval1", val2 = "someval2", val3 = "someval3v" }, + "/Test/someval1/someval2.someval3v", + }, + new object[] + { + "Test/{val1}/{val2}.{val3?}", + null, + new {val1 = "someval1", val2 = "someval2" }, + "/Test/someval1/someval2", + }, + new object[] + { + "Test/{val1}.{val2}.{val3}.{val4?}", + new {val1 = "someval1", val2 = "someval2" }, + new {val4 = "someval4", val3 = "someval3" }, + "/Test/someval1.someval2.someval3.someval4", + }, + new object[] + { + "Test/{val1}.{val2}.{val3}.{val4?}", + new {val1 = "someval1", val2 = "someval2" }, + new {val3 = "someval3" }, + "/Test/someval1.someval2.someval3", + }, + new object[] + { + "Test/.{val2?}", + null, + new {val2 = "someval2" }, + "/Test/.someval2", + }, + new object[] + { + "Test/.{val2?}", + null, + null, + "/Test/", + }, + new object[] + { + "Test/{val1}.{val2}", + new {val1 = "someval1", val2 = "someval2" }, + new {val3 = "someval3" }, + "/Test/someval1.someval2?val3=someval3", + }, + }; + } + } + + [Theory] + [MemberData(nameof(OptionalParamValues))] + public void TreeRouter_GenerateLink_Match_WithOptionalParameters( + string template, + object ambientValues, + object values, + string expected) + { + // Arrange + var builder = CreateBuilder(); + MapOutboundEntry(builder, template); + var route = builder.Build(); + + var context = CreateVirtualPathContext(values, ambientValues); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.NotNull(pathData); + Assert.Equal(expected, pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } + + [Fact] + public async Task TreeRouter_ReplacesExistingRouteValues_IfNotNull() + { + // Arrange + var builder = CreateBuilder(); + MapInboundEntry(builder, "Foo/{*path}"); + var route = builder.Build(); + + var context = CreateRouteContext("/Foo/Bar"); + + var originalRouteData = context.RouteData; + originalRouteData.Values.Add("path", "default"); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.Equal("Bar", context.RouteData.Values["path"]); + } + + [Fact] + public async Task TreeRouter_DoesNotReplaceExistingRouteValues_IfNull() + { + // Arrange + var builder = CreateBuilder(); + MapInboundEntry(builder, "Foo/{*path}"); + var route = builder.Build(); + + var context = CreateRouteContext("/Foo/"); + + var originalRouteData = context.RouteData; + originalRouteData.Values.Add("path", "default"); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.Equal("default", context.RouteData.Values["path"]); + } + + [Fact] + public async Task TreeRouter_SnapshotsRouteData() + { + // Arrange + RouteValueDictionary nestedValues = null; + List nestedRouters = null; + + var next = new Mock(); + next + .Setup(r => r.RouteAsync(It.IsAny())) + .Callback(c => + { + nestedValues = new RouteValueDictionary(c.RouteData.Values); + nestedRouters = new List(c.RouteData.Routers); + c.Handler = null; // Not a match + }) + .Returns(Task.FromResult(0)); + + var builder = CreateBuilder(); + MapInboundEntry(builder, "api/Store", handler: next.Object); + var route = builder.Build(); + + var context = CreateRouteContext("/api/Store"); + + var routeData = context.RouteData; + routeData.Values.Add("action", "Index"); + + var originalValues = new RouteValueDictionary(context.RouteData.Values); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.Equal(originalValues, context.RouteData.Values); + Assert.NotEqual(nestedValues, context.RouteData.Values); + } + + [Fact] + public async Task TreeRouter_SnapshotsRouteData_ResetsWhenNotMatched() + { + // Arrange + RouteValueDictionary nestedValues = null; + List nestedRouters = null; + + var next = new Mock(); + next + .Setup(r => r.RouteAsync(It.IsAny())) + .Callback(c => + { + nestedValues = new RouteValueDictionary(c.RouteData.Values); + nestedRouters = new List(c.RouteData.Routers); + c.Handler = null; // Not a match + }) + .Returns(Task.FromResult(0)); + + var builder = CreateBuilder(); + MapInboundEntry(builder, "api/Store", handler: next.Object); + var route = builder.Build(); + + var context = CreateRouteContext("/api/Store"); + + context.RouteData.Values.Add("action", "Index"); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.NotEqual(nestedValues, context.RouteData.Values); + + // The new routedata is a copy + Assert.Equal("Index", context.RouteData.Values["action"]); + Assert.Equal("Index", nestedValues["action"]); + Assert.DoesNotContain(context.RouteData.Values, kvp => kvp.Key == "test_route_group"); + Assert.Single(nestedValues, kvp => kvp.Key == "test_route_group"); + + Assert.Empty(context.RouteData.Routers); + + Assert.Single(nestedRouters); + Assert.Equal(next.Object.GetType(), nestedRouters[0].GetType()); + } + + [Fact] + public async Task TreeRouter_SnapshotsRouteData_ResetsWhenThrows() + { + // Arrange + RouteValueDictionary nestedValues = null; + List nestedRouters = null; + + var next = new Mock(); + next + .Setup(r => r.RouteAsync(It.IsAny())) + .Callback(c => + { + nestedValues = new RouteValueDictionary(c.RouteData.Values); + nestedRouters = new List(c.RouteData.Routers); + throw new Exception(); + }) + .Returns(Task.FromResult(0)); + + var builder = CreateBuilder(); + MapInboundEntry(builder, "api/Store", handler: next.Object); + var route = builder.Build(); + + var context = CreateRouteContext("/api/Store"); + context.RouteData.Values.Add("action", "Index"); + + // Act + await Assert.ThrowsAsync(() => route.RouteAsync(context)); + + // Assert + Assert.NotEqual(nestedValues, context.RouteData.Values); + + Assert.Equal("Index", context.RouteData.Values["action"]); + Assert.Equal("Index", nestedValues["action"]); + Assert.DoesNotContain(context.RouteData.Values, kvp => kvp.Key == "test_route_group"); + Assert.Single(nestedValues, kvp => kvp.Key == "test_route_group"); + + Assert.Empty(context.RouteData.Routers); + + Assert.Single(nestedRouters); + Assert.Equal(next.Object.GetType(), nestedRouters[0].GetType()); + } + + [Fact] + public async Task TreeRouter_SnapshotsRouteData_ResetsBeforeMatchingEachRouteEntry() + { + // This test replicates a scenario raised as issue https://github.com/aspnet/Routing/issues/394 + // The RouteValueDictionary entries populated while matching route entries should not be left + // in place if the route entry turns out not to match, because that would leak unwanted state + // to subsequent route entries and might cause "An element with the key ... already exists" + // exceptions. + + // Arrange + RouteValueDictionary nestedValues = null; + var next = new Mock(); + next + .Setup(r => r.RouteAsync(It.IsAny())) + .Callback(c => + { + nestedValues = new RouteValueDictionary(c.RouteData.Values); + c.Handler = NullHandler; + }) + .Returns(Task.CompletedTask); + + var builder = CreateBuilder(); + MapInboundEntry(builder, "cat_{category1}/prod1_{product}"); // Matches on first segment but not on second + MapInboundEntry(builder, "cat_{category2}/prod2_{product}", handler: next.Object); + var route = builder.Build(); + + var context = CreateRouteContext("/cat_examplecategory/prod2_exampleproduct"); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.NotNull(nestedValues); + Assert.Equal("examplecategory", nestedValues["category2"]); + Assert.Equal("exampleproduct", nestedValues["product"]); + 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); + request.SetupGet(r => r.Path).Returns(new PathString(requestPath)); + + var context = new Mock(MockBehavior.Strict); + context.Setup(m => m.RequestServices.GetService(typeof(ILoggerFactory))) + .Returns(NullLoggerFactory.Instance); + + context.SetupGet(c => c.Request).Returns(request.Object); + + return new RouteContext(context.Object); + } + + private static VirtualPathContext CreateVirtualPathContext( + object values, + object ambientValues = null, + string name = null) + { + var mockHttpContext = new Mock(); + mockHttpContext.Setup(h => h.RequestServices.GetService(typeof(ILoggerFactory))) + .Returns(NullLoggerFactory.Instance); + + return new VirtualPathContext( + mockHttpContext.Object, + new RouteValueDictionary(ambientValues), + new RouteValueDictionary(values), + name); + } + + private static InboundRouteEntry MapInboundEntry( + TreeRouteBuilder builder, + string template, + int order = 0, + IRouter handler = null) + { + var entry = builder.MapInbound( + handler ?? new StubRouter(), + TemplateParser.Parse(template), + routeName: null, + order: order); + + // Add a generated 'route group' so we can identify later which entry matched. + entry.Defaults["test_route_group"] = CreateRouteGroup(order, template); + + return entry; + } + + private static OutboundRouteEntry MapOutboundEntry( + TreeRouteBuilder builder, + string template, + object requiredValues = null, + int order = 0, + string name = null, + IRouter handler = null) + { + var entry = builder.MapOutbound( + handler ?? new StubRouter(), + TemplateParser.Parse(template), + requiredLinkValues: new RouteValueDictionary(requiredValues), + routeName: name, + order: order); + + // Add a generated 'route group' so we can identify later which entry matched. + entry.Defaults["test_route_group"] = CreateRouteGroup(order, template); + + return entry; + } + + + private static string CreateRouteGroup(int order, string template) + { + return string.Format("{0}&{1}", order, template); + } + + private static DefaultInlineConstraintResolver CreateConstraintResolver() + { + var options = new RouteOptions(); + var optionsMock = new Mock>(); + optionsMock.SetupGet(o => o.Value).Returns(options); + + return new DefaultInlineConstraintResolver(optionsMock.Object); + } + + private static TreeRouteBuilder CreateBuilder() + { + var objectPoolProvider = new DefaultObjectPoolProvider(); + var objectPolicy = new UriBuilderContextPooledObjectPolicy(); + var objectPool = objectPoolProvider.Create(objectPolicy); + + var constraintResolver = CreateConstraintResolver(); + var builder = new TreeRouteBuilder( + NullLoggerFactory.Instance, + objectPool, + constraintResolver); + return builder; + } + + private static TreeRouteBuilder CreateBuilderUsingObsoleteConstructor() + { + var objectPoolProvider = new DefaultObjectPoolProvider(); + var objectPolicy = new UriBuilderContextPooledObjectPolicy(); + var objectPool = objectPoolProvider.Create(objectPolicy); + + var constraintResolver = CreateConstraintResolver(); +#pragma warning disable CS0618 // Type or member is obsolete + var builder = new TreeRouteBuilder( + NullLoggerFactory.Instance, + UrlEncoder.Default, + objectPool, + constraintResolver); +#pragma warning restore CS0618 // Type or member is obsolete + return builder; + } + + private static TreeRouter CreateTreeRouter( + string firstTemplate, + string secondTemplate) + { + var builder = CreateBuilder(); + MapOutboundEntry(builder, firstTemplate); + MapOutboundEntry(builder, secondTemplate); + return builder.Build(); + } + + private class StubRouter : IRouter + { + public VirtualPathContext GenerationContext { get; set; } + + public RouteContext MatchingContext { get; set; } + + public Func MatchingDelegate { get; set; } + + public VirtualPathData GetVirtualPath(VirtualPathContext context) + { + GenerationContext = context; + return null; + } + + public Task RouteAsync(RouteContext context) + { + if (MatchingDelegate == null) + { + context.Handler = NullHandler; + } + else + { + context.Handler = MatchingDelegate(context) ? NullHandler : null; + } + + return Task.FromResult(true); + } + } + } +} diff --git a/src/Routing/version.props b/src/Routing/version.props new file mode 100644 index 0000000000..669c874829 --- /dev/null +++ b/src/Routing/version.props @@ -0,0 +1,12 @@ + + + 2.1.1 + rtm + $(VersionPrefix) + $(VersionPrefix)-$(VersionSuffix)-final + t000 + a- + $(FeatureBranchVersionPrefix)$(VersionSuffix)-$([System.Text.RegularExpressions.Regex]::Replace('$(FeatureBranchVersionSuffix)', '[^\w-]', '-')) + $(VersionSuffix)-$(BuildNumber) + +