From 93d20ec78c49b9adf190c76fe7b3d7e8036bc7b9 Mon Sep 17 00:00:00 2001 From: Jass Bagga Date: Wed, 10 Jan 2018 12:53:17 -0800 Subject: [PATCH] Revert Dispatcher changes (#508) Addresses aspnet/Home#2741 --- NuGetPackageVerifier.json | 3 +- Routing.sln | 105 -- .../Configs/CoreConfig.cs | 31 - .../DispatcherBenchmark.cs | 113 -- ...t.AspNetCore.Dispatcher.Performance.csproj | 24 - .../Program.cs | 16 - .../RoutingBenchmark.cs | 13 +- .../DispatcherSample/DispatcherSample.csproj | 20 - .../IAuthorizationPolicyMetadata.cs | 20 - .../DispatcherSample/ICorsPolicyMetadata.cs | 20 - samples/DispatcherSample/Program.cs | 24 - samples/DispatcherSample/Startup.cs | 140 -- ...ionTree.Sources.2.1.0-preview1-t000.nuspec | 33 - .../project.assets.json | 52 - .../sharedsources.csproj.nuget.g.props | 15 - .../sharedsources.csproj.nuget.g.targets | 6 - .../TreeEnumerator.cs | 119 -- .../UrlMatchingTree.cs | 235 --- .../Address.cs | 15 - .../DispatcherValueCollection.cs | 790 ---------- .../Endpoint.cs | 15 - .../IDispatcherFeature.cs | 17 - .../MetadataCollection.cs | 104 -- ....AspNetCore.Dispatcher.Abstractions.csproj | 16 - .../Properties/AssemblyInfo.cs | 6 - .../Properties/Resources.Designer.cs | 58 - .../Resources.resx | 126 -- .../Template.cs | 27 - .../TemplateFactory.cs | 15 - .../baseline.netcore.json | 2 - .../AddressTable.cs | 12 - .../AmbiguousEndpointException.cs | 25 - .../DispatcherApplicationBuilderExtensions.cs | 16 - .../CompositeDispatcherDataSource.cs | 57 - .../CompositeHandlerFactory.cs | 44 - .../ConstraintPurpose.cs | 21 - .../AlphaDispatcherValueConstraint.cs | 18 - .../CompositeDispatcherValueConstraint.cs | 52 - .../Constraints/DefaultConstraintFactory.cs | 152 -- .../DispatcherValueConstraintBuilder.cs | 189 --- .../DispatcherValueConstraintContext.cs | 109 -- .../Constraints/IConstraintFactory.cs | 18 - .../Constraints/IDispatcherValueConstraint.cs | 21 - .../IntDispatcherValueConstraint.cs | 36 - .../OptionalDispatcherValueConstraint.cs | 42 - .../RegexDispatcherValueConstraint.cs | 58 - .../RegexStringDispatcherValueConstraint.cs | 20 - .../DefaultAddressTable.cs | 33 - .../DefaultDispatcherDataSource.cs | 31 - .../DefaultTemplateFactory.cs | 49 - .../DefaultDispatcherConfigureOptions.cs | 48 - .../DispatcherServiceCollectionExtensions.cs | 46 - .../DispatcherDataSource.cs | 21 - .../DispatcherEndpointStartupFilter.cs | 24 - .../DispatcherFeature.cs | 17 - .../DispatcherMiddleware.cs | 83 -- .../DispatcherOptions.cs | 45 - .../EndpointMiddleware.cs | 62 - .../EndpointOrderMetadata.cs | 15 - .../EndpointSelector.cs | 16 - .../EndpointSelectorContext.cs | 98 -- .../HandlerFactory.cs | 42 - .../HttpMethodEndpointSelector.cs | 105 -- .../IAddressCollectionProvider.cs | 15 - .../IDefaultMatcherFactory.cs | 12 - .../IEndpointCollectionProvider.cs | 15 - .../IEndpointOrderMetadata.cs | 10 - .../IHandlerFactory.cs | 27 - .../IMatcher.cs | 43 - .../IRoutePatternAddress.cs | 12 - .../IRoutePatternEndpoint.cs | 14 - .../ITemplateFactoryComponent.cs | 9 - .../LoggerExtensions.cs | 197 --- .../MatcherBase.cs | 209 --- .../MatcherCollection.cs | 28 - .../MatcherContext.cs | 48 - .../MatcherEntry.cs | 16 - .../Microsoft.AspNetCore.Dispatcher.csproj | 29 - .../Patterns/ConstraintReference.cs | 64 - .../Patterns/InlineRouteParameterParser.cs | 232 --- .../Patterns/RoutePattern.cs | 71 - .../Patterns/RoutePatternBuilder.cs | 76 - .../Patterns/RoutePatternException.cs | 28 - .../Patterns/RoutePatternLiteral.cs | 32 - .../Patterns/RoutePatternMatcher.cs | 511 ------- .../Patterns/RoutePatternParameter.cs | 51 - .../Patterns/RoutePatternParameterKind.cs | 12 - .../Patterns/RoutePatternParser.cs | 579 -------- .../Patterns/RoutePatternPart.cs | 235 --- .../Patterns/RoutePatternPartKind.cs | 12 - .../Patterns/RoutePatternPathSegment.cs | 35 - .../Patterns/RoutePatternSeparator.cs | 32 - .../Properties/AssemblyInfo.cs | 6 - .../Properties/Resources.Designer.cs | 338 ----- .../Resources.resx | 187 --- .../RoutePatternAddress.cs | 41 - .../RoutePatternAddressSelector.cs | 90 -- .../RoutePatternBinder.cs | 439 ------ .../RoutePatternBinderFactory.cs | 96 -- .../RoutePatternEndpoint.cs | 125 -- .../RoutePatternEndpointHandlerFactory.cs | 26 - .../RoutePatternTemplate.cs | 66 - .../RoutePatternTemplateFactory.cs | 51 - .../RoutePrecedence.cs | 77 - .../TemplateFactoryOfT.cs | 10 - .../Tree/TreeMatcher.cs | 293 ---- .../Tree/TreeMatcherFactory.cs | 31 - .../baseline.netcore.json | 2 - ...oft.AspNetCore.Routing.Abstractions.csproj | 5 +- .../Properties/Resources.Designer.cs | 28 + .../Resources.resx | 6 + .../RouteValueDictionary.cs | 762 +++++++++- .../RoutingServiceCollectionExtensions.cs | 12 +- .../DispatcherValueCollectionExtensions.cs | 21 - .../InlineRouteParameterParser.cs | 233 ++- .../Internal}/BufferValue.cs | 4 +- .../Internal/PathTokenizer.cs | 2 +- .../Internal}/SegmentState.cs | 4 +- .../UriBuilderContextPooledObjectPolicy.cs | 35 + .../Internal}/UriBuildingContext.cs | 20 +- .../Logging/TreeRouterLoggerExtensions.cs | 13 - .../Microsoft.AspNetCore.Routing.csproj | 9 +- .../Properties/Resources.Designer.cs | 238 ++- .../Resources.resx | 105 +- src/Microsoft.AspNetCore.Routing/RouteBase.cs | 6 +- .../Template/InlineConstraint.cs | 11 - .../Template/RouteTemplate.cs | 66 - .../Template/TemplateBinder.cs | 400 ++++- .../Template/TemplateMatcher.cs | 390 ++++- .../Template/TemplateParser.cs | 513 ++++++- .../Template/TemplatePart.cs | 35 - .../Template/TemplateSegment.cs | 10 - .../Tree}/InboundMatch.cs | 39 +- .../Tree}/InboundRouteEntry.cs | 83 +- .../Tree/TreeRouteBuilder.cs | 203 ++- .../Tree/TreeRouter.cs | 137 +- .../Tree}/UrlMatchingNode.cs | 55 +- .../Tree/UrlMatchingTree.cs | 30 + .../breakingchanges.netcore.json | 17 - .../MetadataCollectionTest.cs | 79 - ...etCore.Dispatcher.Abstractions.Test.csproj | 11 - .../ApiAppFixture.cs | 34 - .../ApiAppStartup.cs | 96 -- .../ApiAppTest.cs | 102 -- ...spNetCore.Dispatcher.FunctionalTest.csproj | 15 - .../CompositeDispatcherValueConstraintTest.cs | 60 - .../RegexDispatcherValueConstraintTest.cs | 120 -- .../DefaultTemplateFactoryTest.cs | 64 - .../HttpMethodEndpointSelectorTest.cs | 108 -- ...icrosoft.AspNetCore.Dispatcher.Test.csproj | 19 - .../InlineRouteParameterParserTest.cs | 950 ------------ .../Patterns/RoutePatternMatcherTest.cs | 1131 --------------- .../Patterns/RoutePatternParserTest.cs | 785 ---------- .../RoutePatternBinderTest.cs | 1290 ----------------- .../RoutePatternTemplateTest.cs | 66 - .../Tree/TreeMatcherTest.cs | 904 ------------ .../RouteValueDictionaryTests.cs} | 366 ++--- .../DateTimeRouteConstraintTests.cs | 43 +- .../InlineRouteParameterParserTests.cs | 3 +- .../Internal/PathTokenizerTest.cs | 2 +- .../RouteCollectionTest.cs | 2 - .../RouteTest.cs | 1 - .../Template/RouteTemplateTest.cs | 216 --- .../Template/TemplateBinderTests.cs | 44 +- .../Template/TemplateParserTests.cs | 24 +- .../Tree/TreeRouteBuilderTest.cs | 9 +- .../Tree/TreeRouterTest.cs | 26 +- 167 files changed, 3287 insertions(+), 15112 deletions(-) delete mode 100644 benchmarks/Microsoft.AspNetCore.Dispatcher.Performance/Configs/CoreConfig.cs delete mode 100644 benchmarks/Microsoft.AspNetCore.Dispatcher.Performance/DispatcherBenchmark.cs delete mode 100644 benchmarks/Microsoft.AspNetCore.Dispatcher.Performance/Microsoft.AspNetCore.Dispatcher.Performance.csproj delete mode 100644 benchmarks/Microsoft.AspNetCore.Dispatcher.Performance/Program.cs delete mode 100644 samples/DispatcherSample/DispatcherSample.csproj delete mode 100644 samples/DispatcherSample/IAuthorizationPolicyMetadata.cs delete mode 100644 samples/DispatcherSample/ICorsPolicyMetadata.cs delete mode 100644 samples/DispatcherSample/Program.cs delete mode 100644 samples/DispatcherSample/Startup.cs delete mode 100644 shared/Microsoft.AspNetCore.Routing.DecisionTree.Sourcesobj/Microsoft.AspNetCore.Routing.DecisionTree.Sources.2.1.0-preview1-t000.nuspec delete mode 100644 shared/Microsoft.AspNetCore.Routing.DecisionTree.Sourcesobj/project.assets.json delete mode 100644 shared/Microsoft.AspNetCore.Routing.DecisionTree.Sourcesobj/sharedsources.csproj.nuget.g.props delete mode 100644 shared/Microsoft.AspNetCore.Routing.DecisionTree.Sourcesobj/sharedsources.csproj.nuget.g.targets delete mode 100644 shared/Microsoft.AspNetCore.Routing.UrlMatchingTree.Sources/TreeEnumerator.cs delete mode 100644 shared/Microsoft.AspNetCore.Routing.UrlMatchingTree.Sources/UrlMatchingTree.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher.Abstractions/Address.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher.Abstractions/DispatcherValueCollection.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher.Abstractions/Endpoint.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher.Abstractions/IDispatcherFeature.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher.Abstractions/MetadataCollection.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher.Abstractions/Microsoft.AspNetCore.Dispatcher.Abstractions.csproj delete mode 100644 src/Microsoft.AspNetCore.Dispatcher.Abstractions/Properties/AssemblyInfo.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher.Abstractions/Properties/Resources.Designer.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher.Abstractions/Resources.resx delete mode 100644 src/Microsoft.AspNetCore.Dispatcher.Abstractions/Template.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher.Abstractions/TemplateFactory.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher.Abstractions/baseline.netcore.json delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/AddressTable.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/AmbiguousEndpointException.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/Builder/DispatcherApplicationBuilderExtensions.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/CompositeDispatcherDataSource.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/CompositeHandlerFactory.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/ConstraintPurpose.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/Constraints/AlphaDispatcherValueConstraint.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/Constraints/CompositeDispatcherValueConstraint.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/Constraints/DefaultConstraintFactory.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/Constraints/DispatcherValueConstraintBuilder.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/Constraints/DispatcherValueConstraintContext.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/Constraints/IConstraintFactory.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/Constraints/IDispatcherValueConstraint.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/Constraints/IntDispatcherValueConstraint.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/Constraints/OptionalDispatcherValueConstraint.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/Constraints/RegexDispatcherValueConstraint.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/Constraints/RegexStringDispatcherValueConstraint.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/DefaultAddressTable.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/DefaultDispatcherDataSource.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/DefaultTemplateFactory.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/DependencyInjection/DefaultDispatcherConfigureOptions.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/DependencyInjection/DispatcherServiceCollectionExtensions.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/DispatcherDataSource.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/DispatcherEndpointStartupFilter.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/DispatcherFeature.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/DispatcherMiddleware.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/DispatcherOptions.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/EndpointMiddleware.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/EndpointOrderMetadata.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/EndpointSelector.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/EndpointSelectorContext.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/HandlerFactory.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/HttpMethodEndpointSelector.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/IAddressCollectionProvider.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/IDefaultMatcherFactory.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/IEndpointCollectionProvider.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/IEndpointOrderMetadata.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/IHandlerFactory.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/IMatcher.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/IRoutePatternAddress.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/IRoutePatternEndpoint.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/ITemplateFactoryComponent.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/LoggerExtensions.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/MatcherBase.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/MatcherCollection.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/MatcherContext.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/MatcherEntry.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/Microsoft.AspNetCore.Dispatcher.csproj delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/Patterns/ConstraintReference.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/Patterns/InlineRouteParameterParser.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePattern.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternBuilder.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternException.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternLiteral.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternMatcher.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternParameter.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternParameterKind.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternParser.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternPart.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternPartKind.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternPathSegment.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternSeparator.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/Properties/AssemblyInfo.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/Properties/Resources.Designer.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/Resources.resx delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/RoutePatternAddress.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/RoutePatternAddressSelector.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/RoutePatternBinder.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/RoutePatternBinderFactory.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/RoutePatternEndpoint.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/RoutePatternEndpointHandlerFactory.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/RoutePatternTemplate.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/RoutePatternTemplateFactory.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/RoutePrecedence.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/TemplateFactoryOfT.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/Tree/TreeMatcher.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/Tree/TreeMatcherFactory.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/baseline.netcore.json delete mode 100644 src/Microsoft.AspNetCore.Routing/Dispatcher/DispatcherValueCollectionExtensions.cs rename src/{Microsoft.AspNetCore.Dispatcher => Microsoft.AspNetCore.Routing/Internal}/BufferValue.cs (84%) rename src/{Microsoft.AspNetCore.Dispatcher => Microsoft.AspNetCore.Routing}/Internal/PathTokenizer.cs (99%) rename src/{Microsoft.AspNetCore.Dispatcher => Microsoft.AspNetCore.Routing/Internal}/SegmentState.cs (89%) create mode 100644 src/Microsoft.AspNetCore.Routing/Internal/UriBuilderContextPooledObjectPolicy.cs rename src/{Microsoft.AspNetCore.Dispatcher => Microsoft.AspNetCore.Routing/Internal}/UriBuildingContext.cs (91%) rename {shared/Microsoft.AspNetCore.Routing.UrlMatchingTree.Sources => src/Microsoft.AspNetCore.Routing/Tree}/InboundMatch.cs (57%) rename {shared/Microsoft.AspNetCore.Routing.UrlMatchingTree.Sources => src/Microsoft.AspNetCore.Routing/Tree}/InboundRouteEntry.cs (56%) rename {shared/Microsoft.AspNetCore.Routing.UrlMatchingTree.Sources => src/Microsoft.AspNetCore.Routing/Tree}/UrlMatchingNode.cs (71%) create mode 100644 src/Microsoft.AspNetCore.Routing/Tree/UrlMatchingTree.cs delete mode 100644 src/Microsoft.AspNetCore.Routing/breakingchanges.netcore.json delete mode 100644 test/Microsoft.AspNetCore.Dispatcher.Abstractions.Test/MetadataCollectionTest.cs delete mode 100644 test/Microsoft.AspNetCore.Dispatcher.Abstractions.Test/Microsoft.AspNetCore.Dispatcher.Abstractions.Test.csproj delete mode 100644 test/Microsoft.AspNetCore.Dispatcher.FunctionalTest/ApiAppFixture.cs delete mode 100644 test/Microsoft.AspNetCore.Dispatcher.FunctionalTest/ApiAppStartup.cs delete mode 100644 test/Microsoft.AspNetCore.Dispatcher.FunctionalTest/ApiAppTest.cs delete mode 100644 test/Microsoft.AspNetCore.Dispatcher.FunctionalTest/Microsoft.AspNetCore.Dispatcher.FunctionalTest.csproj delete mode 100644 test/Microsoft.AspNetCore.Dispatcher.Test/Constraints/CompositeDispatcherValueConstraintTest.cs delete mode 100644 test/Microsoft.AspNetCore.Dispatcher.Test/Constraints/RegexDispatcherValueConstraintTest.cs delete mode 100644 test/Microsoft.AspNetCore.Dispatcher.Test/DefaultTemplateFactoryTest.cs delete mode 100644 test/Microsoft.AspNetCore.Dispatcher.Test/HttpMethodEndpointSelectorTest.cs delete mode 100644 test/Microsoft.AspNetCore.Dispatcher.Test/Microsoft.AspNetCore.Dispatcher.Test.csproj delete mode 100644 test/Microsoft.AspNetCore.Dispatcher.Test/Patterns/InlineRouteParameterParserTest.cs delete mode 100644 test/Microsoft.AspNetCore.Dispatcher.Test/Patterns/RoutePatternMatcherTest.cs delete mode 100644 test/Microsoft.AspNetCore.Dispatcher.Test/Patterns/RoutePatternParserTest.cs delete mode 100644 test/Microsoft.AspNetCore.Dispatcher.Test/RoutePatternBinderTest.cs delete mode 100644 test/Microsoft.AspNetCore.Dispatcher.Test/RoutePatternTemplateTest.cs delete mode 100644 test/Microsoft.AspNetCore.Dispatcher.Test/Tree/TreeMatcherTest.cs rename test/{Microsoft.AspNetCore.Dispatcher.Abstractions.Test/DispatcherValueCollectionTest.cs => Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests/RouteValueDictionaryTests.cs} (72%) rename test/{Microsoft.AspNetCore.Dispatcher.Test => Microsoft.AspNetCore.Routing.Tests}/Internal/PathTokenizerTest.cs (98%) delete mode 100644 test/Microsoft.AspNetCore.Routing.Tests/Template/RouteTemplateTest.cs diff --git a/NuGetPackageVerifier.json b/NuGetPackageVerifier.json index fab2174a91..9ed455034c 100644 --- a/NuGetPackageVerifier.json +++ b/NuGetPackageVerifier.json @@ -2,8 +2,7 @@ "adx-nonshipping": { "rules": [], "packages": { - "Microsoft.AspNetCore.Routing.DecisionTree.Sources": {}, - "Microsoft.AspNetCore.Routing.UrlMatchingTree.Sources": {} + "Microsoft.AspNetCore.Routing.DecisionTree.Sources": {} } }, "Default": { diff --git a/Routing.sln b/Routing.sln index 32219e6abc..13657187fa 100644 --- a/Routing.sln +++ b/Routing.sln @@ -45,20 +45,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Routin EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "benchmarks", "benchmarks", "{D5F39F59-5725-4127-82E7-67028D006185}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Dispatcher", "src\Microsoft.AspNetCore.Dispatcher\Microsoft.AspNetCore.Dispatcher.csproj", "{3FEBCDA2-0381-47B8-A400-4A998D62F86F}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Dispatcher.Abstractions", "src\Microsoft.AspNetCore.Dispatcher.Abstractions\Microsoft.AspNetCore.Dispatcher.Abstractions.csproj", "{3153A4B2-BF6B-44EB-8113-F425F07F86E6}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Dispatcher.Test", "test\Microsoft.AspNetCore.Dispatcher.Test\Microsoft.AspNetCore.Dispatcher.Test.csproj", "{DB2ABDCA-639B-4E0D-B64F-5F6A98A9EC92}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Dispatcher.Abstractions.Test", "test\Microsoft.AspNetCore.Dispatcher.Abstractions.Test\Microsoft.AspNetCore.Dispatcher.Abstractions.Test.csproj", "{14ACBCB4-3B99-425F-A5E2-07E228DEBF63}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DispatcherSample", "samples\DispatcherSample\DispatcherSample.csproj", "{6EBC8AE2-CFF7-46E1-8427-9111FD4F3E85}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Dispatcher.Performance", "benchmarks\Microsoft.AspNetCore.Dispatcher.Performance\Microsoft.AspNetCore.Dispatcher.Performance.csproj", "{30AF355D-E3AB-4FF5-8A59-A253AFEBA26A}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Dispatcher.FunctionalTest", "test\Microsoft.AspNetCore.Dispatcher.FunctionalTest\Microsoft.AspNetCore.Dispatcher.FunctionalTest.csproj", "{32107601-C9BE-467B-894C-C9F2E35F03E4}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -159,90 +145,6 @@ Global {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 - {3FEBCDA2-0381-47B8-A400-4A998D62F86F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3FEBCDA2-0381-47B8-A400-4A998D62F86F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3FEBCDA2-0381-47B8-A400-4A998D62F86F}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {3FEBCDA2-0381-47B8-A400-4A998D62F86F}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {3FEBCDA2-0381-47B8-A400-4A998D62F86F}.Debug|x86.ActiveCfg = Debug|Any CPU - {3FEBCDA2-0381-47B8-A400-4A998D62F86F}.Debug|x86.Build.0 = Debug|Any CPU - {3FEBCDA2-0381-47B8-A400-4A998D62F86F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3FEBCDA2-0381-47B8-A400-4A998D62F86F}.Release|Any CPU.Build.0 = Release|Any CPU - {3FEBCDA2-0381-47B8-A400-4A998D62F86F}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {3FEBCDA2-0381-47B8-A400-4A998D62F86F}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {3FEBCDA2-0381-47B8-A400-4A998D62F86F}.Release|x86.ActiveCfg = Release|Any CPU - {3FEBCDA2-0381-47B8-A400-4A998D62F86F}.Release|x86.Build.0 = Release|Any CPU - {3153A4B2-BF6B-44EB-8113-F425F07F86E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3153A4B2-BF6B-44EB-8113-F425F07F86E6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3153A4B2-BF6B-44EB-8113-F425F07F86E6}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {3153A4B2-BF6B-44EB-8113-F425F07F86E6}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {3153A4B2-BF6B-44EB-8113-F425F07F86E6}.Debug|x86.ActiveCfg = Debug|Any CPU - {3153A4B2-BF6B-44EB-8113-F425F07F86E6}.Debug|x86.Build.0 = Debug|Any CPU - {3153A4B2-BF6B-44EB-8113-F425F07F86E6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3153A4B2-BF6B-44EB-8113-F425F07F86E6}.Release|Any CPU.Build.0 = Release|Any CPU - {3153A4B2-BF6B-44EB-8113-F425F07F86E6}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {3153A4B2-BF6B-44EB-8113-F425F07F86E6}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {3153A4B2-BF6B-44EB-8113-F425F07F86E6}.Release|x86.ActiveCfg = Release|Any CPU - {3153A4B2-BF6B-44EB-8113-F425F07F86E6}.Release|x86.Build.0 = Release|Any CPU - {DB2ABDCA-639B-4E0D-B64F-5F6A98A9EC92}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DB2ABDCA-639B-4E0D-B64F-5F6A98A9EC92}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DB2ABDCA-639B-4E0D-B64F-5F6A98A9EC92}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {DB2ABDCA-639B-4E0D-B64F-5F6A98A9EC92}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {DB2ABDCA-639B-4E0D-B64F-5F6A98A9EC92}.Debug|x86.ActiveCfg = Debug|Any CPU - {DB2ABDCA-639B-4E0D-B64F-5F6A98A9EC92}.Debug|x86.Build.0 = Debug|Any CPU - {DB2ABDCA-639B-4E0D-B64F-5F6A98A9EC92}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DB2ABDCA-639B-4E0D-B64F-5F6A98A9EC92}.Release|Any CPU.Build.0 = Release|Any CPU - {DB2ABDCA-639B-4E0D-B64F-5F6A98A9EC92}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {DB2ABDCA-639B-4E0D-B64F-5F6A98A9EC92}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {DB2ABDCA-639B-4E0D-B64F-5F6A98A9EC92}.Release|x86.ActiveCfg = Release|Any CPU - {DB2ABDCA-639B-4E0D-B64F-5F6A98A9EC92}.Release|x86.Build.0 = Release|Any CPU - {14ACBCB4-3B99-425F-A5E2-07E228DEBF63}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {14ACBCB4-3B99-425F-A5E2-07E228DEBF63}.Debug|Any CPU.Build.0 = Debug|Any CPU - {14ACBCB4-3B99-425F-A5E2-07E228DEBF63}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {14ACBCB4-3B99-425F-A5E2-07E228DEBF63}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {14ACBCB4-3B99-425F-A5E2-07E228DEBF63}.Debug|x86.ActiveCfg = Debug|Any CPU - {14ACBCB4-3B99-425F-A5E2-07E228DEBF63}.Debug|x86.Build.0 = Debug|Any CPU - {14ACBCB4-3B99-425F-A5E2-07E228DEBF63}.Release|Any CPU.ActiveCfg = Release|Any CPU - {14ACBCB4-3B99-425F-A5E2-07E228DEBF63}.Release|Any CPU.Build.0 = Release|Any CPU - {14ACBCB4-3B99-425F-A5E2-07E228DEBF63}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {14ACBCB4-3B99-425F-A5E2-07E228DEBF63}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {14ACBCB4-3B99-425F-A5E2-07E228DEBF63}.Release|x86.ActiveCfg = Release|Any CPU - {14ACBCB4-3B99-425F-A5E2-07E228DEBF63}.Release|x86.Build.0 = Release|Any CPU - {6EBC8AE2-CFF7-46E1-8427-9111FD4F3E85}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6EBC8AE2-CFF7-46E1-8427-9111FD4F3E85}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6EBC8AE2-CFF7-46E1-8427-9111FD4F3E85}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {6EBC8AE2-CFF7-46E1-8427-9111FD4F3E85}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {6EBC8AE2-CFF7-46E1-8427-9111FD4F3E85}.Debug|x86.ActiveCfg = Debug|Any CPU - {6EBC8AE2-CFF7-46E1-8427-9111FD4F3E85}.Debug|x86.Build.0 = Debug|Any CPU - {6EBC8AE2-CFF7-46E1-8427-9111FD4F3E85}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6EBC8AE2-CFF7-46E1-8427-9111FD4F3E85}.Release|Any CPU.Build.0 = Release|Any CPU - {6EBC8AE2-CFF7-46E1-8427-9111FD4F3E85}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {6EBC8AE2-CFF7-46E1-8427-9111FD4F3E85}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {6EBC8AE2-CFF7-46E1-8427-9111FD4F3E85}.Release|x86.ActiveCfg = Release|Any CPU - {6EBC8AE2-CFF7-46E1-8427-9111FD4F3E85}.Release|x86.Build.0 = Release|Any CPU - {30AF355D-E3AB-4FF5-8A59-A253AFEBA26A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {30AF355D-E3AB-4FF5-8A59-A253AFEBA26A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {30AF355D-E3AB-4FF5-8A59-A253AFEBA26A}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {30AF355D-E3AB-4FF5-8A59-A253AFEBA26A}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {30AF355D-E3AB-4FF5-8A59-A253AFEBA26A}.Debug|x86.ActiveCfg = Debug|Any CPU - {30AF355D-E3AB-4FF5-8A59-A253AFEBA26A}.Debug|x86.Build.0 = Debug|Any CPU - {30AF355D-E3AB-4FF5-8A59-A253AFEBA26A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {30AF355D-E3AB-4FF5-8A59-A253AFEBA26A}.Release|Any CPU.Build.0 = Release|Any CPU - {30AF355D-E3AB-4FF5-8A59-A253AFEBA26A}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {30AF355D-E3AB-4FF5-8A59-A253AFEBA26A}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {30AF355D-E3AB-4FF5-8A59-A253AFEBA26A}.Release|x86.ActiveCfg = Release|Any CPU - {30AF355D-E3AB-4FF5-8A59-A253AFEBA26A}.Release|x86.Build.0 = Release|Any CPU - {32107601-C9BE-467B-894C-C9F2E35F03E4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {32107601-C9BE-467B-894C-C9F2E35F03E4}.Debug|Any CPU.Build.0 = Debug|Any CPU - {32107601-C9BE-467B-894C-C9F2E35F03E4}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {32107601-C9BE-467B-894C-C9F2E35F03E4}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {32107601-C9BE-467B-894C-C9F2E35F03E4}.Debug|x86.ActiveCfg = Debug|Any CPU - {32107601-C9BE-467B-894C-C9F2E35F03E4}.Debug|x86.Build.0 = Debug|Any CPU - {32107601-C9BE-467B-894C-C9F2E35F03E4}.Release|Any CPU.ActiveCfg = Release|Any CPU - {32107601-C9BE-467B-894C-C9F2E35F03E4}.Release|Any CPU.Build.0 = Release|Any CPU - {32107601-C9BE-467B-894C-C9F2E35F03E4}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {32107601-C9BE-467B-894C-C9F2E35F03E4}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {32107601-C9BE-467B-894C-C9F2E35F03E4}.Release|x86.ActiveCfg = Release|Any CPU - {32107601-C9BE-467B-894C-C9F2E35F03E4}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -256,13 +158,6 @@ Global {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} - {3FEBCDA2-0381-47B8-A400-4A998D62F86F} = {0E966C37-7334-4D96-AAF6-9F49FBD166E3} - {3153A4B2-BF6B-44EB-8113-F425F07F86E6} = {0E966C37-7334-4D96-AAF6-9F49FBD166E3} - {DB2ABDCA-639B-4E0D-B64F-5F6A98A9EC92} = {95359B4B-4C85-4B44-A75B-0621905C4CF6} - {14ACBCB4-3B99-425F-A5E2-07E228DEBF63} = {95359B4B-4C85-4B44-A75B-0621905C4CF6} - {6EBC8AE2-CFF7-46E1-8427-9111FD4F3E85} = {C3ADD55B-B9C7-4061-8AD4-6A70D1AE3B2E} - {30AF355D-E3AB-4FF5-8A59-A253AFEBA26A} = {D5F39F59-5725-4127-82E7-67028D006185} - {32107601-C9BE-467B-894C-C9F2E35F03E4} = {95359B4B-4C85-4B44-A75B-0621905C4CF6} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {36C8D815-B7F1-479D-894B-E606FB8DECDA} diff --git a/benchmarks/Microsoft.AspNetCore.Dispatcher.Performance/Configs/CoreConfig.cs b/benchmarks/Microsoft.AspNetCore.Dispatcher.Performance/Configs/CoreConfig.cs deleted file mode 100644 index 7be4184d9d..0000000000 --- a/benchmarks/Microsoft.AspNetCore.Dispatcher.Performance/Configs/CoreConfig.cs +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using BenchmarkDotNet.Columns; -using BenchmarkDotNet.Configs; -using BenchmarkDotNet.Diagnosers; -using BenchmarkDotNet.Engines; -using BenchmarkDotNet.Jobs; -using BenchmarkDotNet.Validators; - -namespace Microsoft.AspNetCore.Dispatcher.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/benchmarks/Microsoft.AspNetCore.Dispatcher.Performance/DispatcherBenchmark.cs b/benchmarks/Microsoft.AspNetCore.Dispatcher.Performance/DispatcherBenchmark.cs deleted file mode 100644 index befa1725ec..0000000000 --- a/benchmarks/Microsoft.AspNetCore.Dispatcher.Performance/DispatcherBenchmark.cs +++ /dev/null @@ -1,113 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using BenchmarkDotNet.Attributes; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Routing; - -namespace Microsoft.AspNetCore.Dispatcher.Performance -{ - public class DispatcherBenchmark - { - private const int NumberOfRequestTypes = 3; - private const int Iterations = 100; - - private readonly IMatcher _treeMatcher; - private readonly RequestEntry[] _requests; - - public DispatcherBenchmark() - { - var dataSource = new DefaultDispatcherDataSource() - { - Endpoints = - { - new RoutePatternEndpoint("api/Widgets", Benchmark_Delegate), - new RoutePatternEndpoint("api/Widgets/{id}", Benchmark_Delegate), - new RoutePatternEndpoint("api/Widgets/search/{term}", Benchmark_Delegate), - new RoutePatternEndpoint("admin/users/{id}", Benchmark_Delegate), - new RoutePatternEndpoint("admin/users/{id}/manage", Benchmark_Delegate), - }, - }; - - var factory = new TreeMatcherFactory(); - _treeMatcher = factory.CreateMatcher(dataSource, new List()); - - _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 MatcherContext(_requests[j].HttpContext); - - await _treeMatcher.MatchAsync(context); - - Verify(context, j); - } - } - } - - private void Verify(MatcherContext context, int i) - { - if (_requests[i].IsMatch) - { - if (context.Endpoint == null) - { - throw new InvalidOperationException($"Failed {i}"); - } - - var values = _requests[i].Values; - if (values.Count != context.Values.Count) - { - throw new InvalidOperationException($"Failed {i}"); - } - } - else - { - if (context.Endpoint != null) - { - throw new InvalidOperationException($"Failed {i}"); - } - - if (context.Values.Count != 0) - { - throw new InvalidOperationException($"Failed {i}"); - } - } - } - - private struct RequestEntry - { - public HttpContext HttpContext; - public bool IsMatch; - public RouteValueDictionary Values; - } - - private static Task Benchmark_Delegate(HttpContext httpContext) - { - return Task.CompletedTask; - } - } -} diff --git a/benchmarks/Microsoft.AspNetCore.Dispatcher.Performance/Microsoft.AspNetCore.Dispatcher.Performance.csproj b/benchmarks/Microsoft.AspNetCore.Dispatcher.Performance/Microsoft.AspNetCore.Dispatcher.Performance.csproj deleted file mode 100644 index 467863d91c..0000000000 --- a/benchmarks/Microsoft.AspNetCore.Dispatcher.Performance/Microsoft.AspNetCore.Dispatcher.Performance.csproj +++ /dev/null @@ -1,24 +0,0 @@ - - - - netcoreapp2.0;net461 - netcoreapp2.0 - Exe - true - true - false - - - - - - - - - - - - - - - diff --git a/benchmarks/Microsoft.AspNetCore.Dispatcher.Performance/Program.cs b/benchmarks/Microsoft.AspNetCore.Dispatcher.Performance/Program.cs deleted file mode 100644 index 29cf91909d..0000000000 --- a/benchmarks/Microsoft.AspNetCore.Dispatcher.Performance/Program.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Reflection; -using BenchmarkDotNet.Running; - -namespace Microsoft.AspNetCore.Dispatcher.Performance -{ - public class Program - { - public static void Main(string[] args) - { - BenchmarkSwitcher.FromAssembly(typeof(Program).GetTypeInfo().Assembly).Run(args); - } - } -} diff --git a/benchmarks/Microsoft.AspNetCore.Routing.Performance/RoutingBenchmark.cs b/benchmarks/Microsoft.AspNetCore.Routing.Performance/RoutingBenchmark.cs index fedeb5b2e4..303fb5e811 100644 --- a/benchmarks/Microsoft.AspNetCore.Routing.Performance/RoutingBenchmark.cs +++ b/benchmarks/Microsoft.AspNetCore.Routing.Performance/RoutingBenchmark.cs @@ -2,16 +2,20 @@ // 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.Dispatcher; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Internal; using Microsoft.AspNetCore.Routing.Template; using Microsoft.AspNetCore.Routing.Tree; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.ObjectPool; using Microsoft.Extensions.Options; +using System.Diagnostics; namespace Microsoft.AspNetCore.Routing.Performance { @@ -26,11 +30,12 @@ namespace Microsoft.AspNetCore.Routing.Performance public RoutingBenchmark() { var handler = new RouteHandler((next) => Task.FromResult(null)); - + var treeBuilder = new TreeRouteBuilder( NullLoggerFactory.Instance, - new RoutePatternBinderFactory(UrlEncoder.Default, new DefaultObjectPoolProvider()), - new DefaultInlineConstraintResolver(Options.Create(new RouteOptions()))); + UrlEncoder.Default, + new DefaultObjectPool(new UriBuilderContextPooledObjectPolicy(UrlEncoder.Default)), + 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); diff --git a/samples/DispatcherSample/DispatcherSample.csproj b/samples/DispatcherSample/DispatcherSample.csproj deleted file mode 100644 index 09d71085cb..0000000000 --- a/samples/DispatcherSample/DispatcherSample.csproj +++ /dev/null @@ -1,20 +0,0 @@ - - - - netcoreapp2.1;netcoreapp2.0 - $(TargetFrameworks);net461 - - - - - - - - - - - - - - - diff --git a/samples/DispatcherSample/IAuthorizationPolicyMetadata.cs b/samples/DispatcherSample/IAuthorizationPolicyMetadata.cs deleted file mode 100644 index a758f06f10..0000000000 --- a/samples/DispatcherSample/IAuthorizationPolicyMetadata.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -namespace DispatcherSample -{ - public interface IAuthorizationPolicyMetadata - { - string Name { get; } - } - - public class AuthorizationPolicyMetadata : IAuthorizationPolicyMetadata - { - public AuthorizationPolicyMetadata(string name) - { - Name = name; - } - - public string Name { get; } - } -} diff --git a/samples/DispatcherSample/ICorsPolicyMetadata.cs b/samples/DispatcherSample/ICorsPolicyMetadata.cs deleted file mode 100644 index d2d54dc4f5..0000000000 --- a/samples/DispatcherSample/ICorsPolicyMetadata.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -namespace DispatcherSample -{ - public interface ICorsPolicyMetadata - { - string Name { get; } - } - - public class CorsPolicyMetadata : ICorsPolicyMetadata - { - public CorsPolicyMetadata(string name) - { - Name = name; - } - - public string Name { get; } - } -} diff --git a/samples/DispatcherSample/Program.cs b/samples/DispatcherSample/Program.cs deleted file mode 100644 index 70b401a66b..0000000000 --- a/samples/DispatcherSample/Program.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Console; - -namespace DispatcherSample -{ - public class Program - { - public static void Main(string[] args) - { - var host = new WebHostBuilder() - .UseIISIntegration() - .UseKestrel() - .UseStartup() - .ConfigureLogging((c, b) => b.AddProvider(new ConsoleLoggerProvider((category, level) => true, includeScopes: false))) - .Build(); - - host.Run(); - } - } -} diff --git a/samples/DispatcherSample/Startup.cs b/samples/DispatcherSample/Startup.cs deleted file mode 100644 index a9259cc14e..0000000000 --- a/samples/DispatcherSample/Startup.cs +++ /dev/null @@ -1,140 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Dispatcher; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -namespace DispatcherSample -{ - public class Startup - { - public void ConfigureServices(IServiceCollection services) - { - services.AddDispatcher(); - - // This is a temporary layering issue, don't worry about :) - services.AddRouting(); - services.AddSingleton(); - - // Imagine this was done by MVC or another framework. - services.AddSingleton(ConfigureDispatcher()); - services.AddSingleton(); - - } - - public DefaultDispatcherDataSource ConfigureDispatcher() - { - return new DefaultDispatcherDataSource() - { - Addresses = - { - new RoutePatternAddress("{id?}", new { controller = "Home", action = "Index", }, "Home:Index()"), - new RoutePatternAddress("Home/About/{id?}", new { controller = "Home", action = "About", }, "Home:About()"), - new RoutePatternAddress("Admin/Index/{id?}", new { controller = "Admin", action = "Index", }, "Admin:Index()"), - new RoutePatternAddress("Admin/Users/{id?}", new { controller = "Admin", action = "Users", }, "Admin:GetUsers()/Admin:EditUsers()"), - }, - Endpoints = - { - new RoutePatternEndpoint("{id?}", new { controller = "Home", action = "Index", }, Home_Index, "Home:Index()"), - new RoutePatternEndpoint("Home/{id?}", new { controller = "Home", action = "Index", }, Home_Index, "Home:Index()"), - new RoutePatternEndpoint("Home/Index/{id?}", new { controller = "Home", action = "Index", }, Home_Index, "Home:Index()"), - new RoutePatternEndpoint("Home/About/{id?}", new { controller = "Home", action = "About", }, Home_About, "Home:About()"), - new RoutePatternEndpoint("Admin/Index/{id?}", new { controller = "Admin", action = "Index", }, Admin_Index, "Admin:Index()"), - new RoutePatternEndpoint("Admin/Users/{id?}", new { controller = "Admin", action = "Users", }, "GET", Admin_GetUsers, "Admin:GetUsers()", new AuthorizationPolicyMetadata("Admin")), - new RoutePatternEndpoint("Admin/Users/{id?}", new { controller = "Admin", action = "Users", }, "POST", Admin_EditUsers, "Admin:EditUsers()", new AuthorizationPolicyMetadata("Admin")), - }, - }; - } - - public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILogger logger) - { - app.UseDispatcher(); - - app.Use(async (context, next) => - { - logger.LogInformation("Executing fake CORS middleware"); - - var feature = context.Features.Get(); - var policy = feature.Endpoint?.Metadata.GetMetadata(); - logger.LogInformation("using CORS policy {PolicyName}", policy?.Name ?? "default"); - - await next.Invoke(); - }); - - app.Use(async (context, next) => - { - logger.LogInformation("Executing fake AuthZ middleware"); - - var feature = context.Features.Get(); - var policy = feature.Endpoint?.Metadata.GetMetadata(); - if (policy != null) - { - logger.LogInformation("using Auth policy {PolicyName}", policy.Name); - } - - await next.Invoke(); - }); - } - - public static Task Home_Index(HttpContext httpContext) - { - var templateFactory = httpContext.RequestServices.GetRequiredService(); - - return httpContext.Response.WriteAsync( - $"" + - $"" + - $"

Some links you can visit

" + - $"

Home:Index()

" + - $"

Home:About()

" + - $"

Admin:Index()

" + - $"

Admin:GetUsers()/Admin:EditUsers()

" + - $"" + - $""); - } - - public static Task Home_About(HttpContext httpContext) - { - return httpContext.Response.WriteAsync( - $"" + - $"" + - $"

This is a dispatcher sample.

" + - $"" + - $""); - } - - public static Task Admin_Index(HttpContext httpContext) - { - return httpContext.Response.WriteAsync( - $"" + - $"" + - $"

This is the admin page.

" + - $"" + - $""); - } - - public static Task Admin_GetUsers(HttpContext httpContext) - { - return httpContext.Response.WriteAsync( - $"" + - $"" + - $"

Users: rynowak, jbagga

" + - $"" + - $""); - } - - public static Task Admin_EditUsers(HttpContext httpContext) - { - return httpContext.Response.WriteAsync( - $"" + - $"" + - $"

blerp

" + - $"" + - $""); - } - } -} diff --git a/shared/Microsoft.AspNetCore.Routing.DecisionTree.Sourcesobj/Microsoft.AspNetCore.Routing.DecisionTree.Sources.2.1.0-preview1-t000.nuspec b/shared/Microsoft.AspNetCore.Routing.DecisionTree.Sourcesobj/Microsoft.AspNetCore.Routing.DecisionTree.Sources.2.1.0-preview1-t000.nuspec deleted file mode 100644 index afa65da9fb..0000000000 --- a/shared/Microsoft.AspNetCore.Routing.DecisionTree.Sourcesobj/Microsoft.AspNetCore.Routing.DecisionTree.Sources.2.1.0-preview1-t000.nuspec +++ /dev/null @@ -1,33 +0,0 @@ - - - - Microsoft.AspNetCore.Routing.DecisionTree.Sources - 2.1.0-preview1-t000 - sharedsources - sharedsources - false - Microsoft.AspNetCore.Routing.DecisionTree.Sources - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/shared/Microsoft.AspNetCore.Routing.DecisionTree.Sourcesobj/project.assets.json b/shared/Microsoft.AspNetCore.Routing.DecisionTree.Sourcesobj/project.assets.json deleted file mode 100644 index b00adfda03..0000000000 --- a/shared/Microsoft.AspNetCore.Routing.DecisionTree.Sourcesobj/project.assets.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "version": 3, - "targets": { - ".NETStandard,Version=v1.0": {} - }, - "libraries": {}, - "projectFileDependencyGroups": { - ".NETStandard,Version=v1.0": [] - }, - "packageFolders": { - "C:\\Users\\rynowak\\.nuget\\packages\\": {} - }, - "project": { - "version": "2.1.0-preview1-t000", - "restore": { - "projectUniqueName": "C:\\Users\\rynowak\\.dotnet\\buildtools\\korebuild\\2.1.0-preview1-15509\\modules\\sharedsources\\sharedsources.csproj", - "projectName": "Microsoft.AspNetCore.Routing.DecisionTree.Sources", - "projectPath": "C:\\Users\\rynowak\\.dotnet\\buildtools\\korebuild\\2.1.0-preview1-15509\\modules\\sharedsources\\sharedsources.csproj", - "packagesPath": "C:\\Users\\rynowak\\.nuget\\packages\\", - "outputPath": "D:\\k\\Routing/shared/Microsoft.AspNetCore.Routing.DecisionTree.Sourcesobj\\", - "projectStyle": "PackageReference", - "configFilePaths": [ - "C:\\Users\\rynowak\\AppData\\Roaming\\NuGet\\NuGet.Config", - "C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.Offline.config" - ], - "originalTargetFrameworks": [ - "netstandard1.0" - ], - "sources": { - "C:\\Program Files (x86)\\Microsoft SDKs\\NuGetPackages\\": {}, - "C:\\Users\\rynowak\\.dotnet\\NuGetFallbackFolder": {}, - "C:\\Users\\rynowak\\.dotnet\\x64\\sdk\\NuGetFallbackFolder": {}, - "C:\\Users\\rynowak\\private_nuget": {}, - "https://www.nuget.org/api/v2/": {} - }, - "frameworks": { - "netstandard1.0": { - "projectReferences": {} - } - }, - "warningProperties": { - "allWarningsAsErrors": true, - "warnAsError": [ - "NU1605" - ] - } - }, - "frameworks": { - "netstandard1.0": {} - } - } -} \ No newline at end of file diff --git a/shared/Microsoft.AspNetCore.Routing.DecisionTree.Sourcesobj/sharedsources.csproj.nuget.g.props b/shared/Microsoft.AspNetCore.Routing.DecisionTree.Sourcesobj/sharedsources.csproj.nuget.g.props deleted file mode 100644 index b85f9b2c87..0000000000 --- a/shared/Microsoft.AspNetCore.Routing.DecisionTree.Sourcesobj/sharedsources.csproj.nuget.g.props +++ /dev/null @@ -1,15 +0,0 @@ - - - - True - NuGet - D:\k\Routing\shared\Microsoft.AspNetCore.Routing.DecisionTree.Sourcesobj\project.assets.json - $(UserProfile)\.nuget\packages\ - C:\Users\rynowak\.nuget\packages\ - PackageReference - 4.4.0 - - - $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - - \ No newline at end of file diff --git a/shared/Microsoft.AspNetCore.Routing.DecisionTree.Sourcesobj/sharedsources.csproj.nuget.g.targets b/shared/Microsoft.AspNetCore.Routing.DecisionTree.Sourcesobj/sharedsources.csproj.nuget.g.targets deleted file mode 100644 index 53cfaa19b1..0000000000 --- a/shared/Microsoft.AspNetCore.Routing.DecisionTree.Sourcesobj/sharedsources.csproj.nuget.g.targets +++ /dev/null @@ -1,6 +0,0 @@ - - - - $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - - \ No newline at end of file diff --git a/shared/Microsoft.AspNetCore.Routing.UrlMatchingTree.Sources/TreeEnumerator.cs b/shared/Microsoft.AspNetCore.Routing.UrlMatchingTree.Sources/TreeEnumerator.cs deleted file mode 100644 index a5b58f5aa7..0000000000 --- a/shared/Microsoft.AspNetCore.Routing.UrlMatchingTree.Sources/TreeEnumerator.cs +++ /dev/null @@ -1,119 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Collections; -using System.Collections.Generic; -using System.Diagnostics; -using Microsoft.AspNetCore.Dispatcher.Internal; -#if ROUTING -using Microsoft.AspNetCore.Routing.Template; - -namespace Microsoft.AspNetCore.Routing.Tree -#elif DISPATCHER -namespace Microsoft.AspNetCore.Dispatcher -#else -#error -#endif -{ - internal 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) - { - Debug.Assert(next.Depth < _tokenizer.Count); - if (next.Literals.TryGetValue(_tokenizer[next.Depth].Value, out var node)) - { - _stack.Push(node); - } - } - } - - return false; - } - - public void Reset() - { - _stack.Clear(); - Current = null; - } - } -} diff --git a/shared/Microsoft.AspNetCore.Routing.UrlMatchingTree.Sources/UrlMatchingTree.cs b/shared/Microsoft.AspNetCore.Routing.UrlMatchingTree.Sources/UrlMatchingTree.cs deleted file mode 100644 index 1983c0b082..0000000000 --- a/shared/Microsoft.AspNetCore.Routing.UrlMatchingTree.Sources/UrlMatchingTree.cs +++ /dev/null @@ -1,235 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using Microsoft.AspNetCore.Dispatcher.Patterns; -#if ROUTING -using Microsoft.AspNetCore.Routing.Template; - -namespace Microsoft.AspNetCore.Routing.Tree -#elif DISPATCHER -namespace Microsoft.AspNetCore.Dispatcher -#else -#error -#endif -{ -#if ROUTING - public -#elif DISPATCHER - internal -#else -#error -#endif - 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(0); - - internal static 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; -#if ROUTING - var routePattern = entry.RouteTemplate.ToRoutePattern(); - var matcher = new TemplateMatcher(entry.RouteTemplate, entry.Defaults); -#elif DISPATCHER - var routePattern = entry.RoutePattern; - var matcher = new RoutePatternMatcher(routePattern, entry.Defaults); -#else -#error -#endif - - for (var i = 0; i < routePattern.PathSegments.Count; i++) - { - var segment = routePattern.PathSegments[i]; - if (!segment.IsSimple) - { - // Treat complex segments as a constrained parameter - if (current.ConstrainedParameters == null) - { - current.ConstrainedParameters = new UrlMatchingNode(i + 1); - } - - current = current.ConstrainedParameters; - continue; - } - - Debug.Assert(segment.Parts.Count == 1); - var part = segment.Parts[0]; - if (part.IsLiteral) - { - var literal = (RoutePatternLiteral)part; - if (!current.Literals.TryGetValue(literal.Content, out var next)) - { - next = new UrlMatchingNode(i + 1); - current.Literals.Add(literal.Content, 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(routePattern.PathSegments, i)) - { -#if ROUTING - current.Matches.Add(new InboundMatch() { Entry = entry, TemplateMatcher = matcher }); -#elif DISPATCHER - current.Matches.Add(new InboundMatch() { Entry = entry, RoutePatternMatcher = matcher }); -#else -#error -#endif - } - - var parameter = (RoutePatternParameter)part; - if (parameter != null && parameter.Constraints.Any() && !parameter.IsCatchAll) - { - if (current.ConstrainedParameters == null) - { - current.ConstrainedParameters = new UrlMatchingNode(i + 1); - } - - current = current.ConstrainedParameters; - continue; - } - - if (parameter != null && !parameter.IsCatchAll) - { - if (current.Parameters == null) - { - current.Parameters = new UrlMatchingNode(i + 1); - } - - current = current.Parameters; - continue; - } - - if (parameter != null && parameter.Constraints.Any() && parameter.IsCatchAll) - { - if (current.ConstrainedCatchAlls == null) - { - current.ConstrainedCatchAlls = new UrlMatchingNode(i + 1) { IsCatchAll = true }; - } - - current = current.ConstrainedCatchAlls; - continue; - } - - if (parameter != null && parameter.IsCatchAll) - { - if (current.CatchAlls == null) - { - current.CatchAlls = new UrlMatchingNode(i + 1) { IsCatchAll = true }; - } - - current = current.CatchAlls; - continue; - } - - Debug.Fail("We shouldn't get here."); - } - -#if ROUTING - 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; - }); -#elif DISPATCHER - current.Matches.Add(new InboundMatch() { Entry = entry, RoutePatternMatcher = matcher }); - current.Matches.Sort((x, y) => - { - var result = x.Entry.Precedence.CompareTo(y.Entry.Precedence); - return result == 0 ? x.Entry.RoutePattern.RawText.CompareTo(y.Entry.RoutePattern.RawText) : result; - }); -#else -#error -#endif - - } - - private static bool RemainingSegmentsAreOptional(IReadOnlyList 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 parameter = (RoutePatternParameter)part; - var isOptionalCatchAllOrHasDefaultValue = parameter.IsOptional || - parameter.IsCatchAll || - parameter.DefaultValue != null; - - if (!isOptionalCatchAllOrHasDefaultValue) - { - // /{parameter} - return false; - } - } - - return true; - } - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher.Abstractions/Address.cs b/src/Microsoft.AspNetCore.Dispatcher.Abstractions/Address.cs deleted file mode 100644 index 2fa8e6372a..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher.Abstractions/Address.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Diagnostics; - -namespace Microsoft.AspNetCore.Dispatcher -{ - [DebuggerDisplay("{DisplayName,nq}")] - public abstract class Address - { - public abstract string DisplayName { get; } - - public abstract MetadataCollection Metadata { get; } - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher.Abstractions/DispatcherValueCollection.cs b/src/Microsoft.AspNetCore.Dispatcher.Abstractions/DispatcherValueCollection.cs deleted file mode 100644 index 07fac7420f..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher.Abstractions/DispatcherValueCollection.cs +++ /dev/null @@ -1,790 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Collections; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Diagnostics; -using Microsoft.AspNetCore.Dispatcher.Abstractions; -using Microsoft.Extensions.Internal; - -namespace Microsoft.AspNetCore.Dispatcher -{ - /// - /// An type for dispatcher values. - /// - public class DispatcherValueCollection : IDictionary, IReadOnlyDictionary - { - internal Storage _storage; - - /// - /// Creates an empty . - /// - public DispatcherValueCollection() - { - _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 DispatcherValueCollection(object values) - { - var dictionary = values as DispatcherValueCollection; - 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.FormatDispatcherValueCollection_DuplicateKey(kvp.Key, nameof(DispatcherValueCollection)); - 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.FormatDispatcherValueCollection_DuplicateKey(kvp.Key, nameof(DispatcherValueCollection)); - 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.FormatDispatcherValueCollection_DuplicateKey(key, nameof(DispatcherValueCollection)); - 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(DispatcherValueCollection collection) - { - if (collection == null) - { - throw new ArgumentNullException(); - } - - _storage = collection._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.FormatDispatcherValueCollection_DuplicatePropertyName( - type.FullName, - property.Name, - duplicate.Name, - nameof(DispatcherValueCollection)); - 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/Microsoft.AspNetCore.Dispatcher.Abstractions/Endpoint.cs b/src/Microsoft.AspNetCore.Dispatcher.Abstractions/Endpoint.cs deleted file mode 100644 index 4ddfdfdcb2..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher.Abstractions/Endpoint.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Diagnostics; - -namespace Microsoft.AspNetCore.Dispatcher -{ - [DebuggerDisplay("{GetType().Name} - '{DisplayName,nq}'")] - public abstract class Endpoint - { - public abstract string DisplayName { get; } - - public abstract MetadataCollection Metadata { get; } - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher.Abstractions/IDispatcherFeature.cs b/src/Microsoft.AspNetCore.Dispatcher.Abstractions/IDispatcherFeature.cs deleted file mode 100644 index 92cc6e6a95..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher.Abstractions/IDispatcherFeature.cs +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using Microsoft.AspNetCore.Http; - -namespace Microsoft.AspNetCore.Dispatcher -{ - public interface IDispatcherFeature - { - Endpoint Endpoint { get; set; } - - Func Handler { get; set; } - - DispatcherValueCollection Values { get; set; } - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher.Abstractions/MetadataCollection.cs b/src/Microsoft.AspNetCore.Dispatcher.Abstractions/MetadataCollection.cs deleted file mode 100644 index 8fb0ced2c2..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher.Abstractions/MetadataCollection.cs +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; - -namespace Microsoft.AspNetCore.Dispatcher -{ - public class MetadataCollection : IReadOnlyList - { - private readonly object[] _items; - - public MetadataCollection() - { - _items = Array.Empty(); - } - - public MetadataCollection(IEnumerable items) - { - if (items == null) - { - throw new ArgumentNullException(nameof(items)); - } - - _items = items.ToArray(); - } - - public object this[int index] => _items[index]; - - public int Count => _items.Length; - - public T GetMetadata() where T : class - { - for (var i = _items.Length -1; i >= 0; i--) - { - var item = _items[i] as T; - if (item !=null) - { - return item; - } - } - - return default; - } - - public IEnumerable GetOrderedMetadata() where T : class - { - for (var i = 0; i < _items.Length; i++) - { - var item = _items[i] as T; - if (item != null) - { - yield return item; - } - } - } - - public Enumerator GetEnumerator() => new Enumerator(this); - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - - public struct Enumerator : IEnumerator - { - private object[] _items; - private int _index; - private object _current; - - internal Enumerator(MetadataCollection collection) - { - _items = collection._items; - _index = 0; - _current = null; - } - - public object Current => _current; - - public void Dispose() - { - } - - public bool MoveNext() - { - if (_index < _items.Length) - { - _current = _items[_index++]; - return true; - } - - _current = null; - return false; - } - - public void Reset() - { - _index = 0; - _current = null; - } - } - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher.Abstractions/Microsoft.AspNetCore.Dispatcher.Abstractions.csproj b/src/Microsoft.AspNetCore.Dispatcher.Abstractions/Microsoft.AspNetCore.Dispatcher.Abstractions.csproj deleted file mode 100644 index 634baf1b04..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher.Abstractions/Microsoft.AspNetCore.Dispatcher.Abstractions.csproj +++ /dev/null @@ -1,16 +0,0 @@ - - - - ASP.NET Core abstractions for dispatching requests to endpoints and use addresses to generate URLs. - netstandard2.0 - $(NoWarn);CS1591 - true - aspnetcore;routing;dispatcher - - - - - - - - diff --git a/src/Microsoft.AspNetCore.Dispatcher.Abstractions/Properties/AssemblyInfo.cs b/src/Microsoft.AspNetCore.Dispatcher.Abstractions/Properties/AssemblyInfo.cs deleted file mode 100644 index 7694000f8e..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher.Abstractions/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Runtime.CompilerServices; - -[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Dispatcher.Abstractions.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Dispatcher.Abstractions/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Dispatcher.Abstractions/Properties/Resources.Designer.cs deleted file mode 100644 index 7c28389bab..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher.Abstractions/Properties/Resources.Designer.cs +++ /dev/null @@ -1,58 +0,0 @@ -// -namespace Microsoft.AspNetCore.Dispatcher.Abstractions -{ - using System.Globalization; - using System.Reflection; - using System.Resources; - - internal static class Resources - { - private static readonly ResourceManager _resourceManager - = new ResourceManager("Microsoft.AspNetCore.Dispatcher.Abstractions.Resources", typeof(Resources).GetTypeInfo().Assembly); - - /// - /// An element with the key '{0}' already exists in the {1}. - /// - internal static string DispatcherValueCollection_DuplicateKey - { - get => GetString("DispatcherValueCollection_DuplicateKey"); - } - - /// - /// An element with the key '{0}' already exists in the {1}. - /// - internal static string FormatDispatcherValueCollection_DuplicateKey(object p0, object p1) - => string.Format(CultureInfo.CurrentCulture, GetString("DispatcherValueCollection_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 DispatcherValueCollection_DuplicatePropertyName - { - get => GetString("DispatcherValueCollection_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 FormatDispatcherValueCollection_DuplicatePropertyName(object p0, object p1, object p2, object p3) - => string.Format(CultureInfo.CurrentCulture, GetString("DispatcherValueCollection_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/Microsoft.AspNetCore.Dispatcher.Abstractions/Resources.resx b/src/Microsoft.AspNetCore.Dispatcher.Abstractions/Resources.resx deleted file mode 100644 index 642921cc04..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher.Abstractions/Resources.resx +++ /dev/null @@ -1,126 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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/Microsoft.AspNetCore.Dispatcher.Abstractions/Template.cs b/src/Microsoft.AspNetCore.Dispatcher.Abstractions/Template.cs deleted file mode 100644 index 94b56d402c..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher.Abstractions/Template.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using Microsoft.AspNetCore.Http; - -namespace Microsoft.AspNetCore.Dispatcher -{ - public abstract class Template - { - public virtual string GetUrl() - { - return GetUrl(null, new DispatcherValueCollection()); - } - - public virtual string GetUrl(HttpContext httpContext) - { - return GetUrl(httpContext, new DispatcherValueCollection()); - } - - public virtual string GetUrl(DispatcherValueCollection values) - { - return GetUrl(null, values); - } - - public abstract string GetUrl(HttpContext httpContext, DispatcherValueCollection values); - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher.Abstractions/TemplateFactory.cs b/src/Microsoft.AspNetCore.Dispatcher.Abstractions/TemplateFactory.cs deleted file mode 100644 index 5fdbabc93b..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher.Abstractions/TemplateFactory.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -namespace Microsoft.AspNetCore.Dispatcher -{ - public abstract class TemplateFactory - { - public Template GetTemplate(object values) - { - return GetTemplateFromKey(new DispatcherValueCollection(values)); - } - - public abstract Template GetTemplateFromKey(TKey key) where TKey : class; - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher.Abstractions/baseline.netcore.json b/src/Microsoft.AspNetCore.Dispatcher.Abstractions/baseline.netcore.json deleted file mode 100644 index 7a73a41bfd..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher.Abstractions/baseline.netcore.json +++ /dev/null @@ -1,2 +0,0 @@ -{ -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Dispatcher/AddressTable.cs b/src/Microsoft.AspNetCore.Dispatcher/AddressTable.cs deleted file mode 100644 index 8ae1019518..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/AddressTable.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Collections.Generic; - -namespace Microsoft.AspNetCore.Dispatcher -{ - public abstract class AddressTable - { - public abstract IReadOnlyList> AddressGroups { get; } - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher/AmbiguousEndpointException.cs b/src/Microsoft.AspNetCore.Dispatcher/AmbiguousEndpointException.cs deleted file mode 100644 index 4f54f46cb4..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/AmbiguousEndpointException.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Runtime.Serialization; - -namespace Microsoft.AspNetCore.Dispatcher -{ - /// - /// An exception which indicates multiple matches in endpoint selection. - /// - [Serializable] - public class AmbiguousEndpointException : Exception - { - public AmbiguousEndpointException(string message) - : base(message) - { - } - - protected AmbiguousEndpointException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Dispatcher/Builder/DispatcherApplicationBuilderExtensions.cs b/src/Microsoft.AspNetCore.Dispatcher/Builder/DispatcherApplicationBuilderExtensions.cs deleted file mode 100644 index 55a04b0211..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/Builder/DispatcherApplicationBuilderExtensions.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using Microsoft.AspNetCore.Dispatcher; - -namespace Microsoft.AspNetCore.Builder -{ - public static class DispatcherApplicationBuilderExtensions - { - public static IApplicationBuilder UseDispatcher(this IApplicationBuilder builder) - { - builder.Properties.Add("Dispatcher", true); - return builder.UseMiddleware(); - } - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher/CompositeDispatcherDataSource.cs b/src/Microsoft.AspNetCore.Dispatcher/CompositeDispatcherDataSource.cs deleted file mode 100644 index 9925d8a188..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/CompositeDispatcherDataSource.cs +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.Extensions.Primitives; - -namespace Microsoft.AspNetCore.Dispatcher -{ - public class CompositeDispatcherDataSource : DispatcherDataSource - { - private readonly DispatcherDataSource[] _dataSources; - - public CompositeDispatcherDataSource(IEnumerable dataSources) - { - if (dataSources == null) - { - throw new ArgumentNullException(nameof(dataSources)); - } - - _dataSources = dataSources.ToArray(); - - var changeTokens = new IChangeToken[_dataSources.Length]; - for (var i = 0; i < _dataSources.Length; i++) - { - changeTokens[i] = _dataSources[i].ChangeToken; - } - - ChangeToken = new CompositeChangeToken(changeTokens); - } - - public override IChangeToken ChangeToken { get; } - - protected override IReadOnlyList
GetAddresses() - { - var addresses = new List
(); - for (var i = 0; i < _dataSources.Length; i++) - { - addresses.AddRange(((IAddressCollectionProvider)_dataSources[i]).Addresses); - } - - return addresses; - } - - protected override IReadOnlyList GetEndpoints() - { - var endpoints = new List(); - for (var i = 0; i < _dataSources.Length; i++) - { - endpoints.AddRange(((IEndpointCollectionProvider)_dataSources[i]).Endpoints); - } - - return endpoints; - } - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher/CompositeHandlerFactory.cs b/src/Microsoft.AspNetCore.Dispatcher/CompositeHandlerFactory.cs deleted file mode 100644 index ab7ae23b81..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/CompositeHandlerFactory.cs +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.AspNetCore.Http; - -namespace Microsoft.AspNetCore.Dispatcher -{ - public class CompositeHandlerFactory : IHandlerFactory - { - private readonly IHandlerFactory[] _factories; - - public CompositeHandlerFactory(IEnumerable factories) - { - if (factories == null) - { - throw new ArgumentNullException(nameof(factories)); - } - - _factories = factories.ToArray(); - } - - public Func CreateHandler(Endpoint endpoint) - { - if (endpoint == null) - { - throw new ArgumentNullException(nameof(endpoint)); - } - - for (var i = 0; i < _factories.Length; i++) - { - var handler = _factories[i].CreateHandler(endpoint); - if (handler != null) - { - return handler; - } - } - - return null; - } - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher/ConstraintPurpose.cs b/src/Microsoft.AspNetCore.Dispatcher/ConstraintPurpose.cs deleted file mode 100644 index b88d791cdb..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/ConstraintPurpose.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -namespace Microsoft.AspNetCore.Dispatcher -{ - /// - /// Represents the purpose for invoking an . - /// - public enum ConstraintPurpose - { - /// - /// A request URL is being processed by the dispatcher. - /// - IncomingRequest, - - /// - /// A URL is being created by a template. - /// - TemplateExecution, - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher/Constraints/AlphaDispatcherValueConstraint.cs b/src/Microsoft.AspNetCore.Dispatcher/Constraints/AlphaDispatcherValueConstraint.cs deleted file mode 100644 index 8291289ff1..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/Constraints/AlphaDispatcherValueConstraint.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -namespace Microsoft.AspNetCore.Dispatcher -{ - /// - /// Constrains a dispatcher value parameter to contain only lowercase or uppercase letters A through Z in the English alphabet. - /// - public class AlphaDispatcherValueConstraint : RegexDispatcherValueConstraint - { - /// - /// Initializes a new instance of the class. - /// - public AlphaDispatcherValueConstraint() : base(@"^[a-z]*$") - { - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Dispatcher/Constraints/CompositeDispatcherValueConstraint.cs b/src/Microsoft.AspNetCore.Dispatcher/Constraints/CompositeDispatcherValueConstraint.cs deleted file mode 100644 index 8e7ea40209..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/Constraints/CompositeDispatcherValueConstraint.cs +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; - -namespace Microsoft.AspNetCore.Dispatcher -{ - /// - /// Constrains a dispatcher value by several child constraints. - /// - public class CompositeDispatcherValueConstraint : IDispatcherValueConstraint - { - /// - /// Initializes a new instance of the class. - /// - /// The child constraints that must match for this constraint to match. - public CompositeDispatcherValueConstraint(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(DispatcherValueConstraintContext constraintContext) - { - if (constraintContext == null) - { - throw new ArgumentNullException(nameof(constraintContext)); - } - - foreach (var constraint in Constraints) - { - if (!constraint.Match(constraintContext)) - { - return false; - } - } - - return true; - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Dispatcher/Constraints/DefaultConstraintFactory.cs b/src/Microsoft.AspNetCore.Dispatcher/Constraints/DefaultConstraintFactory.cs deleted file mode 100644 index 687dc84afd..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/Constraints/DefaultConstraintFactory.cs +++ /dev/null @@ -1,152 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Reflection; -using Microsoft.Extensions.Options; - -namespace Microsoft.AspNetCore.Dispatcher -{ - /// - /// 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 DefaultConstraintFactory : IConstraintFactory - { - private readonly IDictionary _constraintMap; - - /// - /// Initializes a new instance of the class. - /// - /// - /// Accessor for containing the constraints of interest. - /// - public DefaultConstraintFactory(IOptions dispatcherOptions) - { - _constraintMap = dispatcherOptions.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 IDispatcherValueConstraint ResolveConstraint(string constraint) - { - if (constraint == null) - { - throw new ArgumentNullException(nameof(constraint)); - } - - string constraintKey; - string argumentString; - var indexOfFirstOpenParens = constraint.IndexOf('('); - if (indexOfFirstOpenParens >= 0 && constraint.EndsWith(")", StringComparison.Ordinal)) - { - constraintKey = constraint.Substring(0, indexOfFirstOpenParens); - argumentString = constraint.Substring(indexOfFirstOpenParens + 1, - constraint.Length - indexOfFirstOpenParens - 2); - } - else - { - constraintKey = constraint; - argumentString = null; - } - - if (!_constraintMap.TryGetValue(constraintKey, out var constraintType)) - { - // Cannot resolve the constraint key - return null; - } - - if (!typeof(IDispatcherValueConstraint).GetTypeInfo().IsAssignableFrom(constraintType.GetTypeInfo())) - { - throw new InvalidOperationException( - Resources.FormatDefaultConstraintResolver_TypeNotConstraint( - constraintType, constraintKey, typeof(IDispatcherValueConstraint).Name)); - } - - try - { - return CreateConstraint(constraintType, argumentString); - } - catch (Exception exception) - { - throw new InvalidOperationException( - $"An error occurred while trying to create an instance of route constraint '{constraintType.FullName}'.", - exception); - } - } - - private static IDispatcherValueConstraint CreateConstraint(Type constraintType, string argumentString) - { - // No arguments - call the default constructor - if (argumentString == null) - { - return (IDispatcherValueConstraint)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 RegexDispatcherValueConstraint 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 InvalidOperationException( - Resources.FormatDefaultConstraintResolver_CouldNotFindCtor( - constraintTypeInfo.Name, arguments.Length)); - } - else if (constructorMatches == 1) - { - activationConstructor = matchingConstructors[0]; - parameters = ConvertArguments(activationConstructor.GetParameters(), arguments); - } - else - { - throw new InvalidOperationException( - Resources.FormatDefaultConstraintResolver_AmbiguousCtors( - constraintTypeInfo.Name, arguments.Length)); - } - } - - return (IDispatcherValueConstraint)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/Microsoft.AspNetCore.Dispatcher/Constraints/DispatcherValueConstraintBuilder.cs b/src/Microsoft.AspNetCore.Dispatcher/Constraints/DispatcherValueConstraintBuilder.cs deleted file mode 100644 index f1363f34e1..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/Constraints/DispatcherValueConstraintBuilder.cs +++ /dev/null @@ -1,189 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; - -namespace Microsoft.AspNetCore.Dispatcher -{ - /// - /// A builder for producing a mapping of keys to . - /// - /// - /// allows iterative building a set of dispatcher value constraints, and will - /// merge multiple entries for the same key. - /// - public class DispatcherValueConstraintBuilder - { - private readonly IConstraintFactory _constraintFactory; - private readonly string _rawText; - private readonly Dictionary> _constraints; - private readonly HashSet _optionalParameters; - - /// - /// Creates a new instance. - /// - /// The . - /// The display name (for use in error messages). - public DispatcherValueConstraintBuilder( - IConstraintFactory constraintFactory, - string rawText) - { - if (constraintFactory == null) - { - throw new ArgumentNullException(nameof(constraintFactory)); - } - - if (rawText == null) - { - throw new ArgumentNullException(nameof(rawText)); - } - - _constraintFactory = constraintFactory; - _rawText = rawText; - - _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) - { - IDispatcherValueConstraint constraint; - if (kvp.Value.Count == 1) - { - constraint = kvp.Value[0]; - } - else - { - constraint = new CompositeDispatcherValueConstraint(kvp.Value.ToArray()); - } - - if (_optionalParameters.Contains(kvp.Key)) - { - var optionalConstraint = new OptionalDispatcherValueConstraint(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 IDispatcherValueConstraint; - if (constraint == null) - { - var regexPattern = value as string; - if (regexPattern == null) - { - throw new InvalidOperationException( - Resources.FormatDispatcherValueConstraintBuilder_ValidationMustBeStringOrCustomConstraint( - key, - value, - _rawText, - typeof(IDispatcherValueConstraint))); - } - - var constraintsRegEx = "^(" + regexPattern + ")$"; - constraint = new RegexDispatcherValueConstraint(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 = _constraintFactory.ResolveConstraint(constraintText); - if (constraint == null) - { - throw new InvalidOperationException( - Resources.FormatDispatcherValueConstraintBuilder_CouldNotResolveConstraint( - key, - constraintText, - _rawText, - _constraintFactory.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, IDispatcherValueConstraint constraint) - { - if (!_constraints.TryGetValue(key, out var list)) - { - list = new List(); - _constraints.Add(key, list); - } - - list.Add(constraint); - } - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher/Constraints/DispatcherValueConstraintContext.cs b/src/Microsoft.AspNetCore.Dispatcher/Constraints/DispatcherValueConstraintContext.cs deleted file mode 100644 index 9ab34cf467..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/Constraints/DispatcherValueConstraintContext.cs +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using Microsoft.AspNetCore.Http; - -namespace Microsoft.AspNetCore.Dispatcher -{ - /// - /// A context object for . - /// - public class DispatcherValueConstraintContext - { - private HttpContext _httpContext; - private DispatcherValueCollection _values; - private ConstraintPurpose _purpose; - private string _key; - - /// - /// Creates a new . - /// - public DispatcherValueConstraintContext() - { - } - - /// - /// Creates a new for the current request. - /// - /// The associated with the current request. - /// The for the current operation. - /// The purpose for invoking the constraint. - public DispatcherValueConstraintContext(HttpContext httpContext, DispatcherValueCollection values, ConstraintPurpose purpose) - { - if (httpContext == null) - { - throw new ArgumentNullException(nameof(httpContext)); - } - - if (values == null) - { - throw new ArgumentNullException(nameof(values)); - } - - HttpContext = httpContext; - Values = values; - Purpose = purpose; - } - - /// - /// Gets or sets the associated with the current request. - /// - public HttpContext HttpContext - { - get => _httpContext; - set - { - if (value == null) - { - throw new ArgumentNullException(nameof(value)); - } - - _httpContext = value; - } - } - - /// - /// Gets or sets the key associated with the current . - /// - public string Key - { - get => _key; - set - { - if (value == null) - { - throw new ArgumentNullException(nameof(value)); - } - - _key = value; - } - } - - /// - /// Gets or sets the purpose of executing the . - /// - public ConstraintPurpose Purpose - { - get => _purpose; - set => _purpose = value; - } - - /// - /// Gets or sets the associated with the current operation. - /// - public DispatcherValueCollection Values - { - get => _values; - set - { - if (value == null) - { - throw new ArgumentNullException(nameof(value)); - } - - _values = value; - } - } - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher/Constraints/IConstraintFactory.cs b/src/Microsoft.AspNetCore.Dispatcher/Constraints/IConstraintFactory.cs deleted file mode 100644 index ca5acbbc74..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/Constraints/IConstraintFactory.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -namespace Microsoft.AspNetCore.Dispatcher -{ - /// - /// Defines an abstraction for resolving constraints as instances of . - /// - public interface IConstraintFactory - { - /// - /// Resolves the constraint. - /// - /// The constraint to resolve. - /// The the constraint was resolved to. - IDispatcherValueConstraint ResolveConstraint(string constraint); - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Dispatcher/Constraints/IDispatcherValueConstraint.cs b/src/Microsoft.AspNetCore.Dispatcher/Constraints/IDispatcherValueConstraint.cs deleted file mode 100644 index 4de894ebd6..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/Constraints/IDispatcherValueConstraint.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -namespace Microsoft.AspNetCore.Dispatcher -{ - /// - /// Defines the contract that a class must implement in order to check whether a URL parameter - /// value is valid for a constraint. - /// - public interface IDispatcherValueConstraint - { - /// - /// Determines whether the current operation should succeed or fail, typically by validating one or - /// more values in . - /// - /// The associated with the current operation. - /// true if the current operation should proceed; otherwise false. - bool Match(DispatcherValueConstraintContext context); - } -} - diff --git a/src/Microsoft.AspNetCore.Dispatcher/Constraints/IntDispatcherValueConstraint.cs b/src/Microsoft.AspNetCore.Dispatcher/Constraints/IntDispatcherValueConstraint.cs deleted file mode 100644 index 77d9f70f74..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/Constraints/IntDispatcherValueConstraint.cs +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Globalization; - -namespace Microsoft.AspNetCore.Dispatcher -{ - /// - /// Constrains a dispatcher value parameter to represent only 32-bit integer values. - /// - public class IntDispatcherValueConstraint : IDispatcherValueConstraint - { - /// - public bool Match(DispatcherValueConstraintContext constraintContext) - { - if (constraintContext == null) - { - throw new ArgumentNullException(nameof(constraintContext)); - } - - if (constraintContext.Values.TryGetValue(constraintContext.Key, out var value) && value != null) - { - if (value is int) - { - return true; - } - - var valueString = Convert.ToString(value, CultureInfo.InvariantCulture); - return int.TryParse(valueString, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result); - } - - return false; - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Dispatcher/Constraints/OptionalDispatcherValueConstraint.cs b/src/Microsoft.AspNetCore.Dispatcher/Constraints/OptionalDispatcherValueConstraint.cs deleted file mode 100644 index f7fccc2a65..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/Constraints/OptionalDispatcherValueConstraint.cs +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; - -namespace Microsoft.AspNetCore.Dispatcher -{ - /// - /// Defines a constraint on an optional parameter. If the parameter is present, then it is constrained by InnerConstraint. - /// - public class OptionalDispatcherValueConstraint : IDispatcherValueConstraint - { - public OptionalDispatcherValueConstraint(IDispatcherValueConstraint innerConstraint) - { - if (innerConstraint == null) - { - throw new ArgumentNullException(nameof(innerConstraint)); - } - - InnerConstraint = innerConstraint; - } - - public IDispatcherValueConstraint InnerConstraint { get; } - - /// - public bool Match(DispatcherValueConstraintContext constraintContext) - { - if (constraintContext == null) - { - throw new ArgumentNullException(nameof(constraintContext)); - } - - if (constraintContext.Values.TryGetValue(constraintContext.Key, out var routeValue) - && routeValue != null) - { - return InnerConstraint.Match(constraintContext); - } - - return true; - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Dispatcher/Constraints/RegexDispatcherValueConstraint.cs b/src/Microsoft.AspNetCore.Dispatcher/Constraints/RegexDispatcherValueConstraint.cs deleted file mode 100644 index 99d0a10cfe..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/Constraints/RegexDispatcherValueConstraint.cs +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Globalization; -using System.Text.RegularExpressions; - -namespace Microsoft.AspNetCore.Dispatcher -{ - public class RegexDispatcherValueConstraint : IDispatcherValueConstraint - { - private static readonly TimeSpan RegexMatchTimeout = TimeSpan.FromSeconds(10); - - public RegexDispatcherValueConstraint(Regex regex) - { - if (regex == null) - { - throw new ArgumentNullException(nameof(regex)); - } - - Constraint = regex; - } - - public RegexDispatcherValueConstraint(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(DispatcherValueConstraintContext constraintContext) - { - if (constraintContext == null) - { - throw new ArgumentNullException(nameof(constraintContext)); - } - - if (constraintContext.Values.TryGetValue(constraintContext.Key, out var routeValue) - && routeValue != null) - { - var parameterValueString = Convert.ToString(routeValue, CultureInfo.InvariantCulture); - - return Constraint.IsMatch(parameterValueString); - } - - return false; - } - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher/Constraints/RegexStringDispatcherValueConstraint.cs b/src/Microsoft.AspNetCore.Dispatcher/Constraints/RegexStringDispatcherValueConstraint.cs deleted file mode 100644 index 67bcf26387..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/Constraints/RegexStringDispatcherValueConstraint.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -namespace Microsoft.AspNetCore.Dispatcher -{ - /// - /// Represents a regex constraint. - /// - public class RegexStringDispatcherValueConstraint : RegexDispatcherValueConstraint - { - /// - /// Initializes a new instance of the class. - /// - /// The regular expression pattern to match. - public RegexStringDispatcherValueConstraint(string regexPattern) - : base(regexPattern) - { - } - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher/DefaultAddressTable.cs b/src/Microsoft.AspNetCore.Dispatcher/DefaultAddressTable.cs deleted file mode 100644 index 4e8d47667b..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/DefaultAddressTable.cs +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using Microsoft.Extensions.Options; - -namespace Microsoft.AspNetCore.Dispatcher -{ - public class DefaultAddressTable : AddressTable - { - private readonly DispatcherOptions _options; - private readonly List
[] _groups; - - public DefaultAddressTable(IOptions options) - { - if (options == null) - { - throw new ArgumentNullException(nameof(options)); - } - - _options = options.Value; - - _groups = new List
[options.Value.Matchers.Count]; - for (var i = 0; i < options.Value.Matchers.Count; i++) - { - _groups[i] = new List
(options.Value.Matchers[i].AddressProvider?.Addresses ?? Array.Empty
()); - } - } - - public override IReadOnlyList> AddressGroups => _groups; - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher/DefaultDispatcherDataSource.cs b/src/Microsoft.AspNetCore.Dispatcher/DefaultDispatcherDataSource.cs deleted file mode 100644 index 2f279122d2..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/DefaultDispatcherDataSource.cs +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Collections.Generic; -using Microsoft.Extensions.FileProviders; -using Microsoft.Extensions.Primitives; - -namespace Microsoft.AspNetCore.Dispatcher -{ - public class DefaultDispatcherDataSource : DispatcherDataSource - { - private readonly List
_addresses; - private readonly List _endpoints; - - public DefaultDispatcherDataSource() - { - _addresses = new List
(); - _endpoints = new List(); - } - - public override IChangeToken ChangeToken { get; } = NullChangeToken.Singleton; - - public IList
Addresses => _addresses; - - public IList Endpoints => _endpoints; - - protected override IReadOnlyList
GetAddresses() => _addresses; - - protected override IReadOnlyList GetEndpoints() => _endpoints; - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher/DefaultTemplateFactory.cs b/src/Microsoft.AspNetCore.Dispatcher/DefaultTemplateFactory.cs deleted file mode 100644 index e984e08683..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/DefaultTemplateFactory.cs +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Microsoft.AspNetCore.Dispatcher -{ - public class DefaultTemplateFactory : TemplateFactory - { - private readonly ITemplateFactoryComponent[] _components; - - public DefaultTemplateFactory(IEnumerable components) - { - if (components == null) - { - throw new ArgumentNullException(nameof(components)); - } - - _components = components.ToArray(); - } - - public override Template GetTemplateFromKey(TKey key) - { - if (key == null) - { - throw new ArgumentNullException(nameof(key)); - } - - for (var i = 0; i < _components.Length; i++) - { - var component = _components[i] as TemplateFactory; - if (component == null) - { - continue; - } - - var template = component.GetTemplate(key); - if (template != null) - { - return template; - } - } - - return null; - } - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher/DependencyInjection/DefaultDispatcherConfigureOptions.cs b/src/Microsoft.AspNetCore.Dispatcher/DependencyInjection/DefaultDispatcherConfigureOptions.cs deleted file mode 100644 index e9c7ac59e2..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/DependencyInjection/DefaultDispatcherConfigureOptions.cs +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using Microsoft.AspNetCore.Dispatcher; -using Microsoft.Extensions.Options; - -namespace Microsoft.Extensions.DependencyInjection -{ - internal class DefaultDispatcherConfigureOptions : IConfigureOptions - { - private readonly IEnumerable _dataSources; - private readonly IDefaultMatcherFactory _dispatcherFactory; - private readonly IEnumerable _endpointSelectors; - private readonly IEnumerable _handlerFactories; - - public DefaultDispatcherConfigureOptions( - IDefaultMatcherFactory dispatcherFactory, - IEnumerable dataSources, - IEnumerable endpointSelectors, - IEnumerable handlerFactories) - { - _dispatcherFactory = dispatcherFactory; - _dataSources = dataSources; - _endpointSelectors = endpointSelectors; - _handlerFactories = handlerFactories; - } - - public void Configure(DispatcherOptions options) - { - if (options == null) - { - throw new ArgumentNullException(nameof(options)); - } - - var matcher = _dispatcherFactory.CreateMatcher(new CompositeDispatcherDataSource(_dataSources), _endpointSelectors); - - options.Matchers.Add(new MatcherEntry() - { - Matcher = matcher, - AddressProvider = matcher as IAddressCollectionProvider, - EndpointProvider = matcher as IEndpointCollectionProvider, - HandlerFactory = new CompositeHandlerFactory(_handlerFactories), - }); - } - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher/DependencyInjection/DispatcherServiceCollectionExtensions.cs b/src/Microsoft.AspNetCore.Dispatcher/DependencyInjection/DispatcherServiceCollectionExtensions.cs deleted file mode 100644 index 26d6130505..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/DependencyInjection/DispatcherServiceCollectionExtensions.cs +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using Microsoft.AspNetCore.Dispatcher; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Options; - -namespace Microsoft.Extensions.DependencyInjection -{ - public static class DispatcherServiceCollectionExtensions - { - public static IServiceCollection AddDispatcher(this IServiceCollection services) - { - if (services == null) - { - throw new ArgumentNullException(nameof(services)); - } - - // Adds the EndpointMiddleware at the end of the pipeline if the DispatcherMiddleware is in use. - services.TryAddEnumerable(ServiceDescriptor.Singleton()); - - // Adds a default dispatcher which will collect all data sources and endpoint selectors from DI. - services.TryAddEnumerable(ServiceDescriptor.Transient, DefaultDispatcherConfigureOptions>()); - - // - // Addresses + Templates - // - services.TryAddSingleton(); - services.TryAddSingleton(); - services.TryAddSingleton(); - services.TryAddSingleton(); - - // - // Misc Infrastructure - // - services.TryAddSingleton(); - services.TryAddSingleton(); - - services.TryAddEnumerable(ServiceDescriptor.Singleton()); - - return services; - } - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher/DispatcherDataSource.cs b/src/Microsoft.AspNetCore.Dispatcher/DispatcherDataSource.cs deleted file mode 100644 index b04149bf09..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/DispatcherDataSource.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Collections.Generic; -using Microsoft.Extensions.Primitives; - -namespace Microsoft.AspNetCore.Dispatcher -{ - public abstract class DispatcherDataSource : IAddressCollectionProvider, IEndpointCollectionProvider - { - public abstract IChangeToken ChangeToken { get; } - - protected abstract IReadOnlyList
GetAddresses(); - - protected abstract IReadOnlyList GetEndpoints(); - - IReadOnlyList
IAddressCollectionProvider.Addresses => GetAddresses(); - - IReadOnlyList IEndpointCollectionProvider.Endpoints => GetEndpoints(); - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher/DispatcherEndpointStartupFilter.cs b/src/Microsoft.AspNetCore.Dispatcher/DispatcherEndpointStartupFilter.cs deleted file mode 100644 index 4e40e1e640..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/DispatcherEndpointStartupFilter.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; - -namespace Microsoft.AspNetCore.Dispatcher -{ - public class DispatcherEndpointStartupFilter : IStartupFilter - { - public Action Configure(Action next) - { - return builder => - { - next(builder); - if (builder.Properties.ContainsKey("Dispatcher")) - { - builder.UseMiddleware(); - } - }; - } - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher/DispatcherFeature.cs b/src/Microsoft.AspNetCore.Dispatcher/DispatcherFeature.cs deleted file mode 100644 index 6506f6efcb..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/DispatcherFeature.cs +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using Microsoft.AspNetCore.Http; - -namespace Microsoft.AspNetCore.Dispatcher -{ - public class DispatcherFeature : IDispatcherFeature - { - public Endpoint Endpoint { get; set; } - - public Func Handler { get; set; } - - public DispatcherValueCollection Values { get; set; } - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher/DispatcherMiddleware.cs b/src/Microsoft.AspNetCore.Dispatcher/DispatcherMiddleware.cs deleted file mode 100644 index b93d908207..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/DispatcherMiddleware.cs +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -namespace Microsoft.AspNetCore.Dispatcher -{ - public class DispatcherMiddleware - { - private readonly ILogger _logger; - private readonly DispatcherOptions _options; - private readonly RequestDelegate _next; - - public DispatcherMiddleware(IOptions options, ILogger logger, RequestDelegate next) - { - if (options == null) - { - throw new ArgumentNullException(nameof(options)); - } - - if (logger == null) - { - throw new ArgumentNullException(nameof(logger)); - } - - if (next == null) - { - throw new ArgumentNullException(nameof(next)); - } - - _options = options.Value; - _logger = logger; - _next = next; - } - - public async Task Invoke(HttpContext httpContext) - { - var feature = new DispatcherFeature(); - httpContext.Features.Set(feature); - - var context = new MatcherContext(httpContext); - foreach (var entry in _options.Matchers) - { - await entry.Matcher.MatchAsync(context); - - if (context.ShortCircuit != null) - { - feature.Endpoint = context.Endpoint; - feature.Values = context.Values; - - await context.ShortCircuit(httpContext); - - _logger.RequestShortCircuitedDispatcherMiddleware(context); - return; - } - - if (context.Endpoint != null) - { - _logger.EndpointMatchedDispatcherMiddleware(context.Endpoint); - feature.Endpoint = context.Endpoint; - feature.Values = context.Values; - - feature.Handler = entry.HandlerFactory.CreateHandler(feature.Endpoint); - if (feature.Handler == null) - { - _logger.HandlerNotCreated(entry); - throw new InvalidOperationException("Couldn't create a handler, that's bad."); - } - - break; - } - - _logger.NoEndpointsMatchedMatcher(entry.Matcher); - } - - await _next(httpContext); - } - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher/DispatcherOptions.cs b/src/Microsoft.AspNetCore.Dispatcher/DispatcherOptions.cs deleted file mode 100644 index 6e55164498..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/DispatcherOptions.cs +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; - -namespace Microsoft.AspNetCore.Dispatcher -{ - public class DispatcherOptions - { - public MatcherCollection Matchers { get; } = new MatcherCollection(); - - 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(IntDispatcherValueConstraint) }, - - //// Regex-based constraints - { "alpha", typeof(AlphaDispatcherValueConstraint) }, - { "regex", typeof(RegexStringDispatcherValueConstraint) }, - }; - } - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher/EndpointMiddleware.cs b/src/Microsoft.AspNetCore.Dispatcher/EndpointMiddleware.cs deleted file mode 100644 index b86c713210..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/EndpointMiddleware.cs +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -namespace Microsoft.AspNetCore.Dispatcher -{ - public class EndpointMiddleware - { - private readonly ILogger _logger; - private readonly DispatcherOptions _options; - private readonly RequestDelegate _next; - - public EndpointMiddleware(IOptions options, ILogger logger, RequestDelegate next) - { - if (options == null) - { - throw new ArgumentNullException(nameof(options)); - } - - if (logger == null) - { - throw new ArgumentNullException(nameof(logger)); - } - - if (next == null) - { - throw new ArgumentNullException(nameof(next)); - } - - _options = options.Value; - _logger = logger; - _next = next; - } - - public async Task Invoke(HttpContext httpContext) - { - var feature = httpContext.Features.Get(); - if (feature.Handler != null) - { - _logger.ExecutingEndpoint(feature.Endpoint); - - try - { - await feature.Handler(_next)(httpContext); - } - finally - { - _logger.ExecutedEndpoint(feature.Endpoint); - } - - return; - } - - await _next(httpContext); - } - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher/EndpointOrderMetadata.cs b/src/Microsoft.AspNetCore.Dispatcher/EndpointOrderMetadata.cs deleted file mode 100644 index eefbf1efa2..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/EndpointOrderMetadata.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -namespace Microsoft.AspNetCore.Dispatcher -{ - public class EndpointOrderMetadata : IEndpointOrderMetadata - { - public EndpointOrderMetadata(int order) - { - Order = order; - } - - public int Order { get; } - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher/EndpointSelector.cs b/src/Microsoft.AspNetCore.Dispatcher/EndpointSelector.cs deleted file mode 100644 index 4d1f540731..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/EndpointSelector.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Threading.Tasks; - -namespace Microsoft.AspNetCore.Dispatcher -{ - public abstract class EndpointSelector - { - public abstract Task SelectAsync(EndpointSelectorContext context); - - public virtual void Initialize(IEndpointCollectionProvider endpointProvider) - { - } - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher/EndpointSelectorContext.cs b/src/Microsoft.AspNetCore.Dispatcher/EndpointSelectorContext.cs deleted file mode 100644 index a1ea827aa3..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/EndpointSelectorContext.cs +++ /dev/null @@ -1,98 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; - -namespace Microsoft.AspNetCore.Dispatcher -{ - public sealed class EndpointSelectorContext - { - private int _index; - - public EndpointSelectorContext(HttpContext httpContext, DispatcherValueCollection values, IList endpoints, IList selectors) - { - if (httpContext == null) - { - throw new ArgumentNullException(nameof(httpContext)); - } - - if (values == null) - { - throw new ArgumentNullException(nameof(values)); - } - - if (endpoints == null) - { - throw new ArgumentNullException(nameof(endpoints)); - } - - if (selectors == null) - { - throw new ArgumentNullException(nameof(selectors)); - } - - HttpContext = httpContext; - Values = values; - Endpoints = endpoints; - Selectors = selectors; - } - - public IList Endpoints { get; } - - public HttpContext HttpContext { get; } - - public IList Selectors { get; } - - public RequestDelegate ShortCircuit { get; set; } - - public DispatcherValueCollection Values { get; } - - public Task InvokeNextAsync() - { - if (_index >= Selectors.Count) - { - return Task.CompletedTask; - } - - var selector = Selectors[_index++]; - return selector.SelectAsync(this); - } - - public Snapshot CreateSnapshot() - { - return new Snapshot(_index, Endpoints); - } - - public void RestoreSnapshot(Snapshot snapshot) - { - snapshot.Apply(this); - } - - public struct Snapshot - { - private readonly int _index; - private readonly Endpoint[] _endpoints; - - internal Snapshot(int index, IList endpoints) - { - _index = index; - _endpoints = endpoints.ToArray(); - } - - internal void Apply(EndpointSelectorContext context) - { - context._index = _index; - - context.Endpoints.Clear(); - for (var i = 0; i < _endpoints.Length; i++) - { - context.Endpoints.Add(_endpoints[i]); - } - } - } - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher/HandlerFactory.cs b/src/Microsoft.AspNetCore.Dispatcher/HandlerFactory.cs deleted file mode 100644 index 98d6d83ce4..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/HandlerFactory.cs +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using Microsoft.AspNetCore.Http; - -namespace Microsoft.AspNetCore.Dispatcher -{ - /// - /// A simple implementation of that adapts a particular endpoint - /// type to a handler. - /// - public sealed class HandlerFactory : IHandlerFactory - { - private readonly Func> _adapter; - - public HandlerFactory(Func> adapter) - { - if (adapter == null) - { - throw new ArgumentNullException(nameof(adapter)); - } - - _adapter = adapter; - } - - public Func CreateHandler(Endpoint endpoint) - { - if (endpoint == null) - { - throw new ArgumentNullException(nameof(endpoint)); - } - - if (endpoint is TEndpoint myTypeOfEndpoint) - { - return _adapter(myTypeOfEndpoint); - } - - return null; - } - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher/HttpMethodEndpointSelector.cs b/src/Microsoft.AspNetCore.Dispatcher/HttpMethodEndpointSelector.cs deleted file mode 100644 index d860e3101e..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/HttpMethodEndpointSelector.cs +++ /dev/null @@ -1,105 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -namespace Microsoft.AspNetCore.Dispatcher -{ - public class HttpMethodEndpointSelector : EndpointSelector - { - private object _lock; - private bool _servicesInitialized; - - public HttpMethodEndpointSelector() - { - _lock = new object(); - } - - protected ILogger Logger { get; private set; } - - public override async Task SelectAsync(EndpointSelectorContext context) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - EnsureServicesInitialized(context); - - var snapshot = context.CreateSnapshot(); - - var fallbackEndpoints = new List(); - for (var i = context.Endpoints.Count - 1; i >= 0; i--) - { - var endpoint = context.Endpoints[i] as IRoutePatternEndpoint; - if (endpoint == null || endpoint.HttpMethod == null) - { - // No metadata. - Logger.NoHttpMethodFound(context.Endpoints[i]); - - fallbackEndpoints.Add(context.Endpoints[i]); - context.Endpoints.RemoveAt(i); - } - else if (string.Equals(endpoint.HttpMethod, context.HttpContext.Request.Method, StringComparison.OrdinalIgnoreCase)) - { - // The request method matches the endpoint's HTTP method. - Logger.RequestMethodMatchedEndpointMethod(endpoint.HttpMethod, context.Endpoints[i]); - } - else - { - // Not a match. - Logger.RequestMethodDidNotMatchEndpointMethod(context.HttpContext.Request.Method, endpoint.HttpMethod, context.Endpoints[i]); - context.Endpoints.RemoveAt(i); - } - } - - // Now the list of endpoints only contains those that have an HTTP method preference AND match the current - // request. - await context.InvokeNextAsync(); - - if (context.Endpoints.Count == 0) - { - Logger.NoEndpointMatchedRequestMethod(context.HttpContext.Request.Method); - - // Nothing matched, do the fallback. - context.RestoreSnapshot(snapshot); - - context.Endpoints.Clear(); - - for (var i = 0; i < fallbackEndpoints.Count; i++) - { - context.Endpoints.Add(fallbackEndpoints[i]); - } - - await context.InvokeNextAsync(); - } - } - - protected void EnsureServicesInitialized(EndpointSelectorContext context) - { - if (Volatile.Read(ref _servicesInitialized)) - { - return; - } - - EnsureServicesInitializedSlow(context); - } - - private void EnsureServicesInitializedSlow(EndpointSelectorContext context) - { - lock (_lock) - { - if (!Volatile.Read(ref _servicesInitialized)) - { - var services = context.HttpContext.RequestServices; - Logger = services.GetRequiredService().CreateLogger(GetType()); - } - } - } - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher/IAddressCollectionProvider.cs b/src/Microsoft.AspNetCore.Dispatcher/IAddressCollectionProvider.cs deleted file mode 100644 index 75f942bcb3..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/IAddressCollectionProvider.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Collections.Generic; -using Microsoft.Extensions.Primitives; - -namespace Microsoft.AspNetCore.Dispatcher -{ - public interface IAddressCollectionProvider - { - IReadOnlyList
Addresses { get; } - - IChangeToken ChangeToken { get; } - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher/IDefaultMatcherFactory.cs b/src/Microsoft.AspNetCore.Dispatcher/IDefaultMatcherFactory.cs deleted file mode 100644 index 45a7ace09d..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/IDefaultMatcherFactory.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Collections.Generic; - -namespace Microsoft.AspNetCore.Dispatcher -{ - public interface IDefaultMatcherFactory - { - IMatcher CreateMatcher(DispatcherDataSource dataSource, IEnumerable endpointSelectors); - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher/IEndpointCollectionProvider.cs b/src/Microsoft.AspNetCore.Dispatcher/IEndpointCollectionProvider.cs deleted file mode 100644 index b4c5fa1d12..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/IEndpointCollectionProvider.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Collections.Generic; -using Microsoft.Extensions.Primitives; - -namespace Microsoft.AspNetCore.Dispatcher -{ - public interface IEndpointCollectionProvider - { - IReadOnlyList Endpoints { get; } - - IChangeToken ChangeToken { get; } - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher/IEndpointOrderMetadata.cs b/src/Microsoft.AspNetCore.Dispatcher/IEndpointOrderMetadata.cs deleted file mode 100644 index 31b270ae8f..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/IEndpointOrderMetadata.cs +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -namespace Microsoft.AspNetCore.Dispatcher -{ - public interface IEndpointOrderMetadata - { - int Order { get; } - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher/IHandlerFactory.cs b/src/Microsoft.AspNetCore.Dispatcher/IHandlerFactory.cs deleted file mode 100644 index f534cfb642..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/IHandlerFactory.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using Microsoft.AspNetCore.Http; - -namespace Microsoft.AspNetCore.Dispatcher -{ - /// - /// - /// An interface for components that can create a middleware-like delegate from an . - /// - /// - /// Implementations registered in the application services using the service type - /// will be automatically added to set of handler factories used by the default dispatcher. - /// - /// - public interface IHandlerFactory - { - /// - /// Creates a middleware-like delegate for the provided . - /// - /// The that will execute for the current request. - /// An or null. - Func CreateHandler(Endpoint endpoint); - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher/IMatcher.cs b/src/Microsoft.AspNetCore.Dispatcher/IMatcher.cs deleted file mode 100644 index a9762a3c38..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/IMatcher.cs +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; - -namespace Microsoft.AspNetCore.Dispatcher -{ - /// - /// An interface for components that can select an given the current request, as part - /// of the execution of . - /// - /// - /// - /// implementations can also optionally implement the - /// and interfaces to provide addition information. - /// - /// - /// Use to register instances of that will be used by the - /// . - /// - /// - public interface IMatcher - { - /// - /// Attempts to asynchronously select an for the current request. - /// - /// The associated with the current request. - /// A which represents the asynchronous completion of the operation. - /// - /// - /// An implementation should use data from the current request () to select the - /// and set and optionally - /// to indicate a successful result. - /// - /// - /// If the matcher encounters an immediate failure condition, the implementation should set - /// to a that will respond to the current request. - /// - /// - Task MatchAsync(MatcherContext context); - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher/IRoutePatternAddress.cs b/src/Microsoft.AspNetCore.Dispatcher/IRoutePatternAddress.cs deleted file mode 100644 index ace0c2974d..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/IRoutePatternAddress.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -namespace Microsoft.AspNetCore.Dispatcher -{ - public interface IRoutePatternAddress - { - string Pattern { get; } - - DispatcherValueCollection Defaults { get; } - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher/IRoutePatternEndpoint.cs b/src/Microsoft.AspNetCore.Dispatcher/IRoutePatternEndpoint.cs deleted file mode 100644 index ad583762f6..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/IRoutePatternEndpoint.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -namespace Microsoft.AspNetCore.Dispatcher -{ - public interface IRoutePatternEndpoint - { - string HttpMethod { get; } - - string Pattern { get; } - - DispatcherValueCollection Values { get; } - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher/ITemplateFactoryComponent.cs b/src/Microsoft.AspNetCore.Dispatcher/ITemplateFactoryComponent.cs deleted file mode 100644 index 071f3e1913..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/ITemplateFactoryComponent.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -namespace Microsoft.AspNetCore.Dispatcher -{ - public interface ITemplateFactoryComponent - { - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher/LoggerExtensions.cs b/src/Microsoft.AspNetCore.Dispatcher/LoggerExtensions.cs deleted file mode 100644 index 6143a9b3e1..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/LoggerExtensions.cs +++ /dev/null @@ -1,197 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; - -namespace Microsoft.AspNetCore.Dispatcher -{ - internal static class LoggerExtensions - { - // MatcherBase - private static readonly Action _ambiguousEndpoints = LoggerMessage.Define( - LogLevel.Error, - new EventId(0, "AmbiguousEndpoints"), - "Request matched multiple endpoints resulting in ambiguity. Matching endpoints: {AmbiguousEndpoints}"); - - private static readonly Action _noEndpointsMatched = LoggerMessage.Define( - LogLevel.Debug, - new EventId(1, "NoEndpointsMatched"), - "No endpoints matched the current request path '{PathString}'."); - - private static readonly Action _requestShortCircuitedMatcherBase = LoggerMessage.Define( - LogLevel.Information, - new EventId(2, "RequestShortCircuited_MatcherBase"), - "The current request '{RequestPath}' was short circuited."); - - private static readonly Action _endpointMatchedMatcherBase = LoggerMessage.Define( - LogLevel.Information, - new EventId(3, "EndpointMatched_MatcherBase"), - "Request matched endpoint '{endpointName}'."); - - // DispatcherMiddleware - private static readonly Action _handlerNotCreated = LoggerMessage.Define( - LogLevel.Error, - new EventId(0, "HandlerNotCreated"), - "A handler could not be created for '{MatcherType}'."); - - private static readonly Action _requestShortCircuitedDispatcherMiddleware = LoggerMessage.Define( - LogLevel.Information, - new EventId(1, "RequestShortCircuited_DispatcherMiddleware"), - "The current request '{RequestPath}' was short circuited."); - - private static readonly Action _endpointMatchedDispatcherMiddleware = LoggerMessage.Define( - LogLevel.Information, - new EventId(2, "EndpointMatched_DispatcherMiddleware"), - "Request matched endpoint '{endpointName}'."); - - private static readonly Action _noEndpointsMatchedMatcher = LoggerMessage.Define( - LogLevel.Debug, - new EventId(3, "NoEndpointsMatchedMatcher"), - "No endpoints matched matcher '{Matcher}'."); - - //EndpointMiddleware - private static readonly Action _executingEndpoint = LoggerMessage.Define( - LogLevel.Information, - new EventId(0, "ExecutingEndpoint"), - "Executing endpoint '{EndpointName}'."); - - private static readonly Action _executedEndpoint = LoggerMessage.Define( - LogLevel.Information, - new EventId(1, "ExecutedEndpoint"), - "Executed endpoint '{EndpointName}'."); - - // HttpMethodEndpointSelector - private static readonly Action _noHttpMethodFound = LoggerMessage.Define( - LogLevel.Information, - new EventId(0, "NoHttpMethodFound"), - "No HTTP method specified for endpoint '{EndpointName}'."); - - private static readonly Action _requestMethodMatchedEndpointMethod = LoggerMessage.Define( - LogLevel.Information, - new EventId(1, "RequestMethodMatchedEndpointMethod"), - "Request method matched HTTP method '{Method}' for endpoint '{EndpointName}'."); - - private static readonly Action _requestMethodDidNotMatchEndpointMethod = LoggerMessage.Define( - LogLevel.Information, - new EventId(2, "RequestMethodDidNotMatchEndpointMethod"), - "Request method '{RequestMethod}' did not match HTTP method '{EndpointMethod}' for endpoint '{EndpointName}'."); - - private static readonly Action _noEndpointMatchedRequestMethod = LoggerMessage.Define( - LogLevel.Information, - new EventId(3, "NoEndpointMatchedRequestMethod"), - "No endpoint matched request method '{Method}'."); - - // TreeMatcher - private static readonly Action _requestShortCircuited = LoggerMessage.Define( - LogLevel.Information, - new EventId(3, "RequestShortCircuited"), - "The current request '{RequestPath}' was short circuited."); - - private static readonly Action _matchedRoute = LoggerMessage.Define( - LogLevel.Debug, - 1, - "Request successfully matched the route pattern '{RoutePattern}'."); - - private static readonly Action _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, - IDispatcherValueConstraint routeConstraint) - { - _routeValueDoesNotMatchConstraint(logger, routeValue, routeKey, routeConstraint, null); - } - - public static void RequestShortCircuited(this ILogger logger, MatcherContext matcherContext) - { - var requestPath = matcherContext.HttpContext.Request.Path; - _requestShortCircuited(logger, requestPath, null); - } - - public static void MatchedRoute( - this ILogger logger, - string routePattern) - { - _matchedRoute(logger, routePattern, null); - } - - public static void AmbiguousEndpoints(this ILogger logger, string ambiguousEndpoints) - { - _ambiguousEndpoints(logger, ambiguousEndpoints, null); - } - - public static void EndpointMatchedMatcherBase(this ILogger logger, Endpoint endpoint) - { - _endpointMatchedMatcherBase(logger, endpoint.DisplayName ?? "Unnamed endpoint", null); - } - - public static void NoEndpointsMatched(this ILogger logger, PathString pathString) - { - _noEndpointsMatched(logger, pathString, null); - } - - public static void RequestShortCircuitedMatcherBase(this ILogger logger, MatcherContext matcherContext) - { - var requestPath = matcherContext.HttpContext.Request.Path; - _requestShortCircuitedMatcherBase(logger, requestPath, null); - } - - public static void EndpointMatchedDispatcherMiddleware(this ILogger logger, Endpoint endpoint) - { - _endpointMatchedDispatcherMiddleware(logger, endpoint.DisplayName ?? "Unnamed endpoint", null); - } - - public static void RequestShortCircuitedDispatcherMiddleware(this ILogger logger, MatcherContext matcherContext) - { - var requestPath = matcherContext.HttpContext.Request.Path; - _requestShortCircuitedDispatcherMiddleware(logger, requestPath, null); - } - - public static void HandlerNotCreated(this ILogger logger, MatcherEntry matcher) - { - var matcherType = matcher.GetType(); - _handlerNotCreated(logger, matcherType, null); - } - - public static void NoEndpointsMatchedMatcher(this ILogger logger, IMatcher matcher) - { - _noEndpointsMatchedMatcher(logger, matcher, null); - } - - public static void ExecutingEndpoint(this ILogger logger, Endpoint endpoint) - { - _executingEndpoint(logger, endpoint.DisplayName ?? "Unnamed endpoint", null); - } - - public static void ExecutedEndpoint(this ILogger logger, Endpoint endpoint) - { - _executedEndpoint(logger, endpoint.DisplayName ?? "Unnamed endpoint", null); - } - - public static void NoHttpMethodFound(this ILogger logger, Endpoint endpoint) - { - _noHttpMethodFound(logger, endpoint.DisplayName ?? "Unnamed endpoint", null); - } - - public static void RequestMethodMatchedEndpointMethod(this ILogger logger, string httpMethod, Endpoint endpoint) - { - _requestMethodMatchedEndpointMethod(logger, httpMethod, endpoint.DisplayName ?? "Unnamed endpoint", null); - } - - public static void RequestMethodDidNotMatchEndpointMethod(this ILogger logger, string requestMethod, string endpointMethod, Endpoint endpoint) - { - _requestMethodDidNotMatchEndpointMethod(logger, requestMethod, endpointMethod, endpoint.DisplayName ?? "Unnamed endpoint", null); - } - - public static void NoEndpointMatchedRequestMethod(this ILogger logger, string requestMethod) - { - _noEndpointMatchedRequestMethod(logger, requestMethod, null); - } - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher/MatcherBase.cs b/src/Microsoft.AspNetCore.Dispatcher/MatcherBase.cs deleted file mode 100644 index 45228ca4e0..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/MatcherBase.cs +++ /dev/null @@ -1,209 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.FileProviders; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Primitives; - -namespace Microsoft.AspNetCore.Dispatcher -{ - public abstract class MatcherBase : IMatcher, IAddressCollectionProvider, IEndpointCollectionProvider - { - private List
_addresses; - private List _endpoints; - private List _endpointSelectors; - - private object _lock; - private bool _servicesInitialized; - private bool _selectorsInitialized; - private readonly Func _selectorInitializer; - - public MatcherBase() - { - _lock = new object(); - _selectorInitializer = InitializeSelectors; - } - - protected ILogger Logger { get; private set; } - - public virtual IList
Addresses - { - get - { - if (_addresses == null) - { - _addresses = new List
(); - } - - return _addresses; - } - } - - public virtual DispatcherDataSource DataSource { get; set; } - - public virtual IList Endpoints - { - get - { - if (_endpoints == null) - { - _endpoints = new List(); - } - - return _endpoints; - } - } - - public virtual IList Selectors - { - get - { - if (_endpointSelectors == null) - { - _endpointSelectors = new List(); - } - - return _endpointSelectors; - } - } - - public IChangeToken ChangeToken => DataSource?.ChangeToken ?? NullChangeToken.Singleton; - - IReadOnlyList
IAddressCollectionProvider.Addresses => GetAddresses(); - - IReadOnlyList IEndpointCollectionProvider.Endpoints => GetEndpoints(); - - protected virtual IReadOnlyList
GetAddresses() - { - return ((IAddressCollectionProvider)DataSource)?.Addresses ?? _addresses ?? (IReadOnlyList
)Array.Empty
(); - } - - protected virtual IReadOnlyList GetEndpoints() - { - return ((IEndpointCollectionProvider)DataSource)?.Endpoints ?? _endpoints ?? (IReadOnlyList)Array.Empty(); - } - - public virtual async Task MatchAsync(MatcherContext context) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - EnsureServicesInitialized(context); - - context.Values = await MatchRequestAsync(context.HttpContext); - if (context.Values != null) - { - await SelectEndpointAsync(context, GetEndpoints()); - } - } - - protected virtual Task MatchRequestAsync(HttpContext httpContext) - { - // By default don't apply any criteria or provide any values. - return Task.FromResult(new DispatcherValueCollection()); - } - - protected virtual void InitializeServices(IServiceProvider services) - { - } - - protected virtual async Task SelectEndpointAsync(MatcherContext context, IEnumerable endpoints) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - if (endpoints == null) - { - throw new ArgumentNullException(nameof(endpoints)); - } - - EnsureSelectorsInitialized(); - - var selectorContext = new EndpointSelectorContext(context.HttpContext, context.Values, endpoints.ToList(), Selectors.ToList()); - await selectorContext.InvokeNextAsync(); - - if (selectorContext.ShortCircuit != null) - { - context.ShortCircuit = selectorContext.ShortCircuit; - Logger.RequestShortCircuitedMatcherBase(context); - return; - } - - switch (selectorContext.Endpoints.Count) - { - case 0: - Logger.NoEndpointsMatched(context.HttpContext.Request.Path); - return; - - case 1: - context.Endpoint = selectorContext.Endpoints[0]; - - Logger.EndpointMatchedMatcherBase(context.Endpoint); - return; - - default: - var endpointNames = string.Join( - Environment.NewLine, - selectorContext.Endpoints.Select(a => a.DisplayName)); - - Logger.AmbiguousEndpoints(endpointNames); - - var message = Resources.FormatAmbiguousEndpoints( - Environment.NewLine, - endpointNames); - - throw new AmbiguousEndpointException(message); - } - } - - protected void EnsureSelectorsInitialized() - { - object _ = null; - LazyInitializer.EnsureInitialized(ref _, ref _selectorsInitialized, ref _lock, _selectorInitializer); - } - - private object InitializeSelectors() - { - foreach (var selector in Selectors) - { - selector.Initialize(this); - } - - return null; - } - - protected void EnsureServicesInitialized(MatcherContext context) - { - if (Volatile.Read(ref _servicesInitialized)) - { - return; - } - - EnsureServicesInitializedSlow(context); - } - - private void EnsureServicesInitializedSlow(MatcherContext context) - { - lock (_lock) - { - if (!Volatile.Read(ref _servicesInitialized)) - { - var services = context.HttpContext.RequestServices; - Logger = services.GetRequiredService().CreateLogger(GetType()); - InitializeServices(services); - } - } - } - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher/MatcherCollection.cs b/src/Microsoft.AspNetCore.Dispatcher/MatcherCollection.cs deleted file mode 100644 index b2dbe8a6bf..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/MatcherCollection.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Collections.ObjectModel; - -namespace Microsoft.AspNetCore.Dispatcher -{ - public class MatcherCollection : Collection - { - public void Add(IMatcher matcher, IHandlerFactory handerFactory) - { - if (matcher == null) - { - throw new ArgumentNullException(nameof(matcher)); - } - - Add(new MatcherEntry() - { - Matcher = matcher, - AddressProvider = matcher as IAddressCollectionProvider, - EndpointProvider = matcher as IEndpointCollectionProvider, - - HandlerFactory = handerFactory, - }); - } - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher/MatcherContext.cs b/src/Microsoft.AspNetCore.Dispatcher/MatcherContext.cs deleted file mode 100644 index 6f49fc9885..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/MatcherContext.cs +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using Microsoft.AspNetCore.Http; - -namespace Microsoft.AspNetCore.Dispatcher -{ - /// - /// A context object for . - /// - public class MatcherContext - { - /// - /// Creates a new for the current request. - /// - /// The associated with the current request. - public MatcherContext(HttpContext httpContext) - { - if (httpContext == null) - { - throw new ArgumentNullException(nameof(httpContext)); - } - - HttpContext = httpContext; - } - - /// - /// Gets the associated with the current request. - /// - public HttpContext HttpContext { get; } - - /// - /// Gets or sets the selected by the matcher. - /// - public Endpoint Endpoint { get; set; } - - /// - /// Gets or sets a short-circuit delegate provided by the matcher. - /// - public RequestDelegate ShortCircuit { get; set; } - - /// - /// Gets or sets a provided by the matcher. - /// - public DispatcherValueCollection Values { get; set; } - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher/MatcherEntry.cs b/src/Microsoft.AspNetCore.Dispatcher/MatcherEntry.cs deleted file mode 100644 index 2bc36a44b9..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/MatcherEntry.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -namespace Microsoft.AspNetCore.Dispatcher -{ - public class MatcherEntry - { - public IHandlerFactory HandlerFactory { get; set; } - - public IMatcher Matcher { get; set; } - - public IAddressCollectionProvider AddressProvider { get; set; } - - public IEndpointCollectionProvider EndpointProvider { get; set; } - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher/Microsoft.AspNetCore.Dispatcher.csproj b/src/Microsoft.AspNetCore.Dispatcher/Microsoft.AspNetCore.Dispatcher.csproj deleted file mode 100644 index dc1cfdf225..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/Microsoft.AspNetCore.Dispatcher.csproj +++ /dev/null @@ -1,29 +0,0 @@ - - - - ASP.NET Core middleware for dispatching requests to endpoints and use addresses to generate URLs. - netstandard2.0 - $(DefineConstants);DISPATCHER - $(NoWarn);CS1591 - true - aspnetcore;routing;dispatcher - - - - - - - - - - - - - - - - - - - - diff --git a/src/Microsoft.AspNetCore.Dispatcher/Patterns/ConstraintReference.cs b/src/Microsoft.AspNetCore.Dispatcher/Patterns/ConstraintReference.cs deleted file mode 100644 index 1324c6c4f6..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/Patterns/ConstraintReference.cs +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Diagnostics; - -namespace Microsoft.AspNetCore.Dispatcher.Patterns -{ - /// - /// The parsed representation of a constraint in a parameter. - /// - [DebuggerDisplay("{DebuggerToString()}")] - public sealed class ConstraintReference - { - /// - /// Creates a new . - /// - /// The constraint identifier. - /// A new . - public static ConstraintReference Create(string content) - { - if (content == null) - { - throw new ArgumentNullException(nameof(content)); - } - - return new ConstraintReference(null, content); - } - - /// - /// Creates a new . - /// - /// The raw text of the constraint identifier. - /// The constraint identifier. - /// A new . - public static ConstraintReference CreateFromText(string rawText, string content) - { - if (content == null) - { - throw new ArgumentNullException(nameof(content)); - } - - return new ConstraintReference(rawText, content); - } - - private ConstraintReference(string rawText, string content) - { - RawText = rawText; - Content = content; - } - - /// - /// Gets the constraint text. - /// - public string Content { get; } - - public string RawText { get; } - - private string DebuggerToString() - { - return RawText ?? Content; - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Dispatcher/Patterns/InlineRouteParameterParser.cs b/src/Microsoft.AspNetCore.Dispatcher/Patterns/InlineRouteParameterParser.cs deleted file mode 100644 index 05c4eb4a0a..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/Patterns/InlineRouteParameterParser.cs +++ /dev/null @@ -1,232 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Microsoft.AspNetCore.Dispatcher.Patterns -{ - public static class InlineRouteParameterParser - { - public static RoutePatternParameter ParseRouteParameter(string text, string parameter) - { - if (parameter == null) - { - throw new ArgumentNullException(nameof(parameter)); - } - - if (parameter.Length == 0) - { - return new RoutePatternParameter(null, string.Empty, null, RoutePatternParameterKind.Standard, Array.Empty()); - } - - var startIndex = 0; - var endIndex = parameter.Length - 1; - - var parameterKind = RoutePatternParameterKind.Standard; - if (parameter[0] == '*') - { - parameterKind = RoutePatternParameterKind.CatchAll; - startIndex++; - } - - if (parameter[endIndex] == '?') - { - parameterKind = RoutePatternParameterKind.Optional; - endIndex--; - } - - var currentIndex = startIndex; - - // Parse parameter name - var parameterName = string.Empty; - - while (currentIndex <= endIndex) - { - var currentChar = parameter[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 = parameter.Substring(startIndex, currentIndex - startIndex); - - // Roll the index back and move to the constraint parsing stage. - currentIndex--; - break; - } - else if (currentIndex == endIndex) - { - parameterName = parameter.Substring(startIndex, currentIndex - startIndex + 1); - } - - currentIndex++; - } - - var parseResults = ParseConstraints(parameter, currentIndex, endIndex); - currentIndex = parseResults.CurrentIndex; - - string defaultValue = null; - if (currentIndex <= endIndex && - parameter[currentIndex] == '=') - { - defaultValue = parameter.Substring(currentIndex + 1, endIndex - currentIndex); - } - - return new RoutePatternParameter(text, parameterName, defaultValue, parameterKind, parseResults.Constraints.ToArray()); - } - - private static ConstraintParseResults ParseConstraints( - string parameter, - int currentIndex, - int endIndex) - { - var constraints = new List(); - var state = ParseState.Start; - var startIndex = currentIndex; - do - { - var currentChar = currentIndex > endIndex ? null : (char?)parameter[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 = parameter.Substring(startIndex, currentIndex - startIndex); - constraints.Add(ConstraintReference.CreateFromText(constraintText, 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?)parameter[currentIndex + 1]; - switch (nextChar) - { - case null: - state = ParseState.End; - constraintText = parameter.Substring(startIndex, currentIndex - startIndex + 1); - constraints.Add(ConstraintReference.CreateFromText(constraintText, constraintText)); - break; - case ':': - state = ParseState.Start; - constraintText = parameter.Substring(startIndex, currentIndex - startIndex + 1); - constraints.Add(ConstraintReference.CreateFromText(constraintText, constraintText)); - startIndex = currentIndex + 1; - break; - case '=': - state = ParseState.End; - constraintText = parameter.Substring(startIndex, currentIndex - startIndex + 1); - constraints.Add(ConstraintReference.CreateFromText(constraintText, 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 = parameter.IndexOf(')', currentIndex + 1); - if (indexOfClosingParantheses == -1) - { - constraintText = parameter.Substring(startIndex, currentIndex - startIndex); - constraints.Add(ConstraintReference.CreateFromText(constraintText, 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 = parameter.Substring(startIndex, currentIndex - startIndex); - constraints.Add(ConstraintReference.CreateFromText(constraintText, constraintText)); - break; - case ':': - constraintText = parameter.Substring(startIndex, currentIndex - startIndex); - constraints.Add(ConstraintReference.CreateFromText(constraintText, constraintText)); - startIndex = currentIndex + 1; - break; - case '(': - state = ParseState.InsideParenthesis; - break; - case '=': - state = ParseState.End; - constraintText = parameter.Substring(startIndex, currentIndex - startIndex); - constraints.Add(ConstraintReference.CreateFromText(constraintText, constraintText)); - currentIndex--; - break; - } - break; - } - - currentIndex++; - - } while (state != ParseState.End); - - return new ConstraintParseResults - { - CurrentIndex = currentIndex, - Constraints = constraints - }; - } - - 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/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePattern.cs b/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePattern.cs deleted file mode 100644 index c2100fcaa0..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePattern.cs +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; - -namespace Microsoft.AspNetCore.Dispatcher.Patterns -{ - [DebuggerDisplay("{DebuggerToString()}")] - public sealed class RoutePattern - { - private const string SeparatorString = "/"; - - internal RoutePattern( - string rawText, - RoutePatternParameter[] parameters, - RoutePatternPathSegment[] pathSegments) - { - Debug.Assert(parameters != null); - Debug.Assert(pathSegments != null); - - RawText = rawText; - Parameters = parameters; - PathSegments = pathSegments; - } - - public string RawText { get; } - - public IReadOnlyList Parameters { get; } - - public IReadOnlyList PathSegments { get; } - - public static RoutePattern Parse(string pattern) - { - try - { - return RoutePatternParser.Parse(pattern); - } - catch (RoutePatternException ex) - { - throw new ArgumentException(ex.Message, nameof(pattern), ex); - } - } - - /// - /// 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 RoutePatternParameter 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; - } - - private string DebuggerToString() - { - return RawText ?? string.Join(SeparatorString, PathSegments.Select(s => s.DebuggerToString())); - } - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternBuilder.cs b/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternBuilder.cs deleted file mode 100644 index b6d8fb4842..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternBuilder.cs +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Microsoft.AspNetCore.Dispatcher.Patterns -{ - public sealed class RoutePatternBuilder - { - private RoutePatternBuilder() - { - } - - public IList PathSegments { get; } = new List(); - - public string RawText { get; set; } - - public RoutePatternBuilder AddPathSegment(params RoutePatternPart[] parts) - { - if (parts == null) - { - throw new ArgumentNullException(nameof(parts)); - } - - if (parts.Length == 0) - { - throw new ArgumentException(Resources.RoutePatternBuilder_CollectionCannotBeEmpty, nameof(parts)); - } - - return AddPathSegmentFromText(null, parts); - } - - public RoutePatternBuilder AddPathSegmentFromText(string text, params RoutePatternPart[] parts) - { - if (parts == null) - { - throw new ArgumentNullException(nameof(parts)); - } - - if (parts.Length == 0) - { - throw new ArgumentException(Resources.RoutePatternBuilder_CollectionCannotBeEmpty, nameof(parts)); - } - - var segment = new RoutePatternPathSegment(text, parts.ToArray()); - PathSegments.Add(segment); - - return this; - } - - public RoutePattern Build() - { - var parameters = new List(); - for (var i = 0; i < PathSegments.Count; i++) - { - var segment = PathSegments[i]; - for (var j = 0; j < segment.Parts.Count; j++) - { - if (segment.Parts[j] is RoutePatternParameter parameter) - { - parameters.Add(parameter); - } - } - } - - return new RoutePattern(RawText, parameters.ToArray(), PathSegments.ToArray()); - } - - public static RoutePatternBuilder Create(string text) - { - return new RoutePatternBuilder() { RawText = text, }; - } - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternException.cs b/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternException.cs deleted file mode 100644 index 3ff669f318..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternException.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; - -namespace Microsoft.AspNetCore.Dispatcher.Patterns -{ - public class RoutePatternException : Exception - { - public RoutePatternException(string pattern, string message) - : base(message) - { - if (pattern == null) - { - throw new ArgumentNullException(nameof(pattern)); - } - - if (message == null) - { - throw new ArgumentNullException(nameof(message)); - } - - Pattern = pattern; - } - - public string Pattern { get; } - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternLiteral.cs b/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternLiteral.cs deleted file mode 100644 index 11868416f0..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternLiteral.cs +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Diagnostics; - -namespace Microsoft.AspNetCore.Dispatcher.Patterns -{ - [DebuggerDisplay("{DebuggerToString()}")] - public sealed class RoutePatternLiteral : RoutePatternPart - { - internal RoutePatternLiteral(string rawText, string content) - { - Debug.Assert(!string.IsNullOrEmpty(content)); - - RawText = rawText; - Content = content; - - PartKind = RoutePatternPartKind.Literal; - } - - public string Content { get; } - - public override RoutePatternPartKind PartKind { get; } - - public override string RawText { get; } - - internal override string DebuggerToString() - { - return RawText; - } - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternMatcher.cs b/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternMatcher.cs deleted file mode 100644 index 3da6a745c4..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternMatcher.cs +++ /dev/null @@ -1,511 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using Microsoft.AspNetCore.Dispatcher.Internal; -using Microsoft.AspNetCore.Dispatcher.Patterns; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Primitives; - -namespace Microsoft.AspNetCore.Dispatcher -{ - public class RoutePatternMatcher - { - 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 RoutePatternMatcher( - RoutePattern routePattern, - DispatcherValueCollection defaults) - { - if (routePattern == null) - { - throw new ArgumentNullException(nameof(routePattern)); - } - - RoutePattern = routePattern; - Defaults = defaults ?? new DispatcherValueCollection(); - - // Perf: cache the default value for each parameter (other than complex segments). - _hasDefaultValue = new bool[RoutePattern.PathSegments.Count]; - _defaultValues = new object[RoutePattern.PathSegments.Count]; - - for (var i = 0; i < RoutePattern.PathSegments.Count; i++) - { - var segment = RoutePattern.PathSegments[i]; - if (!segment.IsSimple) - { - continue; - } - - var part = segment.Parts[0]; - if (!part.IsParameter) - { - continue; - } - - var parameter = (RoutePatternParameter)part; - if (Defaults.TryGetValue(parameter.Name, out var value)) - { - _hasDefaultValue[i] = true; - _defaultValues[i] = value; - } - } - } - - public DispatcherValueCollection Defaults { get; } - - public RoutePattern RoutePattern { get; } - - public bool TryMatch(PathString path, DispatcherValueCollection 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 stringSegment in pathTokenizer) - { - if (stringSegment.Length == 0) - { - return false; - } - - var pathSegment = i >= RoutePattern.PathSegments.Count ? null : RoutePattern.PathSegments[i]; - if (pathSegment == null && stringSegment.Length > 0) - { - // If pathSegment is null, then we're out of route segments. All we can match is the empty - // string. - return false; - } - else if (pathSegment.IsSimple && pathSegment.Parts[0] is RoutePatternParameter parameter && parameter.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 (!TryMatchLiterals(i++, stringSegment, pathSegment)) - { - return false; - } - } - - for (; i < RoutePattern.PathSegments.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 pathSegment = RoutePattern.PathSegments[i]; - Debug.Assert(pathSegment != null); - - if (!pathSegment.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 = pathSegment.Parts[0]; - if (part.IsLiteral || part.IsSeparator) - { - // If the segment is a simple literal - which need the URL to provide a value, so we don't match. - return false; - } - - var parameter = (RoutePatternParameter)part; - if (parameter.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. - if (!_hasDefaultValue[i] && !parameter.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 pathSegment = RoutePattern.PathSegments[i++]; - if (SavePathSegmentsAsValues(i, values, requestSegment, pathSegment)) - { - break; - } - if (!pathSegment.IsSimple) - { - if (!MatchComplexSegment(pathSegment, requestSegment.ToString(), Defaults, values)) - { - return false; - } - } - } - - for (; i < RoutePattern.PathSegments.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 pathSegment = RoutePattern.PathSegments[i]; - Debug.Assert(pathSegment != null); - Debug.Assert(pathSegment.IsSimple); - - var part = pathSegment.Parts[0]; - Debug.Assert(part.IsParameter); - - // It's ok for a catch-all to produce a null value - if (part is RoutePatternParameter parameter && (parameter.IsCatchAll || _hasDefaultValue[i])) - { - // Don't replace an existing value with a null. - var defaultValue = _defaultValues[i]; - if (defaultValue != null || !values.ContainsKey(parameter.Name)) - { - values[parameter.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 TryMatchLiterals(int index, StringSegment stringSegment, RoutePatternPathSegment pathSegment) - { - if (pathSegment.IsSimple && !pathSegment.Parts[0].IsParameter) - { - // This is a literal segment, so we need to match the text, or the route isn't a match. - if (pathSegment.Parts[0].IsLiteral) - { - var part = (RoutePatternLiteral)pathSegment.Parts[0]; - - if (!stringSegment.Equals(part.Content, StringComparison.OrdinalIgnoreCase)) - { - return false; - } - } - else - { - var part = (RoutePatternSeparator)pathSegment.Parts[0]; - - if (!stringSegment.Equals(part.Content, StringComparison.OrdinalIgnoreCase)) - { - return false; - } - } - } - else if (pathSegment.IsSimple && pathSegment.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 = (RoutePatternParameter)pathSegment.Parts[0]; - if (stringSegment.Length == 0 && - !_hasDefaultValue[index] && - !part.IsOptional) - { - // There's no value for this parameter, the route can't match. - return false; - } - } - else - { - Debug.Assert(!pathSegment.IsSimple); - // Don't attempt to validate a complex segment at this point other than being non-emtpy, - // do it in the second pass. - } - return true; - } - - private bool SavePathSegmentsAsValues(int index, DispatcherValueCollection values, StringSegment requestSegment, RoutePatternPathSegment pathSegment) - { - if (pathSegment.IsSimple && pathSegment.Parts[0] is RoutePatternParameter parameter && parameter.IsCatchAll) - { - // A catch-all captures til the end of the string. - var captured = requestSegment.Buffer.Substring(requestSegment.Offset); - if (captured.Length > 0) - { - values[parameter.Name] = captured; - } - else - { - // It's ok for a catch-all to produce a null value, so we don't check _hasDefaultValue. - values[parameter.Name] = _defaultValues[index]; - } - - // A catch-all has to be the last part, so we're done. - return true; - } - else if (pathSegment.IsSimple && pathSegment.Parts[0].IsParameter) - { - // A simple parameter captures the whole segment, or a default value if nothing was - // provided. - parameter = (RoutePatternParameter)pathSegment.Parts[0]; - if (requestSegment.Length > 0) - { - values[parameter.Name] = requestSegment.ToString(); - } - else - { - if (_hasDefaultValue[index]) - { - values[parameter.Name] = _defaultValues[index]; - } - } - } - return false; - } - - private bool MatchComplexSegment( - RoutePatternPathSegment routeSegment, - string requestSegment, - IReadOnlyDictionary defaults, - DispatcherValueCollection 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] is RoutePatternParameter parameter && parameter.IsOptional && - routeSegment.Parts[indexOfLastSegment - 1].IsSeparator) - { - if (MatchComplexSegmentCore(routeSegment, requestSegment, Defaults, values, indexOfLastSegment)) - { - return true; - } - else - { - var separator = (RoutePatternSeparator)routeSegment.Parts[indexOfLastSegment - 1]; - if (requestSegment.EndsWith( - separator.Content, - StringComparison.OrdinalIgnoreCase)) - return false; - - return MatchComplexSegmentCore( - routeSegment, - requestSegment, - Defaults, - values, - indexOfLastSegment - 2); - } - } - else - { - return MatchComplexSegmentCore(routeSegment, requestSegment, Defaults, values, indexOfLastSegment); - } - } - - private bool MatchComplexSegmentCore( - RoutePatternPathSegment routeSegment, - string requestSegment, - IReadOnlyDictionary defaults, - DispatcherValueCollection 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; - - RoutePatternParameter parameterNeedsValue = null; // Keeps track of a parameter segment that is pending a value - RoutePatternPart lastLiteral = null; // Keeps track of the left-most literal we've encountered - - var outValues = new DispatcherValueCollection(); - - 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 = (RoutePatternParameter)part; - } - else - { - Debug.Assert(part.IsLiteral || part.IsSeparator); - 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; - } - - int indexOfLiteral; - if (part.IsLiteral) - { - var literal = (RoutePatternLiteral)part; - indexOfLiteral = requestSegment.LastIndexOf( - literal.Content, - startIndex, - StringComparison.OrdinalIgnoreCase); - } - else - { - var literal = (RoutePatternSeparator)part; - indexOfLiteral = requestSegment.LastIndexOf( - literal.Content, - 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 (part is RoutePatternLiteral literal && ((indexOfLiteral + literal.Content.Length) != requestSegment.Length)) - { - return false; - } - else if (part is RoutePatternSeparator separator && ((indexOfLiteral + separator.Content.Length) != requestSegment.Length)) - { - return false; - } - } - - newLastIndex = indexOfLiteral; - } - - if ((parameterNeedsValue != null) && - (((lastLiteral != null) && !part.IsParameter) || (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 - { - if (lastLiteral.IsLiteral) - { - var literal = (RoutePatternLiteral)lastLiteral; - parameterStartIndex = newLastIndex + literal.Content.Length; - } - else - { - var separator = (RoutePatternSeparator)lastLiteral; - parameterStartIndex = newLastIndex + separator.Content.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/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternParameter.cs b/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternParameter.cs deleted file mode 100644 index 14395dd8ba..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternParameter.cs +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Collections.Generic; -using System.Diagnostics; - -namespace Microsoft.AspNetCore.Dispatcher.Patterns -{ - [DebuggerDisplay("{DebuggerToString()}")] - public class RoutePatternParameter : RoutePatternPart - { - internal RoutePatternParameter( - string rawText, - string name, - object defaultValue, - RoutePatternParameterKind parameterKind, - ConstraintReference[] constraints) - { - // See #475 - this code should have some asserts, but it can't because of the design of InlineRouteParameterParser. - - RawText = rawText; - Name = name; - DefaultValue = defaultValue; - ParameterKind = parameterKind; - Constraints = constraints; - - PartKind = RoutePatternPartKind.Parameter; - } - - public IReadOnlyList Constraints { get; } - - public object DefaultValue { get; } - - public bool IsCatchAll => ParameterKind == RoutePatternParameterKind.CatchAll; - - public bool IsOptional => ParameterKind == RoutePatternParameterKind.Optional; - - public RoutePatternParameterKind ParameterKind { get; } - - public override RoutePatternPartKind PartKind { get; } - - public string Name { get; } - - public override string RawText { get; } - - internal override string DebuggerToString() - { - return RawText ?? "{" + (IsCatchAll ? "*" : string.Empty) + Name + (IsOptional ? "?" : string.Empty) + "}"; - } - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternParameterKind.cs b/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternParameterKind.cs deleted file mode 100644 index 8dcacafa70..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternParameterKind.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -namespace Microsoft.AspNetCore.Dispatcher.Patterns -{ - public enum RoutePatternParameterKind - { - Standard, - Optional, - CatchAll, - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternParser.cs b/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternParser.cs deleted file mode 100644 index 4e6e792a10..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternParser.cs +++ /dev/null @@ -1,579 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Diagnostics; - -namespace Microsoft.AspNetCore.Dispatcher.Patterns -{ - internal static class RoutePatternParser - { - 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 = "."; - - internal static readonly char[] InvalidParameterNameChars = new char[] - { - Separator, - OpenBrace, - CloseBrace, - QuestionMark, - Asterisk - }; - - public static RoutePattern Parse(string pattern) - { - if (pattern == null) - { - throw new ArgumentNullException(nameof(pattern)); - } - - var trimmedPattern = TrimPrefix(pattern); - - var context = new TemplateParserContext(trimmedPattern); - var segments = new List(); - - while (context.MoveNext()) - { - var i = context.Index; - - 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 RoutePatternException(pattern, Resources.TemplateRoute_CannotHaveConsecutiveSeparators); - } - - if (!ParseSegment(context, segments)) - { - throw new RoutePatternException(pattern, context.Error); - } - - // A successful parse should always result in us being at the end or at a separator. - Debug.Assert(context.AtEnd() || context.Current == Separator); - - if (context.Index <= i) - { - throw new InvalidProgramException("Infinite loop in the parser. This is a bug."); - } - } - - if (IsAllValid(context, segments)) - { - var builder = RoutePatternBuilder.Create(pattern); - for (var i = 0; i < segments.Count; i++) - { - builder.PathSegments.Add(segments[i]); - } - - return builder.Build(); - } - else - { - throw new RoutePatternException(pattern, context.Error); - } - } - - private static bool ParseSegment(TemplateParserContext context, List segments) - { - Debug.Assert(context != null); - Debug.Assert(segments != null); - - var parts = new List(); - - while (true) - { - var i = context.Index; - - if (context.Current == OpenBrace) - { - if (!context.MoveNext()) - { - // 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, parts)) - { - return false; - } - } - else - { - // This is a parameter - context.Back(); - if (!ParseParameter(context, parts)) - { - return false; - } - } - } - else - { - if (!ParseLiteral(context, parts)) - { - return false; - } - } - - if (context.Current == Separator || context.AtEnd()) - { - // We've reached the end of the segment - break; - } - - if (context.Index <= i) - { - throw new InvalidProgramException("Infinite loop in the parser. This is a bug."); - } - } - - if (IsSegmentValid(context, parts)) - { - segments.Add(new RoutePatternPathSegment(null, parts.ToArray())); - return true; - } - else - { - return false; - } - } - - private static bool ParseParameter(TemplateParserContext context, List parts) - { - Debug.Assert(context.Current == OpenBrace); - context.Mark(); - - context.MoveNext(); - - while (true) - { - if (context.Current == OpenBrace) - { - // This is an open brace inside of a parameter, it has to be escaped - if (context.MoveNext()) - { - 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.MoveNext()) - { - // This is the end of the string -and we have a valid parameter - break; - } - - if (context.Current == CloseBrace) - { - // This is an 'escaped' brace in a parameter name - } - else - { - // This is the end of the parameter - break; - } - } - - if (!context.MoveNext()) - { - // This is a dangling open-brace, which is not allowed - context.Error = Resources.TemplateRoute_MismatchedParameter; - return false; - } - } - - var text = context.Capture(); - if (text == "{}") - { - context.Error = Resources.FormatTemplateRoute_InvalidParameterName(string.Empty); - return false; - } - - var inside = text.Substring(1, text.Length - 2); - var decoded = inside.Replace("}}", "}").Replace("{{", "{"); - - // At this point, we need to parse the raw name for inline constraint, - // default values and optional parameters. - var templatePart = InlineRouteParameterParser.ParseRouteParameter(text, decoded); - - // See #475 - this is here because InlineRouteParameterParser can't return errors - if (decoded.StartsWith("*") && decoded.EndsWith("?")) - { - 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)) - { - parts.Add(templatePart); - return true; - } - else - { - return false; - } - } - - private static bool ParseLiteral(TemplateParserContext context, List parts) - { - context.Mark(); - - while (true) - { - if (context.Current == Separator) - { - // End of the segment - break; - } - else if (context.Current == OpenBrace) - { - if (!context.MoveNext()) - { - // 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. - context.Back(); - break; - } - } - else if (context.Current == CloseBrace) - { - if (!context.MoveNext()) - { - // 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.MoveNext()) - { - break; - } - } - - var encoded = context.Capture(); - var decoded = encoded.Replace("}}", "}").Replace("{{", "{"); - if (IsValidLiteral(context, decoded)) - { - parts.Add(RoutePatternPart.CreateLiteralFromText(encoded, 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 && - ((RoutePatternParameter)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, List parts) - { - // If a segment has multiple parts, then it can't contain a catch all. - for (var i = 0; i < parts.Count; i++) - { - var part = parts[i]; - if (part.IsParameter && ((RoutePatternParameter)part).IsCatchAll && 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 < parts.Count; i++) - { - var part = parts[i]; - - if (part.IsParameter && ((RoutePatternParameter)part).IsOptional && parts.Count > 1) - { - // This optional parameter is the last part in the segment - if (i == parts.Count - 1) - { - var previousPart = parts[i - 1]; - - if (!previousPart.IsLiteral && !previousPart.IsSeparator) - { - // The optional parameter is preceded by something that is not a literal or separator - // 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, - RoutePatternPathSegment.DebuggerToString(parts), - ((RoutePatternParameter)part).Name, - parts[i - 1].DebuggerToString()); - - return false; - } - else if (previousPart is RoutePatternLiteral literal && literal.Content != 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, - RoutePatternPathSegment.DebuggerToString(parts), - ((RoutePatternParameter)part).Name, - parts[i - 1].DebuggerToString()); - - return false; - } - - parts[i - 1] = RoutePatternPart.CreateSeparatorFromText(previousPart.RawText, ((RoutePatternLiteral)previousPart).Content); - } - 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 ')' - context.Error = string.Format( - Resources.TemplateRoute_OptionalParameterHasTobeTheLast, - RoutePatternPathSegment.DebuggerToString(parts), - ((RoutePatternParameter)part).Name, - parts[i + 1].DebuggerToString()); - - return false; - } - } - } - - // A segment cannot contain two consecutive parameters - var isLastSegmentParameter = false; - for (var i = 0; i < parts.Count; i++) - { - var part = 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 || parameterName.IndexOfAny(InvalidParameterNameChars) >= 0) - { - context.Error = Resources.FormatTemplateRoute_InvalidParameterName(parameterName); - return false; - } - - if (!context.ParameterNames.Add(parameterName)) - { - context.Error = Resources.FormatTemplateRoute_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 = Resources.FormatTemplateRoute_InvalidLiteral(literal); - return false; - } - - return true; - } - - private static string TrimPrefix(string routePattern) - { - if (routePattern.StartsWith("~/", StringComparison.Ordinal)) - { - return routePattern.Substring(2); - } - else if (routePattern.StartsWith("/", StringComparison.Ordinal)) - { - return routePattern.Substring(1); - } - else if (routePattern.StartsWith("~", StringComparison.Ordinal)) - { - throw new RoutePatternException(routePattern, Resources.TemplateRoute_InvalidRouteTemplate); - } - return routePattern; - } - - [DebuggerDisplay("{DebuggerToString()}")] - 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 int Index => _index; - - public string Error - { - get; - set; - } - - public HashSet ParameterNames - { - get { return _parameterNames; } - } - - public bool Back() - { - return --_index >= 0; - } - - public bool AtEnd() - { - return _index >= _template.Length; - } - - public bool MoveNext() - { - return ++_index < _template.Length; - } - - public void Mark() - { - Debug.Assert(_index >= 0); - - // Index is always the index of the character *past* Current - we want to 'mark' Current. - _mark = _index; - } - - public string Capture() - { - if (_mark.HasValue) - { - var value = _template.Substring(_mark.Value, _index - _mark.Value); - _mark = null; - return value; - } - else - { - return null; - } - } - - private string DebuggerToString() - { - if (_index == -1) - { - return _template; - } - else if (_mark.HasValue) - { - return _template.Substring(0, _mark.Value) + - "|" + - _template.Substring(_mark.Value, _index - _mark.Value) + - "|" + - _template.Substring(_index); - } - else - { - return _template.Substring(0, _index) + "|" + _template.Substring(_index); - } - } - } - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternPart.cs b/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternPart.cs deleted file mode 100644 index 8f8733ea88..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternPart.cs +++ /dev/null @@ -1,235 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; - -namespace Microsoft.AspNetCore.Dispatcher.Patterns -{ - public abstract class RoutePatternPart - { - // This class is not an extensibility point. It is abstract so we can extend it - // or add semantics later inside the library. - internal RoutePatternPart() - { - } - - public static RoutePatternLiteral CreateLiteral(string content) - { - if (string.IsNullOrEmpty(content)) - { - throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(content)); - } - - if (content.IndexOf('?') >= 0) - { - throw new ArgumentException(Resources.FormatTemplateRoute_InvalidLiteral(content)); - } - - return new RoutePatternLiteral(null, content); - } - - public static RoutePatternLiteral CreateLiteralFromText(string rawText, string content) - { - if (string.IsNullOrEmpty(content)) - { - throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(content)); - } - - if (content.IndexOf('?') >= 0) - { - throw new ArgumentException(Resources.FormatTemplateRoute_InvalidLiteral(content)); - } - - return new RoutePatternLiteral(rawText, content); - } - - public static RoutePatternSeparator CreateSeparator(string content) - { - if (string.IsNullOrEmpty(content)) - { - throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(content)); - } - - return new RoutePatternSeparator(null, content); - } - - public static RoutePatternSeparator CreateSeparatorFromText(string rawText, string content) - { - if (string.IsNullOrEmpty(content)) - { - throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(content)); - } - - return new RoutePatternSeparator(rawText, content); - } - - public static RoutePatternParameter CreateParameter(string name) - { - if (string.IsNullOrEmpty(name)) - { - throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(name)); - } - - if (name.IndexOfAny(RoutePatternParser.InvalidParameterNameChars) >= 0) - { - throw new ArgumentException(Resources.FormatTemplateRoute_InvalidParameterName(name)); - } - - return CreateParameterFromText(null, name, null, RoutePatternParameterKind.Standard, Array.Empty()); - } - - public static RoutePatternParameter CreateParameterFromText(string rawText, string name) - { - if (string.IsNullOrEmpty(name)) - { - throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(name)); - } - - if (name.IndexOfAny(RoutePatternParser.InvalidParameterNameChars) >= 0) - { - throw new ArgumentException(Resources.FormatTemplateRoute_InvalidParameterName(name)); - } - - return CreateParameterFromText(rawText, name, null, RoutePatternParameterKind.Standard, Array.Empty()); - } - - public static RoutePatternParameter CreateParameter(string name, object defaultValue) - { - if (string.IsNullOrEmpty(name)) - { - throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(name)); - } - - if (name.IndexOfAny(RoutePatternParser.InvalidParameterNameChars) >= 0) - { - throw new ArgumentException(Resources.FormatTemplateRoute_InvalidParameterName(name)); - } - - return CreateParameterFromText(null, name, defaultValue, RoutePatternParameterKind.Standard, Array.Empty()); - } - - public static RoutePatternParameter CreateParameterFromText(string rawText, string name, object defaultValue) - { - if (string.IsNullOrEmpty(name)) - { - throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(name)); - } - - if (name.IndexOfAny(RoutePatternParser.InvalidParameterNameChars) >= 0) - { - throw new ArgumentException(Resources.FormatTemplateRoute_InvalidParameterName(name)); - } - - return CreateParameterFromText(rawText, name, defaultValue, RoutePatternParameterKind.Standard, Array.Empty()); - } - - public static RoutePatternParameter CreateParameter( - string name, - object defaultValue, - RoutePatternParameterKind parameterKind) - { - if (string.IsNullOrEmpty(name)) - { - throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(name)); - } - - if (name.IndexOfAny(RoutePatternParser.InvalidParameterNameChars) >= 0) - { - throw new ArgumentException(Resources.FormatTemplateRoute_InvalidParameterName(name)); - } - - if (defaultValue != null && parameterKind == RoutePatternParameterKind.Optional) - { - throw new ArgumentNullException(Resources.TemplateRoute_OptionalCannotHaveDefaultValue, nameof(parameterKind)); - } - - return CreateParameterFromText(null, name, defaultValue, parameterKind, Array.Empty()); - } - - public static RoutePatternParameter CreateParameterFromText( - string rawText, - string name, - object defaultValue, - RoutePatternParameterKind parameterKind) - { - if (string.IsNullOrEmpty(name)) - { - throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(name)); - } - - if (name.IndexOfAny(RoutePatternParser.InvalidParameterNameChars) >= 0) - { - throw new ArgumentException(Resources.FormatTemplateRoute_InvalidParameterName(name)); - } - - if (defaultValue != null && parameterKind == RoutePatternParameterKind.Optional) - { - throw new ArgumentNullException(Resources.TemplateRoute_OptionalCannotHaveDefaultValue, nameof(parameterKind)); - } - - return CreateParameterFromText(rawText, name, defaultValue, parameterKind, Array.Empty()); - } - - public static RoutePatternParameter CreateParameter( - string name, - object defaultValue, - RoutePatternParameterKind parameterKind, - params ConstraintReference[] constraints) - { - - if (string.IsNullOrEmpty(name)) - { - throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(name)); - } - - if (name.IndexOfAny(RoutePatternParser.InvalidParameterNameChars) >= 0) - { - throw new ArgumentException(Resources.FormatTemplateRoute_InvalidParameterName(name)); - } - - if (defaultValue != null && parameterKind == RoutePatternParameterKind.Optional) - { - throw new ArgumentNullException(Resources.TemplateRoute_OptionalCannotHaveDefaultValue, nameof(parameterKind)); - } - - return new RoutePatternParameter(null, name, defaultValue, parameterKind, constraints); - } - - public static RoutePatternParameter CreateParameterFromText( - string rawText, - string name, - object defaultValue, - RoutePatternParameterKind parameterKind, - params ConstraintReference[] constraints) - { - if (string.IsNullOrEmpty(name)) - { - throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(name)); - } - - if (name.IndexOfAny(RoutePatternParser.InvalidParameterNameChars) >= 0) - { - throw new ArgumentException(Resources.FormatTemplateRoute_InvalidParameterName(name)); - } - - if (defaultValue != null && parameterKind == RoutePatternParameterKind.Optional) - { - throw new ArgumentNullException(Resources.TemplateRoute_OptionalCannotHaveDefaultValue, nameof(parameterKind)); - } - - return new RoutePatternParameter(rawText, name, defaultValue, parameterKind, constraints); - } - - public abstract RoutePatternPartKind PartKind { get; } - - public abstract string RawText { get; } - - public bool IsLiteral => PartKind == RoutePatternPartKind.Literal; - - public bool IsParameter => PartKind == RoutePatternPartKind.Parameter; - - public bool IsSeparator => PartKind == RoutePatternPartKind.Separator; - - internal abstract string DebuggerToString(); - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternPartKind.cs b/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternPartKind.cs deleted file mode 100644 index 1deb20cc94..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternPartKind.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -namespace Microsoft.AspNetCore.Dispatcher.Patterns -{ - public enum RoutePatternPartKind - { - Literal, - Parameter, - Separator, - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternPathSegment.cs b/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternPathSegment.cs deleted file mode 100644 index 8a691472f7..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternPathSegment.cs +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; - -namespace Microsoft.AspNetCore.Dispatcher.Patterns -{ - [DebuggerDisplay("{DebuggerToString()}")] - public sealed class RoutePatternPathSegment - { - internal RoutePatternPathSegment(string rawText, RoutePatternPart[] parts) - { - RawText = rawText; - Parts = parts; - } - - public bool IsSimple => Parts.Count == 1; - - public IReadOnlyList Parts { get; } - - public string RawText { get; set; } - - internal string DebuggerToString() - { - return RawText ?? DebuggerToString(Parts); - } - - internal static string DebuggerToString(IReadOnlyList parts) - { - return string.Join(string.Empty, parts.Select(p => p.DebuggerToString())); - } - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternSeparator.cs b/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternSeparator.cs deleted file mode 100644 index 240baa3824..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternSeparator.cs +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Diagnostics; - -namespace Microsoft.AspNetCore.Dispatcher.Patterns -{ - [DebuggerDisplay("{DebuggerToString()}")] - public sealed class RoutePatternSeparator : RoutePatternPart - { - internal RoutePatternSeparator(string rawText, string content) - { - Debug.Assert(!string.IsNullOrEmpty(content)); - - RawText = rawText; - Content = content; - - PartKind = RoutePatternPartKind.Separator; - } - - public string Content { get; } - - public override RoutePatternPartKind PartKind { get; } - - public override string RawText { get; } - - internal override string DebuggerToString() - { - return RawText ?? Content; - } - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher/Properties/AssemblyInfo.cs b/src/Microsoft.AspNetCore.Dispatcher/Properties/AssemblyInfo.cs deleted file mode 100644 index ec6acdf95f..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Runtime.CompilerServices; - -[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Dispatcher.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/src/Microsoft.AspNetCore.Dispatcher/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Dispatcher/Properties/Resources.Designer.cs deleted file mode 100644 index 235ce5baf6..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/Properties/Resources.Designer.cs +++ /dev/null @@ -1,338 +0,0 @@ -// -namespace Microsoft.AspNetCore.Dispatcher -{ - using System.Globalization; - using System.Reflection; - using System.Resources; - - internal static class Resources - { - private static readonly ResourceManager _resourceManager - = new ResourceManager("Microsoft.AspNetCore.Dispatcher.Resources", typeof(Resources).GetTypeInfo().Assembly); - - /// - /// Multiple endpoints matched. The following endpoints matched the request:{0}{0}{1} - /// - internal static string AmbiguousEndpoints - { - get => GetString("AmbiguousEndpoints"); - } - - /// - /// Multiple endpoints matched. The following endpoints matched the request:{0}{0}{1} - /// - internal static string FormatAmbiguousEndpoints(object p0, object p1) - => string.Format(CultureInfo.CurrentCulture, GetString("AmbiguousEndpoints"), p0, p1); - - /// - /// Value cannot be null or empty. - /// - internal static string Argument_NullOrEmpty - { - get => GetString("Argument_NullOrEmpty"); - } - - /// - /// Value cannot be null or empty. - /// - internal static string FormatArgument_NullOrEmpty() - => GetString("Argument_NullOrEmpty"); - - /// - /// 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 DefaultConstraintResolver_AmbiguousCtors - { - get => GetString("DefaultConstraintResolver_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 FormatDefaultConstraintResolver_AmbiguousCtors(object p0, object p1) - => string.Format(CultureInfo.CurrentCulture, GetString("DefaultConstraintResolver_AmbiguousCtors"), p0, p1); - - /// - /// Could not find a constructor for constraint type '{0}' with the following number of parameters: {1}. - /// - internal static string DefaultConstraintResolver_CouldNotFindCtor - { - get => GetString("DefaultConstraintResolver_CouldNotFindCtor"); - } - - /// - /// Could not find a constructor for constraint type '{0}' with the following number of parameters: {1}. - /// - internal static string FormatDefaultConstraintResolver_CouldNotFindCtor(object p0, object p1) - => string.Format(CultureInfo.CurrentCulture, GetString("DefaultConstraintResolver_CouldNotFindCtor"), p0, p1); - - /// - /// The constraint type '{0}' which is mapped to constraint key '{1}' must implement the '{2}' interface. - /// - internal static string DefaultConstraintResolver_TypeNotConstraint - { - get => GetString("DefaultConstraintResolver_TypeNotConstraint"); - } - - /// - /// The constraint type '{0}' which is mapped to constraint key '{1}' must implement the '{2}' interface. - /// - internal static string FormatDefaultConstraintResolver_TypeNotConstraint(object p0, object p1, object p2) - => string.Format(CultureInfo.CurrentCulture, GetString("DefaultConstraintResolver_TypeNotConstraint"), p0, p1, p2); - - /// - /// The constraint entry '{0}' - '{1}' on the route '{2}' could not be resolved by the constraint resolver of type '{3}'. - /// - internal static string DispatcherValueConstraintBuilder_CouldNotResolveConstraint - { - get => GetString("DispatcherValueConstraintBuilder_CouldNotResolveConstraint"); - } - - /// - /// The constraint entry '{0}' - '{1}' on the route '{2}' could not be resolved by the constraint resolver of type '{3}'. - /// - internal static string FormatDispatcherValueConstraintBuilder_CouldNotResolveConstraint(object p0, object p1, object p2, object p3) - => string.Format(CultureInfo.CurrentCulture, GetString("DispatcherValueConstraintBuilder_CouldNotResolveConstraint"), p0, p1, p2, p3); - - /// - /// 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 DispatcherValueConstraintBuilder_ValidationMustBeStringOrCustomConstraint - { - get => GetString("DispatcherValueConstraintBuilder_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 FormatDispatcherValueConstraintBuilder_ValidationMustBeStringOrCustomConstraint(object p0, object p1, object p2, object p3) - => string.Format(CultureInfo.CurrentCulture, GetString("DispatcherValueConstraintBuilder_ValidationMustBeStringOrCustomConstraint"), p0, p1, p2, p3); - - /// - /// The collection cannot be empty. - /// - internal static string RoutePatternBuilder_CollectionCannotBeEmpty - { - get => GetString("RoutePatternBuilder_CollectionCannotBeEmpty"); - } - - /// - /// The collection cannot be empty. - /// - internal static string FormatRoutePatternBuilder_CollectionCannotBeEmpty() - => GetString("RoutePatternBuilder_CollectionCannotBeEmpty"); - - /// - /// 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"); - - /// - /// 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"); - - /// - /// 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. - /// - internal static string TemplateRoute_InvalidRouteTemplate - { - get => GetString("TemplateRoute_InvalidRouteTemplate"); - } - - /// - /// The route template cannot start with a '~' character. - /// - 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"); - - /// - /// 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"); - - /// - /// 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); - - /// - /// 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); - - /// - /// 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"); - - 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/Microsoft.AspNetCore.Dispatcher/Resources.resx b/src/Microsoft.AspNetCore.Dispatcher/Resources.resx deleted file mode 100644 index eec436af5f..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/Resources.resx +++ /dev/null @@ -1,187 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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 - - - Multiple endpoints matched. The following endpoints matched the request:{0}{0}{1} - 0 is the newline - 1 is a newline separate list of action display names - - - Value cannot be null or empty. - - - 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. - - - The constraint entry '{0}' - '{1}' on the route '{2}' could not be resolved by the constraint resolver of type '{3}'. - - - The constraint entry '{0}' - '{1}' on the route '{2}' must have a string value or be of a type which implements '{3}'. - - - The collection cannot be empty. - - - A path segment that contains more than one section, such as a literal section or a parameter, cannot contain a catch-all parameter. - - - 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. - - - 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. - - - An optional parameter cannot have default value. - - - 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}'. - - - The route parameter name '{0}' appears more than one time in the route template. - - - In a route parameter, '{' and '}' must be escaped with '{{' and '}}'. - - \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Dispatcher/RoutePatternAddress.cs b/src/Microsoft.AspNetCore.Dispatcher/RoutePatternAddress.cs deleted file mode 100644 index f53e7a81e6..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/RoutePatternAddress.cs +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; - -namespace Microsoft.AspNetCore.Dispatcher -{ - public class RoutePatternAddress : Address, IRoutePatternAddress - { - public RoutePatternAddress(string pattern, object values, params object[] metadata) - : this(pattern, values, null, metadata) - { - } - - public RoutePatternAddress(string pattern, object values, string displayName, params object[] metadata) - { - if (pattern == null) - { - throw new ArgumentNullException(nameof(pattern)); - } - - if (metadata == null) - { - throw new ArgumentNullException(nameof(metadata)); - } - - Pattern = pattern; - Defaults = new DispatcherValueCollection(values); - DisplayName = displayName; - Metadata = new MetadataCollection(metadata); - } - - public override string DisplayName { get; } - - public override MetadataCollection Metadata { get; } - - public string Pattern { get; } - - public DispatcherValueCollection Defaults { get; } - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher/RoutePatternAddressSelector.cs b/src/Microsoft.AspNetCore.Dispatcher/RoutePatternAddressSelector.cs deleted file mode 100644 index 94e2e407f1..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/RoutePatternAddressSelector.cs +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; - -namespace Microsoft.AspNetCore.Dispatcher -{ - // This isn't a proposed design, just a placeholder to demonstrate that things are wired up correctly. - public class RoutePatternAddressSelector - { - private readonly AddressTable _addressTable; - - public RoutePatternAddressSelector(AddressTable addressTable) - { - if (addressTable == null) - { - throw new ArgumentNullException(nameof(addressTable)); - } - - _addressTable = addressTable; - } - - public Address SelectAddress(object values) - { - return SelectAddress(new DispatcherValueCollection(values)); - } - - public Address SelectAddress(DispatcherValueCollection values) - { - if (values == null) - { - throw new ArgumentNullException(nameof(values)); - } - - // Capture the current state so we don't see partial updates. - var groups = _addressTable.AddressGroups; - for (var i = 0; i < groups.Count; i++) - { - var matches = new List
(); - var group = groups[i]; - - for (var j = 0; j < group.Count; j++) - { - var address = group[j] as IRoutePatternAddress; - if (address == null) - { - continue; - } - - if (IsMatch(address, values)) - { - matches.Add(group[j]); - } - } - - switch (matches.Count) - { - case 0: - // No match, keep going. - break; - - case 1: - return matches[0]; - - default: - throw new InvalidOperationException("Ambiguous bro!"); - - } - } - - return null; - } - - private bool IsMatch(IRoutePatternAddress address, DispatcherValueCollection values) - { - foreach (var kvp in address.Defaults) - { - values.TryGetValue(kvp.Key, out var value); - - if (!string.Equals(Convert.ToString(kvp.Value) ?? string.Empty, Convert.ToString(value) ?? string.Empty, StringComparison.OrdinalIgnoreCase)) - { - return false; - } - } - - return true; - } - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher/RoutePatternBinder.cs b/src/Microsoft.AspNetCore.Dispatcher/RoutePatternBinder.cs deleted file mode 100644 index be6f51f86a..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/RoutePatternBinder.cs +++ /dev/null @@ -1,439 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Collections; -using System.Diagnostics; -using System.Globalization; -using System.Text.Encodings.Web; -using Microsoft.AspNetCore.Dispatcher.Patterns; -using Microsoft.Extensions.ObjectPool; - -namespace Microsoft.AspNetCore.Dispatcher -{ - public class RoutePatternBinder - { - private readonly UrlEncoder _urlEncoder; - private readonly ObjectPool _pool; - - private readonly DispatcherValueCollection _defaults; - private readonly DispatcherValueCollection _filters; - private readonly RoutePattern _pattern; - - internal RoutePatternBinder( - UrlEncoder urlEncoder, - ObjectPool pool, - RoutePattern pattern, - DispatcherValueCollection defaults) - { - if (urlEncoder == null) - { - throw new ArgumentNullException(nameof(urlEncoder)); - } - - if (pool == null) - { - throw new ArgumentNullException(nameof(pool)); - } - - if (pattern == null) - { - throw new ArgumentNullException(nameof(pattern)); - } - - _urlEncoder = urlEncoder; - _pool = pool; - _pattern = pattern; - _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 DispatcherValueCollection(_defaults); - foreach (var parameter in _pattern.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 (DispatcherValueCollection acceptedValues, DispatcherValueCollection combinedValues) GetValues(DispatcherValueCollection ambientValues, DispatcherValueCollection 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 < _pattern.Parameters.Count; i++) - { - var parameter = _pattern.Parameters[i]; - - // If it's a parameter subsegment, examine the current value to see if it matches the new value - var parameterName = parameter.Name; - - var hasNewParameterValue = values.TryGetValue(parameterName, out var 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 < _pattern.Parameters.Count; i++) - { - var parameter = _pattern.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 < _pattern.Parameters.Count; i++) - { - var parameter = _pattern.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, 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 = _pattern.GetParameter(filter.Key); - if (parameter != null) - { - continue; - } - - if (values.TryGetValue(filter.Key, out var 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, 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 DispatcherValueCollection(context.AcceptedValues); - if (ambientValues != null) - { - foreach (var kvp in ambientValues) - { - if (IsRoutePartNonEmpty(kvp.Value)) - { - var parameter = _pattern.GetParameter(kvp.Key); - if (parameter == null && !context.AcceptedValues.ContainsKey(kvp.Key)) - { - combinedValues.Add(kvp.Key, kvp.Value); - } - } - } - } - - return (context.AcceptedValues, combinedValues); - } - - // Step 2: If the route is a match generate the appropriate URI - public string BindValues(DispatcherValueCollection acceptedValues) - { - var context = _pool.Get(); - var result = BindValues(context, acceptedValues); - _pool.Return(context); - return result; - } - - private string BindValues(UriBuildingContext context, DispatcherValueCollection acceptedValues) - { - for (var i = 0; i < _pattern.PathSegments.Count; i++) - { - Debug.Assert(context.BufferState == SegmentState.Beginning); - Debug.Assert(context.UriState == SegmentState.Beginning); - - var segment = _pattern.PathSegments[i]; - - for (var j = 0; j < segment.Parts.Count; j++) - { - var part = segment.Parts[j]; - - if (part.IsLiteral) - { - if (!context.Accept(_urlEncoder, ((RoutePatternLiteral)part).Content)) - { - return null; - } - } - else if (part.IsSeparator) - { - if (!context.Accept(_urlEncoder, ((RoutePatternSeparator)part).Content)) - { - return null; - } - } - else if (part.IsParameter && part is RoutePatternParameter parameter) - { - // If it's a parameter, get its value - object value; - var hasValue = acceptedValues.TryGetValue(parameter.Name, out value); - if (hasValue) - { - acceptedValues.Remove(parameter.Name); - } - - var isSameAsDefault = false; - object defaultValue; - if (_defaults != null && _defaults.TryGetValue(parameter.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(_urlEncoder, 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(_urlEncoder, converted)) - { - if (j != 0 && parameter.IsOptional && segment.Parts[j - 1].IsSeparator) - { - context.Remove(((RoutePatternSeparator)segment.Parts[j - 1]).Content); - } - 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; - } - - /// - /// 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 DispatcherValueCollection _defaults; - private readonly DispatcherValueCollection _acceptedValues; - - public TemplateBindingContext(DispatcherValueCollection defaults) - { - _defaults = defaults; - - _acceptedValues = new DispatcherValueCollection(); - } - - public DispatcherValueCollection 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/Microsoft.AspNetCore.Dispatcher/RoutePatternBinderFactory.cs b/src/Microsoft.AspNetCore.Dispatcher/RoutePatternBinderFactory.cs deleted file mode 100644 index 51829e6745..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/RoutePatternBinderFactory.cs +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Text.Encodings.Web; -using Microsoft.AspNetCore.Dispatcher.Patterns; -using Microsoft.Extensions.ObjectPool; - -namespace Microsoft.AspNetCore.Dispatcher -{ - public class RoutePatternBinderFactory - { - private readonly UrlEncoder _encoder; - private readonly ObjectPool _pool; - - public RoutePatternBinderFactory(UrlEncoder encoder, ObjectPoolProvider objectPoolProvider) - { - if (encoder == null) - { - throw new ArgumentNullException(nameof(encoder)); - } - - if (objectPoolProvider == null) - { - throw new ArgumentNullException(nameof(objectPoolProvider)); - } - - _encoder = encoder; - _pool = objectPoolProvider.Create(new UriBuilderContextPooledObjectPolicy()); - } - - public RoutePatternBinder Create(string pattern) - { - if (pattern == null) - { - throw new ArgumentNullException(nameof(pattern)); - } - - return Create(RoutePattern.Parse(pattern), new DispatcherValueCollection()); - } - - public RoutePatternBinder Create(string pattern, DispatcherValueCollection defaults) - { - if (pattern == null) - { - throw new ArgumentNullException(nameof(pattern)); - } - - if (defaults == null) - { - throw new ArgumentNullException(nameof(defaults)); - } - - return Create(RoutePattern.Parse(pattern), defaults); - } - - public RoutePatternBinder Create(RoutePattern pattern) - { - if (pattern == null) - { - throw new ArgumentNullException(nameof(pattern)); - } - - return Create(pattern, new DispatcherValueCollection()); - } - - public RoutePatternBinder Create(RoutePattern pattern, DispatcherValueCollection defaults) - { - if (pattern == null) - { - throw new ArgumentNullException(nameof(pattern)); - } - - if (defaults == null) - { - throw new ArgumentNullException(nameof(defaults)); - } - - return new RoutePatternBinder(_encoder, _pool, pattern, defaults); - } - - private class UriBuilderContextPooledObjectPolicy : IPooledObjectPolicy - { - public UriBuildingContext Create() - { - return new UriBuildingContext(); - } - - public bool Return(UriBuildingContext obj) - { - obj.Clear(); - return true; - } - } - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher/RoutePatternEndpoint.cs b/src/Microsoft.AspNetCore.Dispatcher/RoutePatternEndpoint.cs deleted file mode 100644 index 1047159b6a..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/RoutePatternEndpoint.cs +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using Microsoft.AspNetCore.Http; - -namespace Microsoft.AspNetCore.Dispatcher -{ - public class RoutePatternEndpoint : Endpoint, IRoutePatternEndpoint - { - public RoutePatternEndpoint(string pattern, RequestDelegate requestDelegate, params object[] metadata) - : this(pattern, (object)null, (string)null, requestDelegate, null, metadata) - { - } - - public RoutePatternEndpoint(string pattern, Func delegateFactory, params object[] metadata) - : this(pattern, (object)null, (string)null, delegateFactory, null, metadata) - { - } - - public RoutePatternEndpoint(string pattern, object values, RequestDelegate requestDelegate, params object[] metadata) - : this(pattern, values, null, requestDelegate, null, metadata) - { - } - - public RoutePatternEndpoint(string pattern, object values, Func delegateFactory, params object[] metadata) - : this(pattern, values, null, delegateFactory, null, metadata) - { - } - - public RoutePatternEndpoint(string pattern, object values, RequestDelegate requestDelegate, string displayName, params object[] metadata) - : this(pattern, values, null, requestDelegate, displayName, metadata) - { - } - - public RoutePatternEndpoint(string pattern, object values, Func delegateFactory, string displayName, params object[] metadata) - : this(pattern, values, null, delegateFactory, displayName, metadata) - { - } - - public RoutePatternEndpoint(string pattern, object values, string httpMethod, RequestDelegate requestDelegate, params object[] metadata) - : this(pattern, values, httpMethod, requestDelegate, null, metadata) - { - } - - public RoutePatternEndpoint(string pattern, object values, string httpMethod, Func delegateFactory, params object[] metadata) - : this(pattern, values, httpMethod, delegateFactory, null, metadata) - { - } - - public RoutePatternEndpoint( - string pattern, - object values, - string httpMethod, - RequestDelegate requestDelegate, - string displayName, - params object[] metadata) - { - if (pattern == null) - { - throw new ArgumentNullException(nameof(pattern)); - } - - if (requestDelegate == null) - { - throw new ArgumentNullException(nameof(requestDelegate)); - } - - if (metadata == null) - { - throw new ArgumentNullException(nameof(metadata)); - } - - Pattern = pattern; - Values = new DispatcherValueCollection(values); - HttpMethod = httpMethod; - HandlerFactory = (next) => requestDelegate; - DisplayName = displayName; - Metadata = new MetadataCollection(metadata); - } - - public RoutePatternEndpoint( - string pattern, - object values, - string httpMethod, - Func delegateFactory, - string displayName, - params object[] metadata) - { - if (pattern == null) - { - throw new ArgumentNullException(nameof(pattern)); - } - - if (delegateFactory == null) - { - throw new ArgumentNullException(nameof(delegateFactory)); - } - - if (metadata == null) - { - throw new ArgumentNullException(nameof(metadata)); - } - - Pattern = pattern; - Values = new DispatcherValueCollection(values); - HttpMethod = httpMethod; - HandlerFactory = delegateFactory; - DisplayName = displayName; - Metadata = new MetadataCollection(metadata); - } - - public override string DisplayName { get; } - - public string HttpMethod { get; } - - public override MetadataCollection Metadata { get; } - - public Func HandlerFactory { get; } - - public string Pattern { get; } - - public DispatcherValueCollection Values { get; } - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher/RoutePatternEndpointHandlerFactory.cs b/src/Microsoft.AspNetCore.Dispatcher/RoutePatternEndpointHandlerFactory.cs deleted file mode 100644 index e77fc82be2..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/RoutePatternEndpointHandlerFactory.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using Microsoft.AspNetCore.Http; - -namespace Microsoft.AspNetCore.Dispatcher -{ - public sealed class RoutePatternEndpointHandlerFactory : IHandlerFactory - { - public Func CreateHandler(Endpoint endpoint) - { - if (endpoint == null) - { - throw new ArgumentNullException(nameof(endpoint)); - } - - if (endpoint is RoutePatternEndpoint routePatternEndpoint) - { - return routePatternEndpoint.HandlerFactory; - } - - return null; - } - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher/RoutePatternTemplate.cs b/src/Microsoft.AspNetCore.Dispatcher/RoutePatternTemplate.cs deleted file mode 100644 index 31327d6c72..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/RoutePatternTemplate.cs +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using Microsoft.AspNetCore.Http; - -namespace Microsoft.AspNetCore.Dispatcher -{ - public class RoutePatternTemplate : Template - { - private readonly RoutePatternBinder _binder; - - public RoutePatternTemplate(RoutePatternBinder binder) - { - if (binder == null) - { - throw new ArgumentNullException(nameof(binder)); - } - - _binder = binder; - } - - public override string GetUrl(DispatcherValueCollection values) - { - if (values == null) - { - throw new ArgumentNullException(nameof(values)); - } - - return GetUrl(null, values); - } - - public override string GetUrl(HttpContext httpContext, DispatcherValueCollection values) - { - if (values == null) - { - throw new ArgumentNullException(nameof(values)); - } - - var ambientValues = GetAmbientValues(httpContext); - var result = _binder.GetValues(ambientValues, values); - if (result.acceptedValues == null) - { - return null; - } - - return _binder.BindValues(result.acceptedValues); - } - - private DispatcherValueCollection GetAmbientValues(HttpContext httpContext) - { - if (httpContext == null) - { - return new DispatcherValueCollection(); - } - - var feature = httpContext.Features.Get(); - if (feature == null) - { - return new DispatcherValueCollection(); - } - - return feature.Values; - } - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher/RoutePatternTemplateFactory.cs b/src/Microsoft.AspNetCore.Dispatcher/RoutePatternTemplateFactory.cs deleted file mode 100644 index 24d9461c27..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/RoutePatternTemplateFactory.cs +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; - -namespace Microsoft.AspNetCore.Dispatcher -{ - internal class RoutePatternTemplateFactory : TemplateFactory - { - private readonly RoutePatternAddressSelector _selector; - private readonly RoutePatternBinderFactory _binderFactory; - - public RoutePatternTemplateFactory(RoutePatternAddressSelector selector, RoutePatternBinderFactory binderFactory) - { - if (selector == null) - { - throw new ArgumentNullException(nameof(selector)); - } - - if (binderFactory == null) - { - throw new ArgumentNullException(nameof(binderFactory)); - } - - _selector = selector; - _binderFactory = binderFactory; - } - - public override Template GetTemplate(DispatcherValueCollection key) - { - if (key == null) - { - throw new ArgumentNullException(nameof(key)); - } - - var address = _selector.SelectAddress(key); - if (address == null) - { - return null; - } - - if (address is IRoutePatternAddress templateAddress) - { - var binder = _binderFactory.Create(templateAddress.Pattern, templateAddress.Defaults); - return new RoutePatternTemplate(binder); - } - - return null; - } - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher/RoutePrecedence.cs b/src/Microsoft.AspNetCore.Dispatcher/RoutePrecedence.cs deleted file mode 100644 index 231b2f0945..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/RoutePrecedence.cs +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Diagnostics; -using System.Linq; -using Microsoft.AspNetCore.Dispatcher.Patterns; - -namespace Microsoft.AspNetCore.Dispatcher -{ - /// - /// Computes precedence for a route pattern. - /// - 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(RoutePattern routePattern) - { - // 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 < routePattern.PathSegments.Count; i++) - { - var segment = routePattern.PathSegments[i]; - - var digit = ComputeInboundPrecedenceDigit(segment); - Debug.Assert(digit >= 0 && digit < 10); - - precedence += decimal.Divide(digit, (decimal)Math.Pow(10, i)); - } - - return precedence; - } - - // 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(RoutePatternPathSegment 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 parameter = (RoutePatternParameter)part; - var digit = parameter.IsCatchAll ? 5 : 3; - - // If there is a dispatcher value constraint for the parameter, reduce order by 1 - // Constrained parameters end up with order 2, Constrained catch alls end up with order 4 - if (parameter.Constraints != null && parameter.Constraints.Any()) - { - digit--; - } - - return digit; - } - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Dispatcher/TemplateFactoryOfT.cs b/src/Microsoft.AspNetCore.Dispatcher/TemplateFactoryOfT.cs deleted file mode 100644 index 10ddc0cf1a..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/TemplateFactoryOfT.cs +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -namespace Microsoft.AspNetCore.Dispatcher -{ - public abstract class TemplateFactory : ITemplateFactoryComponent - { - public abstract Template GetTemplate(TKey key); - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher/Tree/TreeMatcher.cs b/src/Microsoft.AspNetCore.Dispatcher/Tree/TreeMatcher.cs deleted file mode 100644 index 8a534315ac..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/Tree/TreeMatcher.cs +++ /dev/null @@ -1,293 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Dispatcher.Internal; -using Microsoft.AspNetCore.Dispatcher.Patterns; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Internal; -using Primitives = Microsoft.Extensions.Primitives; - -namespace Microsoft.AspNetCore.Dispatcher -{ - public class TreeMatcher : MatcherBase - { - private bool _onChange; - private bool _dataInitialized; - private object _lock; - private Cache _cache; - private IConstraintFactory _constraintFactory; - - private readonly Func _initializer; - - public TreeMatcher() - { - _lock = new object(); - _initializer = CreateCache; - } - - public override async Task MatchAsync(MatcherContext context) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - EnsureTreeMatcherServicesInitialized(context); - - var cache = LazyInitializer.EnsureInitialized(ref _cache, ref _dataInitialized, ref _lock, _initializer); - - var values = new DispatcherValueCollection(); - context.Values = values; - - for (var i = 0; i < cache.Trees.Length; i++) - { - var tree = cache.Trees[i]; - var tokenizer = new PathTokenizer(context.HttpContext.Request.Path); - - var treenumerator = new TreeEnumerator(tree.Root, tokenizer); - - while (treenumerator.MoveNext()) - { - var node = treenumerator.Current; - foreach (var item in node.Matches) - { - var entry = item.Entry; - var matcher = item.RoutePatternMatcher; - - values.Clear(); - if (!matcher.TryMatch(context.HttpContext.Request.Path, values)) - { - continue; - } - - Logger.MatchedRoute(entry.RoutePattern.RawText); - - if (!MatchConstraints(context.HttpContext, values, entry.Constraints)) - { - continue; - } - - await SelectEndpointAsync(context, (entry.Endpoints)); - if (context.ShortCircuit != null) - { - Logger.RequestShortCircuited(context); - return; - } - - if (context.Endpoint != null) - { - if (context.Endpoint is IRoutePatternEndpoint templateEndpoint) - { - foreach (var kvp in templateEndpoint.Values) - { - if (!context.Values.ContainsKey(kvp.Key)) - { - context.Values[kvp.Key] = kvp.Value; - } - } - } - - return; - } - } - } - } - } - - private void EnsureTreeMatcherServicesInitialized(MatcherContext context) - { - EnsureServicesInitialized(context); - if (Volatile.Read(ref _onChange)) - { - return; - } - - lock (_lock) - { - if (!Volatile.Read(ref _onChange)) - { - _onChange = true; - Primitives.ChangeToken.OnChange(() => ChangeToken, () => Volatile.Write(ref _dataInitialized, false)); - } - } - } - - private bool MatchConstraints(HttpContext httpContext, DispatcherValueCollection values, IDictionary constraints) - { - if (constraints != null) - { - foreach (var kvp in constraints) - { - var constraint = kvp.Value; - var constraintContext = new DispatcherValueConstraintContext(httpContext, values, ConstraintPurpose.IncomingRequest) - { - Key = kvp.Key - }; - - if (!constraint.Match(constraintContext)) - { - values.TryGetValue(kvp.Key, out var value); - - Logger.RouteValueDoesNotMatchConstraint(value, kvp.Key, kvp.Value); - return false; - } - } - } - - return true; - } - - internal Cache CreateCache() - { - var endpoints = GetEndpoints(); - - var groups = new Dictionary>(); - - for (var i = 0; i < endpoints.Count; i++) - { - var endpoint = endpoints[i]; - - if (!(endpoint is IRoutePatternEndpoint templateEndpoint)) - { - continue; - } - - var order = endpoint.Metadata?.GetMetadata()?.Order ?? 0; - if (!groups.TryGetValue(new Key(order, templateEndpoint.Pattern), out var group)) - { - group = new List(); - groups.Add(new Key(order, templateEndpoint.Pattern), group); - } - - group.Add(endpoint); - } - - var entries = new List(); - foreach (var group in groups) - { - var routePattern = RoutePattern.Parse(group.Key.RoutePattern); - var entryExists = entries.Any(item => item.RoutePattern.RawText == routePattern.RawText); - if (!entryExists) - { - entries.Add(MapInbound(routePattern, group.Value.ToArray(), group.Key.Order)); - } - } - - var trees = new List(); - for (var i = 0; i < entries.Count; i++) - { - var entry = entries[i]; - - while (trees.Count <= entry.Order) - { - trees.Add(new UrlMatchingTree(entry.Order)); - } - - var tree = trees[entry.Order]; - - UrlMatchingTree.AddEntryToTree(tree, entry); - } - - return new Cache(trees.ToArray()); - } - - private InboundRouteEntry MapInbound( - RoutePattern routePattern, - Endpoint[] endpoints, - int order) - { - if (routePattern == null) - { - throw new ArgumentNullException(nameof(routePattern)); - } - - var entry = new InboundRouteEntry() - { - Precedence = RoutePrecedence.ComputeInbound(routePattern), - RoutePattern = routePattern, - Order = order, - Endpoints = endpoints, - }; - - var constraintBuilder = new DispatcherValueConstraintBuilder(_constraintFactory, routePattern.RawText); - foreach (var parameter in routePattern.Parameters) - { - if (parameter.Constraints != null) - { - if (parameter.IsOptional) - { - constraintBuilder.SetOptional(parameter.Name); - } - - foreach (var constraint in parameter.Constraints) - { - constraintBuilder.AddResolvedConstraint(parameter.Name, constraint.RawText); - } - } - } - - entry.Constraints = constraintBuilder.Build(); - - entry.Defaults = new DispatcherValueCollection(); - foreach (var parameter in entry.RoutePattern.Parameters) - { - if (parameter.DefaultValue != null) - { - entry.Defaults.Add(parameter.Name, parameter.DefaultValue); - } - } - return entry; - } - - private struct Key : IEquatable - { - public readonly int Order; - public readonly string RoutePattern; - - public Key(int order, string routePattern) - { - Order = order; - RoutePattern = routePattern; - } - - public bool Equals(Key other) - { - return Order == other.Order && string.Equals(RoutePattern, other.RoutePattern, StringComparison.OrdinalIgnoreCase); - } - - public override bool Equals(object obj) - { - return obj is Key ? Equals((Key)obj) : false; - } - - public override int GetHashCode() - { - var hash = new HashCodeCombiner(); - hash.Add(Order); - hash.Add(RoutePattern, StringComparer.OrdinalIgnoreCase); - return hash; - } - } - - internal class Cache - { - public readonly UrlMatchingTree[] Trees; - - public Cache(UrlMatchingTree[] trees) - { - Trees = trees; - } - } - - protected override void InitializeServices(IServiceProvider services) - { - _constraintFactory = services.GetRequiredService(); - } - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher/Tree/TreeMatcherFactory.cs b/src/Microsoft.AspNetCore.Dispatcher/Tree/TreeMatcherFactory.cs deleted file mode 100644 index badcf48078..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/Tree/TreeMatcherFactory.cs +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; - -namespace Microsoft.AspNetCore.Dispatcher -{ - public class TreeMatcherFactory : IDefaultMatcherFactory - { - public IMatcher CreateMatcher(DispatcherDataSource dataSource, IEnumerable endpointSelectors) - { - if (dataSource == null) - { - throw new ArgumentNullException(nameof(dataSource)); - } - - var matcher = new TreeMatcher() - { - DataSource = dataSource, - }; - - foreach (var endpointSelector in endpointSelectors) - { - matcher.Selectors.Add(endpointSelector); - } - - return matcher; - } - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher/baseline.netcore.json b/src/Microsoft.AspNetCore.Dispatcher/baseline.netcore.json deleted file mode 100644 index 7a73a41bfd..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/baseline.netcore.json +++ /dev/null @@ -1,2 +0,0 @@ -{ -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Routing.Abstractions/Microsoft.AspNetCore.Routing.Abstractions.csproj b/src/Microsoft.AspNetCore.Routing.Abstractions/Microsoft.AspNetCore.Routing.Abstractions.csproj index 9d3b872866..633a49d503 100644 --- a/src/Microsoft.AspNetCore.Routing.Abstractions/Microsoft.AspNetCore.Routing.Abstractions.csproj +++ b/src/Microsoft.AspNetCore.Routing.Abstractions/Microsoft.AspNetCore.Routing.Abstractions.csproj @@ -13,9 +13,6 @@ Microsoft.AspNetCore.Routing.RouteData - - - - + diff --git a/src/Microsoft.AspNetCore.Routing.Abstractions/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Routing.Abstractions/Properties/Resources.Designer.cs index 2ac5d0972c..8fdf6b8715 100644 --- a/src/Microsoft.AspNetCore.Routing.Abstractions/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Routing.Abstractions/Properties/Resources.Designer.cs @@ -10,6 +10,34 @@ namespace Microsoft.AspNetCore.Routing.Abstractions 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); diff --git a/src/Microsoft.AspNetCore.Routing.Abstractions/Resources.resx b/src/Microsoft.AspNetCore.Routing.Abstractions/Resources.resx index 1af7de150c..40e651af14 100644 --- a/src/Microsoft.AspNetCore.Routing.Abstractions/Resources.resx +++ b/src/Microsoft.AspNetCore.Routing.Abstractions/Resources.resx @@ -117,4 +117,10 @@ 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/Microsoft.AspNetCore.Routing.Abstractions/RouteValueDictionary.cs b/src/Microsoft.AspNetCore.Routing.Abstractions/RouteValueDictionary.cs index cc05d64b6c..d0bc8e9ecf 100644 --- a/src/Microsoft.AspNetCore.Routing.Abstractions/RouteValueDictionary.cs +++ b/src/Microsoft.AspNetCore.Routing.Abstractions/RouteValueDictionary.cs @@ -3,30 +3,402 @@ using System; using System.Collections; +using System.Collections.Concurrent; using System.Collections.Generic; -using Microsoft.AspNetCore.Dispatcher; +using System.Diagnostics; +using Microsoft.AspNetCore.Routing.Abstractions; +using Microsoft.Extensions.Internal; namespace Microsoft.AspNetCore.Routing { - public class RouteValueDictionary : DispatcherValueCollection + /// + /// An type for route values. + /// + public class RouteValueDictionary : IDictionary, IReadOnlyDictionary { + internal Storage _storage; + + /// + /// Creates an empty . + /// public RouteValueDictionary() - : base() { + _storage = EmptyStorage.Instance; } - public RouteValueDictionary(object obj) - : base(obj) + /// + /// 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; } - // Required to avoid a breaking change in the split of RVD/DVC - public new Enumerator GetEnumerator() => new Enumerator(this); - - // Required to avoid a breaking change in the split of RVD/DVC - public new struct Enumerator : IEnumerator> + /// + public object this[string key] { - private DispatcherValueCollection.Enumerator _inner; + 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) { @@ -35,12 +407,15 @@ namespace Microsoft.AspNetCore.Routing throw new ArgumentNullException(); } - _inner = ((DispatcherValueCollection)dictionary).GetEnumerator(); + _storage = dictionary._storage; + + Current = default(KeyValuePair); + _index = -1; } - public KeyValuePair Current => _inner.Current; + public KeyValuePair Current { get; private set; } - object IEnumerator.Current => _inner.Current; + object IEnumerator.Current => Current; public void Dispose() { @@ -48,13 +423,368 @@ namespace Microsoft.AspNetCore.Routing public bool MoveNext() { - return _inner.MoveNext(); + if (++_index < _storage.Count) + { + Current = _storage[_index]; + return true; + } + + Current = default(KeyValuePair); + return false; } public void Reset() { - _inner.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/Microsoft.AspNetCore.Routing/DependencyInjection/RoutingServiceCollectionExtensions.cs b/src/Microsoft.AspNetCore.Routing/DependencyInjection/RoutingServiceCollectionExtensions.cs index 5c757965ad..a0b2b6b891 100644 --- a/src/Microsoft.AspNetCore.Routing/DependencyInjection/RoutingServiceCollectionExtensions.cs +++ b/src/Microsoft.AspNetCore.Routing/DependencyInjection/RoutingServiceCollectionExtensions.cs @@ -3,11 +3,11 @@ using System; using System.Text.Encodings.Web; -using Microsoft.AspNetCore.Dispatcher; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Internal; using Microsoft.AspNetCore.Routing.Tree; using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.ObjectPool; namespace Microsoft.Extensions.DependencyInjection { @@ -28,12 +28,14 @@ namespace Microsoft.Extensions.DependencyInjection throw new ArgumentNullException(nameof(services)); } - // Routing shares lots of infrastructure with the dispatcher. - services.AddDispatcher(); - services.TryAddSingleton(); - services.TryAddTransient(); services.TryAddSingleton(UrlEncoder.Default); + services.TryAddSingleton>(s => + { + var provider = s.GetRequiredService(); + var encoder = s.GetRequiredService(); + return provider.Create(new UriBuilderContextPooledObjectPolicy(encoder)); + }); // The TreeRouteBuilder is a builder for creating routes, it should stay transient because it's // stateful. diff --git a/src/Microsoft.AspNetCore.Routing/Dispatcher/DispatcherValueCollectionExtensions.cs b/src/Microsoft.AspNetCore.Routing/Dispatcher/DispatcherValueCollectionExtensions.cs deleted file mode 100644 index 337b6bcd7c..0000000000 --- a/src/Microsoft.AspNetCore.Routing/Dispatcher/DispatcherValueCollectionExtensions.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using Microsoft.AspNetCore.Routing; - -namespace Microsoft.AspNetCore.Dispatcher -{ - public static class DispatcherValueCollectionExtensions - { - public static RouteValueDictionary AsRouteValueDictionary(this DispatcherValueCollection values) - { - if (values == null) - { - throw new ArgumentNullException(nameof(values)); - } - - return values as RouteValueDictionary ?? new RouteValueDictionary(values); - } - } -} diff --git a/src/Microsoft.AspNetCore.Routing/InlineRouteParameterParser.cs b/src/Microsoft.AspNetCore.Routing/InlineRouteParameterParser.cs index f542cf5717..78131eba97 100644 --- a/src/Microsoft.AspNetCore.Routing/InlineRouteParameterParser.cs +++ b/src/Microsoft.AspNetCore.Routing/InlineRouteParameterParser.cs @@ -1,16 +1,14 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// 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 { - [Obsolete( - "This API is obsolete and will be removed in a future release. It does not report errors correctly. " + - "Use 'TemplateParser.Parse()' and filter for the desired parameter as an alternative.")] public static TemplatePart ParseRouteParameter(string routeParameter) { if (routeParameter == null) @@ -18,9 +16,228 @@ namespace Microsoft.AspNetCore.Routing throw new ArgumentNullException(nameof(routeParameter)); } - // See: #475 - this API has no way to pass the 'raw' text - var inner = AspNetCore.Dispatcher.Patterns.InlineRouteParameterParser.ParseRouteParameter(string.Empty, routeParameter); - return new TemplatePart(inner); + 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/Microsoft.AspNetCore.Dispatcher/BufferValue.cs b/src/Microsoft.AspNetCore.Routing/Internal/BufferValue.cs similarity index 84% rename from src/Microsoft.AspNetCore.Dispatcher/BufferValue.cs rename to src/Microsoft.AspNetCore.Routing/Internal/BufferValue.cs index c4f0031c96..578c396b4d 100644 --- a/src/Microsoft.AspNetCore.Dispatcher/BufferValue.cs +++ b/src/Microsoft.AspNetCore.Routing/Internal/BufferValue.cs @@ -1,9 +1,9 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -namespace Microsoft.AspNetCore.Dispatcher +namespace Microsoft.AspNetCore.Routing.Internal { - internal struct BufferValue + public struct BufferValue { public BufferValue(string value, bool requiresEncoding) { diff --git a/src/Microsoft.AspNetCore.Dispatcher/Internal/PathTokenizer.cs b/src/Microsoft.AspNetCore.Routing/Internal/PathTokenizer.cs similarity index 99% rename from src/Microsoft.AspNetCore.Dispatcher/Internal/PathTokenizer.cs rename to src/Microsoft.AspNetCore.Routing/Internal/PathTokenizer.cs index b6e7aaf645..9418989fdb 100644 --- a/src/Microsoft.AspNetCore.Dispatcher/Internal/PathTokenizer.cs +++ b/src/Microsoft.AspNetCore.Routing/Internal/PathTokenizer.cs @@ -8,7 +8,7 @@ using System.Diagnostics; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; -namespace Microsoft.AspNetCore.Dispatcher.Internal +namespace Microsoft.AspNetCore.Routing.Internal { public struct PathTokenizer : IReadOnlyList { diff --git a/src/Microsoft.AspNetCore.Dispatcher/SegmentState.cs b/src/Microsoft.AspNetCore.Routing/Internal/SegmentState.cs similarity index 89% rename from src/Microsoft.AspNetCore.Dispatcher/SegmentState.cs rename to src/Microsoft.AspNetCore.Routing/Internal/SegmentState.cs index 19843b71bd..35076a0678 100644 --- a/src/Microsoft.AspNetCore.Dispatcher/SegmentState.cs +++ b/src/Microsoft.AspNetCore.Routing/Internal/SegmentState.cs @@ -1,7 +1,7 @@ // 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.Dispatcher +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 @@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.Dispatcher // 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". - internal enum SegmentState + public enum SegmentState { Beginning, Inside, diff --git a/src/Microsoft.AspNetCore.Routing/Internal/UriBuilderContextPooledObjectPolicy.cs b/src/Microsoft.AspNetCore.Routing/Internal/UriBuilderContextPooledObjectPolicy.cs new file mode 100644 index 0000000000..0db44758d9 --- /dev/null +++ b/src/Microsoft.AspNetCore.Routing/Internal/UriBuilderContextPooledObjectPolicy.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 System.Text.Encodings.Web; +using Microsoft.Extensions.ObjectPool; + +namespace Microsoft.AspNetCore.Routing.Internal +{ + public class UriBuilderContextPooledObjectPolicy : IPooledObjectPolicy + { + private readonly UrlEncoder _encoder; + + public UriBuilderContextPooledObjectPolicy(UrlEncoder encoder) + { + if (encoder == null) + { + throw new ArgumentNullException(nameof(encoder)); + } + + _encoder = encoder; + } + + public UriBuildingContext Create() + { + return new UriBuildingContext(_encoder); + } + + public bool Return(UriBuildingContext obj) + { + obj.Clear(); + return true; + } + } +} diff --git a/src/Microsoft.AspNetCore.Dispatcher/UriBuildingContext.cs b/src/Microsoft.AspNetCore.Routing/Internal/UriBuildingContext.cs similarity index 91% rename from src/Microsoft.AspNetCore.Dispatcher/UriBuildingContext.cs rename to src/Microsoft.AspNetCore.Routing/Internal/UriBuildingContext.cs index 55ebc8b304..3b78fe8c78 100644 --- a/src/Microsoft.AspNetCore.Dispatcher/UriBuildingContext.cs +++ b/src/Microsoft.AspNetCore.Routing/Internal/UriBuildingContext.cs @@ -7,10 +7,10 @@ using System.IO; using System.Text; using System.Text.Encodings.Web; -namespace Microsoft.AspNetCore.Dispatcher +namespace Microsoft.AspNetCore.Routing.Internal { [DebuggerDisplay("{DebuggerToString(),nq}")] - internal class UriBuildingContext + public class UriBuildingContext { // Holds the 'accepted' parts of the uri. private readonly StringBuilder _uri; @@ -19,12 +19,14 @@ namespace Microsoft.AspNetCore.Dispatcher // 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() + public UriBuildingContext(UrlEncoder urlEncoder) { + _urlEncoder = urlEncoder; _uri = new StringBuilder(); _buffer = new List(); Writer = new StringWriter(_uri); @@ -40,7 +42,7 @@ namespace Microsoft.AspNetCore.Dispatcher public TextWriter Writer { get; } - public bool Accept(UrlEncoder encoder, string value) + public bool Accept(string value) { if (string.IsNullOrEmpty(value)) { @@ -65,7 +67,7 @@ namespace Microsoft.AspNetCore.Dispatcher { if (_buffer[i].RequiresEncoding) { - encoder.Encode(Writer, _buffer[i].Value); + _urlEncoder.Encode(Writer, _buffer[i].Value); } else { @@ -91,11 +93,11 @@ namespace Microsoft.AspNetCore.Dispatcher if (_uri.Length == 0 && value.Length > 0 && value[0] == '/') { _uri.Append("/"); - encoder.Encode(Writer, value, 1, value.Length - 1); + _urlEncoder.Encode(Writer, value, 1, value.Length - 1); } else { - encoder.Encode(Writer, value); + _urlEncoder.Encode(Writer, value); } return true; @@ -108,7 +110,7 @@ namespace Microsoft.AspNetCore.Dispatcher _lastValueOffset = -1; } - public bool Buffer(UrlEncoder encoder, string value) + public bool Buffer(string value) { if (string.IsNullOrEmpty(value)) { @@ -133,7 +135,7 @@ namespace Microsoft.AspNetCore.Dispatcher { // 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(encoder, value); + var result = Accept(value); // We've already checked the conditions that could result in a rejected part, so this should // always be true. diff --git a/src/Microsoft.AspNetCore.Routing/Logging/TreeRouterLoggerExtensions.cs b/src/Microsoft.AspNetCore.Routing/Logging/TreeRouterLoggerExtensions.cs index c52635ec8c..3f6af8e5dc 100644 --- a/src/Microsoft.AspNetCore.Routing/Logging/TreeRouterLoggerExtensions.cs +++ b/src/Microsoft.AspNetCore.Routing/Logging/TreeRouterLoggerExtensions.cs @@ -2,19 +2,12 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using Microsoft.AspNetCore.Dispatcher; using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Routing.Logging { internal static class TreeRouterLoggerExtensions { - // TreeMatcher - private static readonly Action _requestShortCircuited = LoggerMessage.Define( - LogLevel.Information, - new EventId(3, "RequestShortCircuited"), - "The current request '{RequestPath}' was short circuited."); - private static readonly Action _matchedRoute; static TreeRouterLoggerExtensions() @@ -25,12 +18,6 @@ namespace Microsoft.AspNetCore.Routing.Logging "Request successfully matched the route with name '{RouteName}' and template '{RouteTemplate}'."); } - public static void RequestShortCircuited(this ILogger logger, MatcherContext matcherContext) - { - var requestPath = matcherContext.HttpContext.Request.Path; - _requestShortCircuited(logger, requestPath, null); - } - public static void MatchedRoute( this ILogger logger, string routeName, diff --git a/src/Microsoft.AspNetCore.Routing/Microsoft.AspNetCore.Routing.csproj b/src/Microsoft.AspNetCore.Routing/Microsoft.AspNetCore.Routing.csproj index 1319abc7d9..f1e3d36a55 100644 --- a/src/Microsoft.AspNetCore.Routing/Microsoft.AspNetCore.Routing.csproj +++ b/src/Microsoft.AspNetCore.Routing/Microsoft.AspNetCore.Routing.csproj @@ -1,12 +1,10 @@  - 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 - $(DefineConstants);ROUTING $(NoWarn);CS1591 true aspnetcore;routing @@ -14,21 +12,18 @@ Microsoft.AspNetCore.Routing.RouteCollection - - - - + - + diff --git a/src/Microsoft.AspNetCore.Routing/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Routing/Properties/Resources.Designer.cs index fc8f6159b3..5cb82620c1 100644 --- a/src/Microsoft.AspNetCore.Routing/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Routing/Properties/Resources.Designer.cs @@ -122,6 +122,174 @@ namespace Microsoft.AspNetCore.Routing 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 '/' or '~' character. + /// + internal static string TemplateRoute_InvalidRouteTemplate + { + get => GetString("TemplateRoute_InvalidRouteTemplate"); + } + + /// + /// The route template cannot start with a '/' or '~' character. + /// + 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}'. /// @@ -150,6 +318,48 @@ namespace Microsoft.AspNetCore.Routing 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. /// @@ -178,34 +388,6 @@ namespace Microsoft.AspNetCore.Routing internal static string FormatUnableToFindServices(object p0, object p1, object p2) => string.Format(CultureInfo.CurrentCulture, GetString("UnableToFindServices"), p0, p1, p2); - /// - /// The '{0}' has no '{1}'. '{2}' requires a dispatcher. - /// - internal static string DispatcherFeatureIsRequired - { - get => GetString("DispatcherFeatureIsRequired"); - } - - /// - /// The '{0}' has no '{1}'. '{2}' requires a dispatcher. - /// - internal static string FormatDispatcherFeatureIsRequired(object p0, object p1, object p2) - => string.Format(CultureInfo.CurrentCulture, GetString("DispatcherFeatureIsRequired"), p0, p1, p2); - - /// - /// 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); - /// /// An error occurred while creating the route with name '{0}' and template '{1}'. /// diff --git a/src/Microsoft.AspNetCore.Routing/Resources.resx b/src/Microsoft.AspNetCore.Routing/Resources.resx index 35c7533bfd..96ecf1f445 100644 --- a/src/Microsoft.AspNetCore.Routing/Resources.resx +++ b/src/Microsoft.AspNetCore.Routing/Resources.resx @@ -1,17 +1,17 @@  - @@ -141,24 +141,63 @@ 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 '/' or '~' character. + + + 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. - - The '{0}' has no '{1}'. '{2}' requires a dispatcher. - - - 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. - An error occurred while creating the route with name '{0}' and template '{1}'. diff --git a/src/Microsoft.AspNetCore.Routing/RouteBase.cs b/src/Microsoft.AspNetCore.Routing/RouteBase.cs index 90ef1b05e9..64a7be7693 100644 --- a/src/Microsoft.AspNetCore.Routing/RouteBase.cs +++ b/src/Microsoft.AspNetCore.Routing/RouteBase.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Text.Encodings.Web; using System.Threading.Tasks; -using Microsoft.AspNetCore.Dispatcher; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing.Internal; using Microsoft.AspNetCore.Routing.Logging; @@ -242,8 +241,9 @@ namespace Microsoft.AspNetCore.Routing { if (_binder == null) { - var binderFactory = context.RequestServices.GetRequiredService(); - _binder = new TemplateBinder(binderFactory.Create(ParsedTemplate.ToRoutePattern(), Defaults)); + var urlEncoder = context.RequestServices.GetRequiredService(); + var pool = context.RequestServices.GetRequiredService>(); + _binder = new TemplateBinder(urlEncoder, pool, ParsedTemplate, Defaults); } } diff --git a/src/Microsoft.AspNetCore.Routing/Template/InlineConstraint.cs b/src/Microsoft.AspNetCore.Routing/Template/InlineConstraint.cs index 9274223cad..5c50eefb0e 100644 --- a/src/Microsoft.AspNetCore.Routing/Template/InlineConstraint.cs +++ b/src/Microsoft.AspNetCore.Routing/Template/InlineConstraint.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using Other = Microsoft.AspNetCore.Dispatcher.Patterns.ConstraintReference; namespace Microsoft.AspNetCore.Routing.Template { @@ -25,16 +24,6 @@ namespace Microsoft.AspNetCore.Routing.Template Constraint = constraint; } - public InlineConstraint(Other other) - { - if (other == null) - { - throw new ArgumentNullException(nameof(other)); - } - - Constraint = other.Content; - } - /// /// Gets the constraint text. /// diff --git a/src/Microsoft.AspNetCore.Routing/Template/RouteTemplate.cs b/src/Microsoft.AspNetCore.Routing/Template/RouteTemplate.cs index c79cfd4158..d482f3a1b4 100644 --- a/src/Microsoft.AspNetCore.Routing/Template/RouteTemplate.cs +++ b/src/Microsoft.AspNetCore.Routing/Template/RouteTemplate.cs @@ -5,8 +5,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using Microsoft.AspNetCore.Dispatcher.Patterns; -using Other = Microsoft.AspNetCore.Dispatcher.Patterns.RoutePattern; namespace Microsoft.AspNetCore.Routing.Template { @@ -15,25 +13,6 @@ namespace Microsoft.AspNetCore.Routing.Template { private const string SeparatorString = "/"; - public RouteTemplate(Other other) - { - TemplateText = other.RawText; - Segments = new List(other.PathSegments.Select(p => new TemplateSegment(p))); - 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 RouteTemplate(string template, List segments) { if (segments == null) @@ -99,50 +78,5 @@ namespace Microsoft.AspNetCore.Routing.Template return null; } - - /// - /// Converts the to the equivalent - /// - /// - /// A . - public Other ToRoutePattern() - { - var builder = RoutePatternBuilder.Create(TemplateText); - - for (var i = 0; i < Segments.Count; i++) - { - var segment = Segments[i]; - - var parts = new List(); - for (var j = 0; j < segment.Parts.Count; j++) - { - var part = segment.Parts[j]; - if (part.IsLiteral && part.IsOptionalSeperator) - { - parts.Add(RoutePatternPart.CreateSeparator(part.Text)); - } - else if (part.IsLiteral) - { - parts.Add(RoutePatternPart.CreateLiteral(part.Text)); - } - else - { - var kind = part.IsCatchAll ? - RoutePatternParameterKind.CatchAll : - part.IsOptional ? - RoutePatternParameterKind.Optional : - RoutePatternParameterKind.Standard; - - var constraints = part.InlineConstraints.Select(c => ConstraintReference.Create(c.Constraint)).ToArray(); - - parts.Add(RoutePatternPart.CreateParameter(part.Name, part.DefaultValue, kind, constraints)); - } - } - - builder.AddPathSegment(parts.ToArray()); - } - - return builder.Build(); - } } } diff --git a/src/Microsoft.AspNetCore.Routing/Template/TemplateBinder.cs b/src/Microsoft.AspNetCore.Routing/Template/TemplateBinder.cs index 48364b352b..68770fd2bc 100644 --- a/src/Microsoft.AspNetCore.Routing/Template/TemplateBinder.cs +++ b/src/Microsoft.AspNetCore.Routing/Template/TemplateBinder.cs @@ -2,44 +2,359 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using Microsoft.AspNetCore.Dispatcher; +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 RoutePatternBinder _binder; + private readonly UrlEncoder _urlEncoder; + private readonly ObjectPool _pool; - public TemplateBinder(RoutePatternBinder binder) + private readonly RouteValueDictionary _defaults; + private readonly RouteValueDictionary _filters; + private readonly RouteTemplate _template; + + public TemplateBinder( + UrlEncoder urlEncoder, + ObjectPool pool, + RouteTemplate template, + RouteValueDictionary defaults) { - if (binder == null) + if (urlEncoder == null) { - throw new ArgumentNullException(nameof(binder)); + throw new ArgumentNullException(nameof(urlEncoder)); } - _binder = binder; + 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 acceptedValues, var combinedValues) = _binder.GetValues(ambientValues, values); - if (acceptedValues == null || combinedValues == null) + 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++) { - return null; + 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 = acceptedValues.AsRouteValueDictionary(), - CombinedValues = combinedValues.AsRouteValueDictionary(), + AcceptedValues = context.AcceptedValues, + CombinedValues = combinedValues, }; } // Step 2: If the route is a match generate the appropriate URI public string BindValues(RouteValueDictionary acceptedValues) { - return _binder.BindValues(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; } /// @@ -72,5 +387,66 @@ namespace Microsoft.AspNetCore.Routing.Template } } } + + 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/Microsoft.AspNetCore.Routing/Template/TemplateMatcher.cs b/src/Microsoft.AspNetCore.Routing/Template/TemplateMatcher.cs index bfc51eab4a..e7cc2ab9f5 100644 --- a/src/Microsoft.AspNetCore.Routing/Template/TemplateMatcher.cs +++ b/src/Microsoft.AspNetCore.Routing/Template/TemplateMatcher.cs @@ -2,8 +2,10 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using Microsoft.AspNetCore.Dispatcher; +using System.Collections.Generic; +using System.Diagnostics; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing.Internal; namespace Microsoft.AspNetCore.Routing.Template { @@ -17,7 +19,6 @@ namespace Microsoft.AspNetCore.Routing.Template private readonly object[] _defaultValues; private static readonly char[] Delimiters = new char[] { SeparatorChar }; - private RoutePatternMatcher _routePatternMatcher; public TemplateMatcher( RouteTemplate template, @@ -56,9 +57,6 @@ namespace Microsoft.AspNetCore.Routing.Template _defaultValues[i] = value; } } - - var routePattern = Template.ToRoutePattern(); - _routePatternMatcher = new RoutePatternMatcher(routePattern, Defaults); } public RouteValueDictionary Defaults { get; } @@ -72,7 +70,387 @@ namespace Microsoft.AspNetCore.Routing.Template throw new ArgumentNullException(nameof(values)); } - return _routePatternMatcher.TryMatch(path, 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/Microsoft.AspNetCore.Routing/Template/TemplateParser.cs b/src/Microsoft.AspNetCore.Routing/Template/TemplateParser.cs index d5c2da0120..51f598a486 100644 --- a/src/Microsoft.AspNetCore.Routing/Template/TemplateParser.cs +++ b/src/Microsoft.AspNetCore.Routing/Template/TemplateParser.cs @@ -1,28 +1,525 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// 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.Dispatcher.Patterns; +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) { - throw new ArgumentNullException(routeTemplate); + routeTemplate = String.Empty; } - try + if (IsInvalidRouteTemplate(routeTemplate)) { - var inner = RoutePattern.Parse(routeTemplate); - return new RouteTemplate(inner); + throw new ArgumentException(Resources.TemplateRoute_InvalidRouteTemplate, nameof(routeTemplate)); } - catch (ArgumentException ex) when (ex.InnerException is RoutePatternException) + + var context = new TemplateParserContext(routeTemplate); + var segments = new List(); + + while (context.Next()) { - throw new ArgumentException(ex.InnerException.Message, nameof(routeTemplate), ex.InnerException); + 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 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/Microsoft.AspNetCore.Routing/Template/TemplatePart.cs b/src/Microsoft.AspNetCore.Routing/Template/TemplatePart.cs index f195c50102..70f588a41a 100644 --- a/src/Microsoft.AspNetCore.Routing/Template/TemplatePart.cs +++ b/src/Microsoft.AspNetCore.Routing/Template/TemplatePart.cs @@ -5,47 +5,12 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using Other = Microsoft.AspNetCore.Dispatcher.Patterns.RoutePatternPart; namespace Microsoft.AspNetCore.Routing.Template { [DebuggerDisplay("{DebuggerToString()}")] public class TemplatePart { - public TemplatePart() - { - } - - public TemplatePart(Other other) - { - IsLiteral = other.IsLiteral || other.IsSeparator; - IsParameter = other.IsParameter; - - if (other.IsLiteral && other is Microsoft.AspNetCore.Dispatcher.Patterns.RoutePatternLiteral literal) - { - Text = literal.Content; - } - else if (other.IsParameter && other is Dispatcher.Patterns.RoutePatternParameter parameter) - { - // Text is unused by TemplatePart and assumed to be null when the part is a parameter. - Name = parameter.Name; - IsCatchAll = parameter.IsCatchAll; - IsOptional = parameter.IsOptional; - DefaultValue = parameter.DefaultValue; - InlineConstraints = parameter.Constraints?.Select(p => new InlineConstraint(p)); - } - else if (other.IsSeparator && other is Dispatcher.Patterns.RoutePatternSeparator separator) - { - Text = separator.Content; - IsOptionalSeperator = true; - } - else - { - // Unreachable - throw new NotSupportedException(); - } - } - public static TemplatePart CreateLiteral(string text) { return new TemplatePart() diff --git a/src/Microsoft.AspNetCore.Routing/Template/TemplateSegment.cs b/src/Microsoft.AspNetCore.Routing/Template/TemplateSegment.cs index f7fb6e9cab..4a86526509 100644 --- a/src/Microsoft.AspNetCore.Routing/Template/TemplateSegment.cs +++ b/src/Microsoft.AspNetCore.Routing/Template/TemplateSegment.cs @@ -4,22 +4,12 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using Other = Microsoft.AspNetCore.Dispatcher.Patterns.RoutePatternPathSegment; namespace Microsoft.AspNetCore.Routing.Template { [DebuggerDisplay("{DebuggerToString()}")] public class TemplateSegment { - public TemplateSegment() - { - } - - public TemplateSegment(Other other) - { - Parts = new List(other.Parts.Select(s => new TemplatePart(s))); - } - public bool IsSimple => Parts.Count == 1; public List Parts { get; } = new List(); diff --git a/shared/Microsoft.AspNetCore.Routing.UrlMatchingTree.Sources/InboundMatch.cs b/src/Microsoft.AspNetCore.Routing/Tree/InboundMatch.cs similarity index 57% rename from shared/Microsoft.AspNetCore.Routing.UrlMatchingTree.Sources/InboundMatch.cs rename to src/Microsoft.AspNetCore.Routing/Tree/InboundMatch.cs index 4e20ef93e6..57f1b6db7b 100644 --- a/shared/Microsoft.AspNetCore.Routing.UrlMatchingTree.Sources/InboundMatch.cs +++ b/src/Microsoft.AspNetCore.Routing/Tree/InboundMatch.cs @@ -2,42 +2,21 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Diagnostics; -#if ROUTING using Microsoft.AspNetCore.Routing.Template; -#elif DISPATCHER -using Microsoft.AspNetCore.Dispatcher; -#else -#error -#endif -#if ROUTING namespace Microsoft.AspNetCore.Routing.Tree -#elif DISPATCHER -namespace Microsoft.AspNetCore.Dispatcher -#else -#error -#endif { -#if ROUTING /// /// A candidate route to match incoming URLs in a . /// [DebuggerDisplay("{DebuggerToString(),nq}")] - public -#elif DISPATCHER - [DebuggerDisplay("{DebuggerToString(),nq}")] - internal -#else -#error -#endif - class InboundMatch + public class InboundMatch { /// /// Gets or sets the . /// public InboundRouteEntry Entry { get; set; } -#if ROUTING /// /// Gets or sets the . /// @@ -47,19 +26,5 @@ namespace Microsoft.AspNetCore.Dispatcher { return TemplateMatcher?.Template?.TemplateText; } - -#elif DISPATCHER - /// - /// Gets or sets the . - /// - public RoutePatternMatcher RoutePatternMatcher { get; set; } - - private string DebuggerToString() - { - return RoutePatternMatcher?.RoutePattern?.RawText; - } -#else -#error -#endif -} + } } diff --git a/shared/Microsoft.AspNetCore.Routing.UrlMatchingTree.Sources/InboundRouteEntry.cs b/src/Microsoft.AspNetCore.Routing/Tree/InboundRouteEntry.cs similarity index 56% rename from shared/Microsoft.AspNetCore.Routing.UrlMatchingTree.Sources/InboundRouteEntry.cs rename to src/Microsoft.AspNetCore.Routing/Tree/InboundRouteEntry.cs index ee76a752af..7c4a5f0abc 100644 --- a/shared/Microsoft.AspNetCore.Routing.UrlMatchingTree.Sources/InboundRouteEntry.cs +++ b/src/Microsoft.AspNetCore.Routing/Tree/InboundRouteEntry.cs @@ -2,40 +2,31 @@ // 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.Dispatcher.Patterns; -#if ROUTING using Microsoft.AspNetCore.Routing.Template; -#elif DISPATCHER -using Microsoft.AspNetCore.Dispatcher; -#else -#error -#endif -#if ROUTING namespace Microsoft.AspNetCore.Routing.Tree -#elif DISPATCHER -namespace Microsoft.AspNetCore.Dispatcher -#else -#error -#endif { -#if ROUTING /// - /// Used to build a . Represents a route template that will be used to match incoming + /// Used to build an . Represents a URL template tha will be used to match incoming /// request URLs. /// - public -#elif DISPATCHER - /// - /// Used to build a . Represents a route pattern that will be used to match incoming - /// request URLs. - /// - internal -#else -#error -#endif - class InboundRouteEntry + 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. /// @@ -52,54 +43,14 @@ namespace Microsoft.AspNetCore.Dispatcher /// public decimal Precedence { get; set; } -#if ROUTING /// /// Gets or sets the name of the route. /// public string RouteName { get; set; } - /// - /// Gets or sets the route constraints. - /// - public IDictionary Constraints { get; set; } - /// /// Gets or sets the . /// public RouteTemplate RouteTemplate { 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; } - -#elif DISPATCHER - /// - /// Gets or sets the array of endpoints associated with the entry. - /// - public Endpoint[] Endpoints { get; set; } - - /// - /// Gets or sets the dispatcher value constraints. - /// - public IDictionary Constraints { get; set; } - - /// - /// Gets or sets the dispatcher value defaults. - /// - public DispatcherValueCollection Defaults { get; set; } - - /// - /// Gets or sets the . - /// - public RoutePattern RoutePattern { get; set; } -#else -#error -#endif } } diff --git a/src/Microsoft.AspNetCore.Routing/Tree/TreeRouteBuilder.cs b/src/Microsoft.AspNetCore.Routing/Tree/TreeRouteBuilder.cs index 526b25c47a..e5f7d80823 100644 --- a/src/Microsoft.AspNetCore.Routing/Tree/TreeRouteBuilder.cs +++ b/src/Microsoft.AspNetCore.Routing/Tree/TreeRouteBuilder.cs @@ -5,9 +5,11 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using Microsoft.AspNetCore.Dispatcher; +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 { @@ -18,12 +20,21 @@ namespace Microsoft.AspNetCore.Routing.Tree { private readonly ILogger _logger; private readonly ILogger _constraintLogger; - private readonly RoutePatternBinderFactory _binderFactory; + private readonly UrlEncoder _urlEncoder; + private readonly ObjectPool _objectPool; private readonly IInlineConstraintResolver _constraintResolver; + /// + /// Initializes a new instance of . + /// + /// The . + /// The . + /// The . + /// The . public TreeRouteBuilder( ILoggerFactory loggerFactory, - RoutePatternBinderFactory binderFactory, + UrlEncoder urlEncoder, + ObjectPool objectPool, IInlineConstraintResolver constraintResolver) { if (loggerFactory == null) @@ -31,9 +42,14 @@ namespace Microsoft.AspNetCore.Routing.Tree throw new ArgumentNullException(nameof(loggerFactory)); } - if (binderFactory == null) + if (urlEncoder == null) { - throw new ArgumentNullException(nameof(binderFactory)); + throw new ArgumentNullException(nameof(urlEncoder)); + } + + if (objectPool == null) + { + throw new ArgumentNullException(nameof(objectPool)); } if (constraintResolver == null) @@ -41,7 +57,8 @@ namespace Microsoft.AspNetCore.Routing.Tree throw new ArgumentNullException(nameof(constraintResolver)); } - _binderFactory = binderFactory; + _urlEncoder = urlEncoder; + _objectPool = objectPool; _constraintResolver = constraintResolver; _logger = loggerFactory.CreateLogger(); @@ -76,7 +93,7 @@ namespace Microsoft.AspNetCore.Routing.Tree { Handler = handler, Order = order, - Precedence = Template.RoutePrecedence.ComputeInbound(routeTemplate), + Precedence = RoutePrecedence.ComputeInbound(routeTemplate), RouteName = routeName, RouteTemplate = routeTemplate, }; @@ -148,7 +165,7 @@ namespace Microsoft.AspNetCore.Routing.Tree { Handler = handler, Order = order, - Precedence = Template.RoutePrecedence.ComputeOutbound(routeTemplate), + Precedence = RoutePrecedence.ComputeOutbound(routeTemplate), RequiredLinkValues = requiredLinkValues, RouteName = routeName, RouteTemplate = routeTemplate, @@ -221,19 +238,21 @@ namespace Microsoft.AspNetCore.Routing.Tree foreach (var entry in InboundEntries) { - if (!trees.TryGetValue(entry.Order, out var tree)) + UrlMatchingTree tree; + if (!trees.TryGetValue(entry.Order, out tree)) { tree = new UrlMatchingTree(entry.Order); trees.Add(entry.Order, tree); } - UrlMatchingTree.AddEntryToTree(tree, entry); + AddEntryToTree(tree, entry); } return new TreeRouter( trees.Values.OrderBy(tree => tree.Order).ToArray(), OutboundEntries, - _binderFactory, + _urlEncoder, + _objectPool, _logger, _constraintLogger, version); @@ -248,5 +267,167 @@ namespace Microsoft.AspNetCore.Routing.Tree 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/Microsoft.AspNetCore.Routing/Tree/TreeRouter.cs b/src/Microsoft.AspNetCore.Routing/Tree/TreeRouter.cs index 9152dde3bb..31a1091093 100644 --- a/src/Microsoft.AspNetCore.Routing/Tree/TreeRouter.cs +++ b/src/Microsoft.AspNetCore.Routing/Tree/TreeRouter.cs @@ -2,14 +2,16 @@ // 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.Dispatcher; -using Microsoft.AspNetCore.Dispatcher.Internal; 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 { @@ -29,10 +31,22 @@ namespace Microsoft.AspNetCore.Routing.Tree 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, - RoutePatternBinderFactory binderFactory, + UrlEncoder urlEncoder, + ObjectPool objectPool, ILogger routeLogger, ILogger constraintLogger, int version) @@ -47,9 +61,14 @@ namespace Microsoft.AspNetCore.Routing.Tree throw new ArgumentNullException(nameof(linkGenerationEntries)); } - if (binderFactory == null) + if (urlEncoder == null) { - throw new ArgumentNullException(nameof(binderFactory)); + throw new ArgumentNullException(nameof(urlEncoder)); + } + + if (objectPool == null) + { + throw new ArgumentNullException(nameof(objectPool)); } if (routeLogger == null) @@ -72,7 +91,8 @@ namespace Microsoft.AspNetCore.Routing.Tree foreach (var entry in linkGenerationEntries) { - var binder = new TemplateBinder(binderFactory.Create(entry.RouteTemplate.ToRoutePattern(), entry.Defaults)); + + var binder = new TemplateBinder(urlEncoder, objectPool, entry.RouteTemplate, entry.Defaults); var outboundMatch = new OutboundMatch() { Entry = entry, TemplateBinder = binder }; outboundMatches.Add(outboundMatch); @@ -213,9 +233,112 @@ namespace Microsoft.AspNetCore.Routing.Tree } } + 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) { - if (_namedEntries.TryGetValue(context.RouteName, out var match)) + OutboundMatch match; + if (_namedEntries.TryGetValue(context.RouteName, out match)) { var path = GenerateVirtualPath(context, match.Entry, match.TemplateBinder); if (path != null) diff --git a/shared/Microsoft.AspNetCore.Routing.UrlMatchingTree.Sources/UrlMatchingNode.cs b/src/Microsoft.AspNetCore.Routing/Tree/UrlMatchingNode.cs similarity index 71% rename from shared/Microsoft.AspNetCore.Routing.UrlMatchingTree.Sources/UrlMatchingNode.cs rename to src/Microsoft.AspNetCore.Routing/Tree/UrlMatchingNode.cs index ca0f9b80fa..ffc387efe9 100644 --- a/shared/Microsoft.AspNetCore.Routing.UrlMatchingTree.Sources/UrlMatchingNode.cs +++ b/src/Microsoft.AspNetCore.Routing/Tree/UrlMatchingNode.cs @@ -6,16 +6,12 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; -#if ROUTING namespace Microsoft.AspNetCore.Routing.Tree -#elif DISPATCHER -namespace Microsoft.AspNetCore.Dispatcher -#else -#error -#endif { + /// + /// A node in a . + /// [DebuggerDisplay("{DebuggerToString(),nq}")] -#if ROUTING public class UrlMatchingNode { /// @@ -29,37 +25,11 @@ namespace Microsoft.AspNetCore.Dispatcher Matches = new List(); Literals = new Dictionary(StringComparer.OrdinalIgnoreCase); } - - private string DebuggerToString() - { - return $"Length: {Depth}, Matches: {string.Join(" | ", Matches?.Select(m => $"({m.TemplateMatcher.Template.TemplateText})"))}"; - } -#elif DISPATCHER - internal class UrlMatchingNode - { + /// - /// Initializes a new instance of . + /// Gets the length of the path to this node in the . /// - /// The length of the path to this node in the . - public UrlMatchingNode(int depth) - { - Depth = depth; - - Matches = new List(); - Literals = new Dictionary(StringComparer.OrdinalIgnoreCase); - } - - private string DebuggerToString() - { - return $"Length: {Depth}, Matches: {string.Join(" | ", Matches?.Select(m => $"({m.RoutePatternMatcher.RoutePattern.RawText})"))}"; - } -#else -#error -#endif - /// - /// Gets the length of the path to this node in the . - /// - public int Depth { get; } + public int Depth { get; } /// /// Gets or sets a value indicating whether this node represents a catch all segment. @@ -81,26 +51,31 @@ namespace Microsoft.AspNetCore.Dispatcher /// /// Gets or sets the representing - /// parameter segments with constraints following this segment. + /// parameter segments with constraints following this segment in the . /// public UrlMatchingNode ConstrainedParameters { get; set; } /// /// Gets or sets the representing - /// parameter segments following this segment. + /// 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. + /// 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. + /// 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/Microsoft.AspNetCore.Routing/Tree/UrlMatchingTree.cs b/src/Microsoft.AspNetCore.Routing/Tree/UrlMatchingTree.cs new file mode 100644 index 0000000000..90528d75b9 --- /dev/null +++ b/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/Microsoft.AspNetCore.Routing/breakingchanges.netcore.json b/src/Microsoft.AspNetCore.Routing/breakingchanges.netcore.json deleted file mode 100644 index a53d01d9df..0000000000 --- a/src/Microsoft.AspNetCore.Routing/breakingchanges.netcore.json +++ /dev/null @@ -1,17 +0,0 @@ -[ - { - "TypeId": "public class Microsoft.AspNetCore.Routing.Tree.TreeRouter : Microsoft.AspNetCore.Routing.IRouter", - "MemberId": "public .ctor(Microsoft.AspNetCore.Routing.Tree.UrlMatchingTree[] trees, System.Collections.Generic.IEnumerable linkGenerationEntries, System.Text.Encodings.Web.UrlEncoder urlEncoder, Microsoft.Extensions.ObjectPool.ObjectPool objectPool, Microsoft.Extensions.Logging.ILogger routeLogger, Microsoft.Extensions.Logging.ILogger constraintLogger, System.Int32 version)", - "Kind": "Removal" - }, - { - "TypeId": "public class Microsoft.AspNetCore.Routing.Tree.TreeRouteBuilder", - "MemberId": "public .ctor(Microsoft.Extensions.Logging.ILoggerFactory loggerFactory, System.Text.Encodings.Web.UrlEncoder urlEncoder, Microsoft.Extensions.ObjectPool.ObjectPool objectPool, Microsoft.AspNetCore.Routing.IInlineConstraintResolver constraintResolver)", - "Kind": "Removal" - }, - { - "TypeId": "public class Microsoft.AspNetCore.Routing.Template.TemplateBinder", - "MemberId": "public .ctor(System.Text.Encodings.Web.UrlEncoder urlEncoder, Microsoft.Extensions.ObjectPool.ObjectPool pool, Microsoft.AspNetCore.Routing.Template.RouteTemplate template, Microsoft.AspNetCore.Routing.RouteValueDictionary defaults)", - "Kind": "Removal" - } -] diff --git a/test/Microsoft.AspNetCore.Dispatcher.Abstractions.Test/MetadataCollectionTest.cs b/test/Microsoft.AspNetCore.Dispatcher.Abstractions.Test/MetadataCollectionTest.cs deleted file mode 100644 index d76bcf16d3..0000000000 --- a/test/Microsoft.AspNetCore.Dispatcher.Abstractions.Test/MetadataCollectionTest.cs +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Linq; -using Xunit; - -namespace Microsoft.AspNetCore.Dispatcher -{ - public class MetadataCollectionTest - { - [Fact] - public void GetMetadata_ReturnsLastMatch() - { - // Arrange - var items = new object[] - { - new AuthNMetadata(), - new AuthZMetadata(), - new AuthNMetadata(), - }; - - var collection = new MetadataCollection(items); - - // Act - var result = collection.GetMetadata(); - - // Assert - Assert.Same(items[2], result); - } - - [Fact] - public void GetOrderedMetadata_ReturnsAllMatches() - { - // Arrange - var items = new object[] - { - new AuthNMetadata(), - new AuthZMetadata(), - new AuthNMetadata(), - }; - - var collection = new MetadataCollection(items); - - // Act - var result = collection.GetOrderedMetadata(); - - // Assert - Assert.Equal(new object[] { items[0], items[2] }, result); - } - - [Fact] - public void GetEnumerator_IncludesAllItems() - { - // Arrange - var items = new object[] - { - new AuthNMetadata(), - new AuthZMetadata(), - new AuthNMetadata(), - }; - - var collection = new MetadataCollection(items); - - // Act - var result = collection.ToArray(); - - // Assert - Assert.Equal(items, result); - } - - private class AuthNMetadata - { - } - - private class AuthZMetadata - { - } - } -} diff --git a/test/Microsoft.AspNetCore.Dispatcher.Abstractions.Test/Microsoft.AspNetCore.Dispatcher.Abstractions.Test.csproj b/test/Microsoft.AspNetCore.Dispatcher.Abstractions.Test/Microsoft.AspNetCore.Dispatcher.Abstractions.Test.csproj deleted file mode 100644 index 01d505585e..0000000000 --- a/test/Microsoft.AspNetCore.Dispatcher.Abstractions.Test/Microsoft.AspNetCore.Dispatcher.Abstractions.Test.csproj +++ /dev/null @@ -1,11 +0,0 @@ - - - - $(StandardTestTfms) - - - - - - - diff --git a/test/Microsoft.AspNetCore.Dispatcher.FunctionalTest/ApiAppFixture.cs b/test/Microsoft.AspNetCore.Dispatcher.FunctionalTest/ApiAppFixture.cs deleted file mode 100644 index f699935574..0000000000 --- a/test/Microsoft.AspNetCore.Dispatcher.FunctionalTest/ApiAppFixture.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Net.Http; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.TestHost; - -namespace Microsoft.AspNetCore.Dispatcher.FunctionalTest -{ - public class ApiAppFixture : IDisposable - { - public ApiAppFixture() - { - var builder = new WebHostBuilder(); - builder.UseStartup(); - - Server = new TestServer(builder); - - Client = Server.CreateClient(); - Client.BaseAddress = new Uri("http://locahost"); - } - - public HttpClient Client { get; } - - public TestServer Server { get; } - - public void Dispose() - { - Client.Dispose(); - Server.Dispose(); - } - } -} diff --git a/test/Microsoft.AspNetCore.Dispatcher.FunctionalTest/ApiAppStartup.cs b/test/Microsoft.AspNetCore.Dispatcher.FunctionalTest/ApiAppStartup.cs deleted file mode 100644 index d5d195c436..0000000000 --- a/test/Microsoft.AspNetCore.Dispatcher.FunctionalTest/ApiAppStartup.cs +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -namespace Microsoft.AspNetCore.Dispatcher.FunctionalTest -{ - public class ApiAppStartup - { - public void ConfigureServices(IServiceCollection services) - { - services.AddLogging(); - services.AddDispatcher(); - - // This is a temporary layering issue, don't worry about it :) - services.AddRouting(); - services.AddSingleton(); - - services.Configure(ConfigureDispatcher); - } - - public void Configure(IApplicationBuilder app, ILogger logger) - { - app.UseDispatcher(); - - app.Use(next => async (context) => - { - logger.LogInformation("Executing fake CORS middleware"); - - var feature = context.Features.Get(); - var policy = feature.Endpoint?.Metadata.GetMetadata(); - logger.LogInformation("using CORS policy {PolicyName}", policy?.Name ?? "default"); - - await next(context); - }); - - app.Use(next => async (context) => - { - logger.LogInformation("Executing fake AuthZ middleware"); - - var feature = context.Features.Get(); - var policy = feature.Endpoint?.Metadata.GetMetadata(); - if (policy != null) - { - logger.LogInformation("using Auth policy {PolicyName}", policy.Name); - } - - await next(context); - }); - } - - public void ConfigureDispatcher(DispatcherOptions options) - { - options.Matchers.Add(new TreeMatcher() - { - Endpoints = - { - new RoutePatternEndpoint("api/products", Products_Fallback), - new RoutePatternEndpoint("api/products", new { controller = "Products", action = "Get", }, "GET", Products_Get), - new RoutePatternEndpoint("api/products/{id}", new { controller = "Products", action = "Get", }, "GET", Products_GetWithId), - new RoutePatternEndpoint("api/products", new { controller = "Products", action = "Post", }, "POST", Products_Post), - new RoutePatternEndpoint("api/products/{id}", new { controller = "Products", action = "Put", }, "PUT", Products_Put), - }, - - Selectors = - { - new HttpMethodEndpointSelector(), - }, - }, new RoutePatternEndpointHandlerFactory()); - } - - private Task Products_Fallback(HttpContext httpContext) => httpContext.Response.WriteAsync("Hello, Products_Fallback"); - - private Task Products_Get(HttpContext httpContext) => httpContext.Response.WriteAsync("Hello, Products_Get"); - - private Task Products_GetWithId(HttpContext httpContext) => httpContext.Response.WriteAsync("Hello, Products_GetWithId"); - - private Task Products_Post(HttpContext httpContext) => httpContext.Response.WriteAsync("Hello, Products_Post"); - - private Task Products_Put(HttpContext httpContext) => httpContext.Response.WriteAsync("Hello, Products_Put"); - - private class CorsPolicyMetadata - { - public string Name { get; set; } - } - - private class AuthorizationPolicyMetadata - { - public string Name { get; set; } - } - } -} diff --git a/test/Microsoft.AspNetCore.Dispatcher.FunctionalTest/ApiAppTest.cs b/test/Microsoft.AspNetCore.Dispatcher.FunctionalTest/ApiAppTest.cs deleted file mode 100644 index 36bd16e368..0000000000 --- a/test/Microsoft.AspNetCore.Dispatcher.FunctionalTest/ApiAppTest.cs +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Net; -using System.Net.Http; -using System.Threading.Tasks; -using Xunit; - -namespace Microsoft.AspNetCore.Dispatcher.FunctionalTest -{ - public class ApiAppTest : IClassFixture - { - public ApiAppTest(ApiAppFixture app) - { - Client = app.Client; - } - - public HttpClient Client { get; } - - [Fact] - public async Task ApiApp_CanRouteTo_LiteralEndpoint() - { - // Arrange - var request = new HttpRequestMessage(HttpMethod.Get, "/api/products"); - - // Act - var response = await Client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal("Hello, Products_Get", await response.Content.ReadAsStringAsync()); - } - - [Fact] - public async Task ApiApp_RoutesTo_EndpointWithMatchingHttpMethod() - { - // Arrange - var request = new HttpRequestMessage(HttpMethod.Post, "/api/products"); - - // Act - var response = await Client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal("Hello, Products_Post", await response.Content.ReadAsStringAsync()); - } - - [Fact] - public async Task ApiApp_RoutesTo_EndpointWithMatchingHttpMethod_AndMatchingRoute() - { - // Arrange - var request = new HttpRequestMessage(HttpMethod.Get, "/api/products/3"); - - // Act - var response = await Client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal("Hello, Products_GetWithId", await response.Content.ReadAsStringAsync()); - } - - [Fact] - public async Task ApiApp_RoutesTo_EndpointWithMatchingHttpMethod_DoesNotMatchExpectedRoute() - { - // Arrange - var request = new HttpRequestMessage(HttpMethod.Put, "/api/services/2"); - - // Act - var response = await Client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); - } - - [Fact] - public async Task ApiApp_NoEndpointWithMatchingHttpMethod_FallbackEndpointSelected() - { - // Arrange - var request = new HttpRequestMessage(HttpMethod.Delete, "/api/products"); - - // Act - var response = await Client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal("Hello, Products_Fallback", await response.Content.ReadAsStringAsync()); - } - - [Fact] - public async Task ApiApp_NoEndpointWithMatchingHttpMethod_NoFallbackEndpointMatched() - { - // Arrange - var request = new HttpRequestMessage(HttpMethod.Delete, "/api/products/4"); - - // Act - var response = await Client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); - } - } -} diff --git a/test/Microsoft.AspNetCore.Dispatcher.FunctionalTest/Microsoft.AspNetCore.Dispatcher.FunctionalTest.csproj b/test/Microsoft.AspNetCore.Dispatcher.FunctionalTest/Microsoft.AspNetCore.Dispatcher.FunctionalTest.csproj deleted file mode 100644 index 6d580802da..0000000000 --- a/test/Microsoft.AspNetCore.Dispatcher.FunctionalTest/Microsoft.AspNetCore.Dispatcher.FunctionalTest.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - - $(StandardTestTfms) - - - - - - - - - - - diff --git a/test/Microsoft.AspNetCore.Dispatcher.Test/Constraints/CompositeDispatcherValueConstraintTest.cs b/test/Microsoft.AspNetCore.Dispatcher.Test/Constraints/CompositeDispatcherValueConstraintTest.cs deleted file mode 100644 index 2c24ab60f0..0000000000 --- a/test/Microsoft.AspNetCore.Dispatcher.Test/Constraints/CompositeDispatcherValueConstraintTest.cs +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Linq.Expressions; -using Microsoft.AspNetCore.Http; -using Moq; -using Xunit; - -namespace Microsoft.AspNetCore.Dispatcher -{ - public class CompositeDispatcherValueConstraintTest - { - [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 CompositeDispatcherValueConstraint(new[] { inner1.Object, inner2.Object }); - var actual = TestConstraint(constraint, null); - - // Assert - Assert.Equal(expected, actual); - } - - static Expression> ConstraintMatchMethodExpression = - c => c.Match( - It.IsAny()); - - private static Mock MockConstraintWithResult(bool result) - { - var mock = new Mock(); - mock.Setup(ConstraintMatchMethodExpression) - .Returns(result) - .Verifiable(); - return mock; - } - - private static bool TestConstraint(IDispatcherValueConstraint constraint, object value, Action routeConfig = null) - { - var httpContext = new DefaultHttpContext(); - var values = new DispatcherValueCollection() { { "fake", value } }; - var constraintPurpose = ConstraintPurpose.IncomingRequest; - - var dispatcherValueConstraintContext = new DispatcherValueConstraintContext(httpContext, values, constraintPurpose); - - return constraint.Match(dispatcherValueConstraintContext); - } - } -} diff --git a/test/Microsoft.AspNetCore.Dispatcher.Test/Constraints/RegexDispatcherValueConstraintTest.cs b/test/Microsoft.AspNetCore.Dispatcher.Test/Constraints/RegexDispatcherValueConstraintTest.cs deleted file mode 100644 index 6b6f23bc62..0000000000 --- a/test/Microsoft.AspNetCore.Dispatcher.Test/Constraints/RegexDispatcherValueConstraintTest.cs +++ /dev/null @@ -1,120 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Text.RegularExpressions; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Testing; -using Xunit; - -namespace Microsoft.AspNetCore.Dispatcher -{ - public class RegexDispatcherValueConstraintTest - { - [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 RegexDispatcherValueConstraint(constraintValue); - var values = new DispatcherValueCollection(new { controller = routeValue }); - - // Act - var match = TestConstraint(constraint, values, "controller"); - - // Assert - Assert.Equal(shouldMatch, match); - } - - [Fact] - public void RegexConstraint_TakesRegexAsInput_SimpleMatch() - { - // Arrange - var constraint = new RegexDispatcherValueConstraint(new Regex("^abc$")); - var values = new DispatcherValueCollection(new { controller = "abc" }); - - // Act - var match = TestConstraint(constraint, values, "controller"); - - // Assert - Assert.True(match); - } - - [Fact] - public void RegexConstraintConstructedWithRegex_SimpleFailedMatch() - { - // Arrange - var constraint = new RegexDispatcherValueConstraint(new Regex("^abc$")); - var values = new DispatcherValueCollection(new { controller = "Abc" }); - - // Act - var match = TestConstraint(constraint, values, "controller"); - - // Assert - Assert.False(match); - } - - [Fact] - public void RegexConstraintFailsIfKeyIsNotFoundInRouteValues() - { - // Arrange - var constraint = new RegexDispatcherValueConstraint(new Regex("^abc$")); - var values = new DispatcherValueCollection(new { action = "abc" }); - - // Act - var match = TestConstraint(constraint, values, "controller"); - - // 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 RegexDispatcherValueConstraint("^([a-z]+)$"); - var values = new DispatcherValueCollection(new { controller = "\u0130" }); // Turkish upper-case dotted I - - using (new CultureReplacer(culture)) - { - // Act - var match = TestConstraint(constraint, values, "controller"); - - // Assert - Assert.False(match); - } - } - - private static bool TestConstraint(IDispatcherValueConstraint constraint, DispatcherValueCollection values, string routeKey) - { - var httpContext = new DefaultHttpContext(); - var constraintPurpose = ConstraintPurpose.IncomingRequest; - - var dispatcherValueConstraintContext = new DispatcherValueConstraintContext(httpContext, values, constraintPurpose) - { - Key = routeKey - }; - - return constraint.Match(dispatcherValueConstraintContext); - } - } -} diff --git a/test/Microsoft.AspNetCore.Dispatcher.Test/DefaultTemplateFactoryTest.cs b/test/Microsoft.AspNetCore.Dispatcher.Test/DefaultTemplateFactoryTest.cs deleted file mode 100644 index 90ab9ad210..0000000000 --- a/test/Microsoft.AspNetCore.Dispatcher.Test/DefaultTemplateFactoryTest.cs +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using Moq; -using Xunit; - -namespace Microsoft.AspNetCore.Dispatcher -{ - public class DefaultTemplateFactoryTest - { - [Fact] - public void GetTemplateFromKey_UsesMatchingComponent_SelectsTemplate() - { - // Arrange - var expected = Mock.Of