소스 검색

Merge remote-tracking branch 'Routing/rybrande/masterToSrc' into rybrande/Mondo22ToMaster

Ryan Brandenburg 7 년 전
부모
커밋
01b58c9a3d
100개의 변경된 파일2664개의 추가작업 그리고 4070개의 파일을 삭제
  1. 1 5
      src/Routing/Directory.Build.targets
  2. 4 6
      src/Routing/README.md
  3. 1 1
      src/Routing/benchmarkapps/Benchmarks/Benchmarks.csproj
  4. 10 10
      src/Routing/benchmarkapps/Benchmarks/StartupUsingEndpointRouting.cs
  5. 1 1
      src/Routing/benchmarkapps/Benchmarks/benchmarks.json
  6. 1 1
      src/Routing/benchmarks/Microsoft.AspNetCore.Routing.Performance/Microsoft.AspNetCore.Routing.Performance.csproj
  7. 0 182
      src/Routing/benchmarks/Microsoft.AspNetCore.Routing.Performance/RouteValueDictionaryBenchmark.cs
  8. 29 33
      src/Routing/build/dependencies.props
  9. 1 4
      src/Routing/build/repo.props
  10. 25 0
      src/Routing/samples/RoutingSandbox/AuthorizationMiddleware/AuthorizationAppBuilderExtensions.cs
  11. 25 0
      src/Routing/samples/RoutingSandbox/AuthorizationMiddleware/AuthorizationMetadata.cs
  12. 59 0
      src/Routing/samples/RoutingSandbox/AuthorizationMiddleware/AuthorizationMiddleware.cs
  13. 17 0
      src/Routing/samples/RoutingSandbox/AuthorizationMiddleware/EndpointConventionBuilderExtensions .cs
  14. 33 0
      src/Routing/samples/RoutingSandbox/HelloExtension/EndpointRouteBuilderExtensions.cs
  15. 25 0
      src/Routing/samples/RoutingSandbox/HelloExtension/HelloAppBuilderExtensions.cs
  16. 45 0
      src/Routing/samples/RoutingSandbox/HelloExtension/HelloMiddleware.cs
  17. 10 0
      src/Routing/samples/RoutingSandbox/HelloExtension/HelloOptions.cs
  18. 2 3
      src/Routing/samples/RoutingSandbox/RoutingSandbox.csproj
  19. 56 51
      src/Routing/samples/RoutingSandbox/UseEndpointRoutingStartup.cs
  20. 0 49
      src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/Endpoint.cs
  21. 0 201
      src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/EndpointMetadataCollection.cs
  22. 0 18
      src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/IEndpointFeature.cs
  23. 0 16
      src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/IRouteValuesFeature.cs
  24. 1 1
      src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/Microsoft.AspNetCore.Routing.Abstractions.csproj
  25. 9 0
      src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/Properties/AssemblyInfo.cs
  26. 0 58
      src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/Properties/Resources.Designer.cs
  27. 0 126
      src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/Resources.resx
  28. 0 718
      src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/RouteValueDictionary.cs
  29. 210 0
      src/Routing/src/Microsoft.AspNetCore.Routing/Builder/EndpointRouteBuilderExtensions.cs
  30. 28 5
      src/Routing/src/Microsoft.AspNetCore.Routing/Builder/EndpointRoutingApplicationBuilderExtensions.cs
  31. 34 7
      src/Routing/src/Microsoft.AspNetCore.Routing/CompositeEndpointDataSource.cs
  32. 6 8
      src/Routing/src/Microsoft.AspNetCore.Routing/ConfigureRouteOptions.cs
  33. 25 0
      src/Routing/src/Microsoft.AspNetCore.Routing/DefaultEndpointRouteBuilder.cs
  34. 1 1
      src/Routing/src/Microsoft.AspNetCore.Routing/DefaultLinkGenerator.cs
  35. 19 5
      src/Routing/src/Microsoft.AspNetCore.Routing/DependencyInjection/RoutingServiceCollectionExtensions.cs
  36. 19 0
      src/Routing/src/Microsoft.AspNetCore.Routing/EndpointModel.cs
  37. 1 1
      src/Routing/src/Microsoft.AspNetCore.Routing/EndpointNameAddressScheme.cs
  38. 0 13
      src/Routing/src/Microsoft.AspNetCore.Routing/EndpointOptions.cs
  39. 2 2
      src/Routing/src/Microsoft.AspNetCore.Routing/EndpointRoutingMiddleware.cs
  40. 12 0
      src/Routing/src/Microsoft.AspNetCore.Routing/IEndpointModelConvention.cs
  41. 18 0
      src/Routing/src/Microsoft.AspNetCore.Routing/IEndpointRouteBuilder.cs
  42. 169 0
      src/Routing/src/Microsoft.AspNetCore.Routing/Internal/ArrayBuilder.cs
  43. 1 6
      src/Routing/src/Microsoft.AspNetCore.Routing/Internal/LinkGenerationDecisionTree.cs
  44. 0 3
      src/Routing/src/Microsoft.AspNetCore.Routing/Matching/ILEmitTrieFactory.cs
  45. 0 2
      src/Routing/src/Microsoft.AspNetCore.Routing/Matching/ILEmitTrieJumpTable.cs
  46. 0 4
      src/Routing/src/Microsoft.AspNetCore.Routing/Matching/JumpTableBuilder.cs
  47. 2 6
      src/Routing/src/Microsoft.AspNetCore.Routing/Microsoft.AspNetCore.Routing.csproj
  48. 68 0
      src/Routing/src/Microsoft.AspNetCore.Routing/ModelEndpointDataSource.cs
  49. 227 0
      src/Routing/src/Microsoft.AspNetCore.Routing/Patterns/DefaultRoutePatternTransformer.cs
  50. 7 9
      src/Routing/src/Microsoft.AspNetCore.Routing/Patterns/RouteParameterParser.cs
  51. 27 1
      src/Routing/src/Microsoft.AspNetCore.Routing/Patterns/RoutePattern.cs
  52. 94 14
      src/Routing/src/Microsoft.AspNetCore.Routing/Patterns/RoutePatternFactory.cs
  53. 35 0
      src/Routing/src/Microsoft.AspNetCore.Routing/Patterns/RoutePatternTransformer.cs
  54. 37 0
      src/Routing/src/Microsoft.AspNetCore.Routing/RouteEndpointModel.cs
  55. 2 0
      src/Routing/src/Microsoft.AspNetCore.Routing/RouteOptions.cs
  56. 2 0
      src/Routing/src/Microsoft.AspNetCore.Routing/RouteValueEqualityComparer.cs
  57. 18 7
      src/Routing/src/Microsoft.AspNetCore.Routing/RouteValuesAddressScheme.cs
  58. 17 0
      src/Routing/src/Microsoft.AspNetCore.Routing/Template/RoutePrecedence.cs
  59. 7 0
      src/Routing/src/Microsoft.AspNetCore.Routing/Template/RouteTemplate.cs
  60. 0 7
      src/Routing/test/Directory.Build.props
  61. 0 142
      src/Routing/test/Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests/EndpointMetadataCollectionTests.cs
  62. 1 1
      src/Routing/test/Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests/Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests.csproj
  63. 0 2063
      src/Routing/test/Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests/RouteValueDictionaryTests.cs
  64. 1 7
      src/Routing/test/Microsoft.AspNetCore.Routing.DecisionTree.Sources.Tests/DecisionTreeBuilderTest.cs
  65. 1 1
      src/Routing/test/Microsoft.AspNetCore.Routing.DecisionTree.Sources.Tests/Microsoft.AspNetCore.Routing.DecisionTree.Sources.Tests.csproj
  66. 0 2
      src/Routing/test/Microsoft.AspNetCore.Routing.FunctionalTests/Benchmarks/EndpointRoutingBenchmarkTest.cs
  67. 1 3
      src/Routing/test/Microsoft.AspNetCore.Routing.FunctionalTests/Benchmarks/RouterBenchmarkTest.cs
  68. 36 4
      src/Routing/test/Microsoft.AspNetCore.Routing.FunctionalTests/EndpointRoutingSampleTest.cs
  69. 2 2
      src/Routing/test/Microsoft.AspNetCore.Routing.FunctionalTests/Microsoft.AspNetCore.Routing.FunctionalTests.csproj
  70. 16 0
      src/Routing/test/Microsoft.AspNetCore.Routing.FunctionalTests/RouterSampleTest.cs
  71. 88 15
      src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Builder/EndpointRoutingApplicationBuilderExtensionsTest.cs
  72. 101 0
      src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Builder/MapEndpointEndpointDataSourceBuilderExtensionsTest.cs
  73. 85 11
      src/Routing/test/Microsoft.AspNetCore.Routing.Tests/DefaultLinkGeneratorProcessTemplateTest.cs
  74. 33 20
      src/Routing/test/Microsoft.AspNetCore.Routing.Tests/DefaultLinkGeneratorTest.cs
  75. 3 2
      src/Routing/test/Microsoft.AspNetCore.Routing.Tests/EndpointFactory.cs
  76. 1 2
      src/Routing/test/Microsoft.AspNetCore.Routing.Tests/EndpointRoutingMiddlewareTest.cs
  77. 14 20
      src/Routing/test/Microsoft.AspNetCore.Routing.Tests/LinkGeneratorTestBase.cs
  78. 0 2
      src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Matching/ILEmitTrieFactoryTest.cs
  79. 0 2
      src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Matching/ILEmitTrieJumpTableTest.cs
  80. 4 1
      src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Matching/MatcherConformanceTest.cs
  81. 0 2
      src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Matching/NonVectorizedILEmitTrieJumpTableTest.cs
  82. 0 2
      src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Matching/VectorizedILEmitTrieJumpTableTest.cs
  83. 1 13
      src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Microsoft.AspNetCore.Routing.Tests.csproj
  84. 339 0
      src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Patterns/DefaultRoutePatternTransformerTest.cs
  85. 83 0
      src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Patterns/RoutePatternFactoryTest.cs
  86. 36 0
      src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RouteEndpointModelTest.cs
  87. 75 30
      src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RouteValuesAddressSchemeTest.cs
  88. 27 0
      src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Template/RoutePatternPrecedenceTests.cs
  89. 24 14
      src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Template/RoutePrecedenceTestsBase.cs
  90. 26 0
      src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Template/RouteTemplatePrecedenceTests.cs
  91. 0 7
      src/Routing/test/WebSites/Directory.Build.props
  92. 33 0
      src/Routing/test/WebSites/RoutingWebSite/HelloExtension/EndpointRouteBuilderExtensions.cs
  93. 25 0
      src/Routing/test/WebSites/RoutingWebSite/HelloExtension/HelloAppBuilderExtensions.cs
  94. 45 0
      src/Routing/test/WebSites/RoutingWebSite/HelloExtension/HelloMiddleware.cs
  95. 10 0
      src/Routing/test/WebSites/RoutingWebSite/HelloExtension/HelloOptions.cs
  96. 1 2
      src/Routing/test/WebSites/RoutingWebSite/RoutingWebSite.csproj
  97. 94 105
      src/Routing/test/WebSites/RoutingWebSite/UseEndpointRoutingStartup.cs
  98. 11 0
      src/Routing/test/WebSites/RoutingWebSite/UseRouterStartup.cs
  99. 1 1
      src/Routing/tools/Swaggatherer/Swaggatherer.csproj
  100. 3 9
      src/Routing/version.props

+ 1 - 5
src/Routing/Directory.Build.targets

@@ -1,10 +1,6 @@
 <Project>
   <PropertyGroup>
-    <RuntimeFrameworkVersion Condition=" '$(TargetFramework)' == 'netcoreapp2.0' ">$(MicrosoftNETCoreApp20PackageVersion)</RuntimeFrameworkVersion>
-    <RuntimeFrameworkVersion Condition=" '$(TargetFramework)' == 'netcoreapp2.1' ">$(MicrosoftNETCoreApp21PackageVersion)</RuntimeFrameworkVersion>
-    <RuntimeFrameworkVersion Condition=" '$(TargetFramework)' == 'netcoreapp2.2' ">$(MicrosoftNETCoreApp22PackageVersion)</RuntimeFrameworkVersion>
+    <RuntimeFrameworkVersion Condition=" '$(TargetFramework)' == 'netcoreapp3.0' ">$(MicrosoftNETCoreAppPackageVersion)</RuntimeFrameworkVersion>
     <NETStandardImplicitPackageVersion Condition=" '$(TargetFramework)' == 'netstandard2.0' ">$(NETStandardLibrary20PackageVersion)</NETStandardImplicitPackageVersion>
-    <!-- aspnet/BuildTools#662 Don't police what version of NetCoreApp we use -->
-    <NETCoreAppMaximumVersion>99.9</NETCoreAppMaximumVersion>
   </PropertyGroup>
 </Project>

+ 4 - 6
src/Routing/README.md

@@ -1,10 +1,8 @@
-ASP.NET Routing
-===
+ASP.NET Routing  [Archived]
+===========================
 
-AppVeyor: [![AppVeyor](https://ci.appveyor.com/api/projects/status/fe4o5h1s9ve86nyv/branch/master?svg=true)](https://ci.appveyor.com/project/aspnetci/Routing/branch/master)
-
-Travis:   [![Travis](https://travis-ci.org/aspnet/Routing.svg?branch=master)](https://travis-ci.org/aspnet/Routing)
+**This GitHub project has been archived.** Ongoing development on this project can be found in <https://github.com/aspnet/AspNetCore>.
 
 Contains routing middleware for routing requests to application logic.
 
-This project is part of ASP.NET Core. You can find samples, documentation and getting started instructions for ASP.NET Core at the [Home](https://github.com/aspnet/home) repo.
+This project is part of ASP.NET Core. You can find samples, documentation and getting started instructions for ASP.NET Core at the [AspNetCore](https://github.com/aspnet/AspNetCore) repo.

+ 1 - 1
src/Routing/benchmarkapps/Benchmarks/Benchmarks.csproj

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk.Web">
 
   <PropertyGroup>
-    <TargetFramework>netcoreapp2.2</TargetFramework>
+    <TargetFramework>netcoreapp3.0</TargetFramework>
     <TargetFramework Condition="'$(BenchmarksTargetFramework)' != ''">$(BenchmarksTargetFramework)</TargetFramework>
     <UseP2PReferences Condition="'$(UseP2PReferences)'=='' AND '$(BenchmarksTargetFramework)'==''">true</UseP2PReferences>
   </PropertyGroup>

+ 10 - 10
src/Routing/benchmarkapps/Benchmarks/StartupUsingEndpointRouting.cs

@@ -3,11 +3,10 @@
 
 using System.Text;
 using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Internal;
+using Microsoft.AspNetCore.Builder;
 using Microsoft.AspNetCore.Routing;
 using Microsoft.AspNetCore.Routing.Patterns;
 using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.DependencyInjection.Extensions;
 
 namespace Benchmarks
 {
@@ -18,8 +17,13 @@ namespace Benchmarks
         public void ConfigureServices(IServiceCollection services)
         {
             services.AddRouting();
+        }
 
-            var endpointDataSource = new DefaultEndpointDataSource(new[]
+        public void Configure(IApplicationBuilder app)
+        {
+            app.UseEndpointRouting(builder =>
+            {
+                var endpointDataSource = new DefaultEndpointDataSource(new[]
                 {
                     new RouteEndpoint(
                         requestDelegate: (httpContext) =>
@@ -37,14 +41,10 @@ namespace Benchmarks
                         displayName: "Plaintext"),
                 });
 
-            services.TryAddEnumerable(ServiceDescriptor.Singleton<EndpointDataSource>(endpointDataSource));
-        }
-
-        public void Configure(Microsoft.AspNetCore.Builder.IApplicationBuilder app)
-        {
-            app.UseEndpointRouting();
+                builder.DataSources.Add(endpointDataSource);
+            });
 
             app.UseEndpoint();
         }
     }
-}
+}

+ 1 - 1
src/Routing/benchmarkapps/Benchmarks/benchmarks.json

@@ -8,7 +8,7 @@
     },
     "Source": {
       "Repository": "https://github.com/aspnet/routing.git",
-      "BranchOrCommit": "release/2.2",
+      "BranchOrCommit": "master",
       "Project": "benchmarkapps/Benchmarks/Benchmarks.csproj"
     },
     "Port": 8080

+ 1 - 1
src/Routing/benchmarks/Microsoft.AspNetCore.Routing.Performance/Microsoft.AspNetCore.Routing.Performance.csproj

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>netcoreapp2.2</TargetFramework>
+    <TargetFramework>netcoreapp3.0</TargetFramework>
     <OutputType>Exe</OutputType>
     <ServerGarbageCollection>true</ServerGarbageCollection>
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>

+ 0 - 182
src/Routing/benchmarks/Microsoft.AspNetCore.Routing.Performance/RouteValueDictionaryBenchmark.cs

@@ -1,182 +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 BenchmarkDotNet.Attributes;
-
-namespace Microsoft.AspNetCore.Routing
-{
-    public class RouteValueDictionaryBenchmark
-    {
-        private RouteValueDictionary _arrayValues;
-        private RouteValueDictionary _propertyValues;
-
-        // We modify the route value dictionaries in many of these benchmarks.
-        [IterationSetup]
-        public void Setup()
-        {
-            _arrayValues = new RouteValueDictionary()
-            {
-                { "action", "Index" },
-                { "controller", "Home" },
-                { "id", "17" },
-            };
-            _propertyValues = new RouteValueDictionary(new { action = "Index", controller = "Home", id = "17" });
-        }
-
-        [Benchmark]
-        public RouteValueDictionary AddSingleItem()
-        {
-            var dictionary = new RouteValueDictionary
-            {
-                { "action", "Index" }
-            };
-            return dictionary;
-        }
-
-        [Benchmark]
-        public RouteValueDictionary AddThreeItems()
-        {
-            var dictionary = new RouteValueDictionary
-            {
-                { "action", "Index" },
-                { "controller", "Home" },
-                { "id", "15" }
-            };
-            return dictionary;
-        }
-
-        [Benchmark]
-        public RouteValueDictionary ConditionalAdd_ContainsKeyAdd()
-        {
-            var dictionary = _arrayValues;
-
-            if (!dictionary.ContainsKey("action"))
-            {
-                dictionary.Add("action", "Index");
-            }
-
-            if (!dictionary.ContainsKey("controller"))
-            {
-                dictionary.Add("controller", "Home");
-            }
-
-            if (!dictionary.ContainsKey("area"))
-            {
-                dictionary.Add("area", "Admin");
-            }
-
-            return dictionary;
-        }
-        
-        [Benchmark]
-        public RouteValueDictionary ConditionalAdd_TryAdd()
-        {
-            var dictionary = _arrayValues;
-
-            dictionary.TryAdd("action", "Index");
-            dictionary.TryAdd("controller", "Home");
-            dictionary.TryAdd("area", "Admin");
-
-            return dictionary;
-        }
-
-        [Benchmark]
-        public RouteValueDictionary ForEachThreeItems_Array()
-        {
-            var dictionary = _arrayValues;
-            foreach (var kvp in dictionary)
-            {
-                GC.KeepAlive(kvp.Value);
-            }
-            return dictionary;
-        }
-
-        [Benchmark]
-        public RouteValueDictionary ForEachThreeItems_Properties()
-        {
-            var dictionary = _propertyValues;
-            foreach (var kvp in dictionary)
-            {
-                GC.KeepAlive(kvp.Value);
-            }
-            return dictionary;
-        }
-
-        [Benchmark]
-        public RouteValueDictionary GetThreeItems_Array()
-        {
-            var dictionary = _arrayValues;
-            GC.KeepAlive(dictionary["action"]);
-            GC.KeepAlive(dictionary["controller"]);
-            GC.KeepAlive(dictionary["id"]);
-            return dictionary;
-        }
-
-        [Benchmark]
-        public RouteValueDictionary GetThreeItems_Properties()
-        {
-            var dictionary = _propertyValues;
-            GC.KeepAlive(dictionary["action"]);
-            GC.KeepAlive(dictionary["controller"]);
-            GC.KeepAlive(dictionary["id"]);
-            return dictionary;
-        }
-
-        [Benchmark]
-        public RouteValueDictionary SetSingleItem()
-        {
-            var dictionary = new RouteValueDictionary
-            {
-                ["action"] = "Index"
-            };
-            return dictionary;
-        }
-
-        [Benchmark]
-        public RouteValueDictionary SetExistingItem()
-        {
-            var dictionary = _arrayValues;
-            dictionary["action"] = "About";
-            return dictionary;
-        }
-
-        [Benchmark]
-        public RouteValueDictionary SetThreeItems()
-        {
-            var dictionary = new RouteValueDictionary
-            {
-                ["action"] = "Index",
-                ["controller"] = "Home",
-                ["id"] = "15"
-            };
-            return dictionary;
-        }
-
-        [Benchmark]
-        public RouteValueDictionary TryGetValueThreeItems_Array()
-        {
-            var dictionary = _arrayValues;
-            dictionary.TryGetValue("action", out var action);
-            dictionary.TryGetValue("controller", out var controller);
-            dictionary.TryGetValue("id", out var id);
-            GC.KeepAlive(action);
-            GC.KeepAlive(controller);
-            GC.KeepAlive(id);
-            return dictionary;
-        }
-
-        [Benchmark]
-        public RouteValueDictionary TryGetValueThreeItems_Properties()
-        {
-            var dictionary = _propertyValues;
-            dictionary.TryGetValue("action", out var action);
-            dictionary.TryGetValue("controller", out var controller);
-            dictionary.TryGetValue("id", out var id);
-            GC.KeepAlive(action);
-            GC.KeepAlive(controller);
-            GC.KeepAlive(id);
-            return dictionary;
-        }
-    }
-}

+ 29 - 33
src/Routing/build/dependencies.props

@@ -4,43 +4,39 @@
   </PropertyGroup>
   <PropertyGroup Label="Package Versions">
     <BenchmarkDotNetPackageVersion>0.10.13</BenchmarkDotNetPackageVersion>
-    <InternalAspNetCoreSdkPackageVersion>2.2.0-preview2-20181011.10</InternalAspNetCoreSdkPackageVersion>
-    <MicrosoftAspNetCoreAppPackageVersion>2.2.0-preview3-35496</MicrosoftAspNetCoreAppPackageVersion>
-    <MicrosoftAspNetCoreBenchmarkRunnerSourcesPackageVersion>2.2.0-preview3-35496</MicrosoftAspNetCoreBenchmarkRunnerSourcesPackageVersion>
-    <MicrosoftAspNetCoreHostingAbstractionsPackageVersion>2.2.0-preview3-35496</MicrosoftAspNetCoreHostingAbstractionsPackageVersion>
-    <MicrosoftAspNetCoreHostingPackageVersion>2.2.0-preview3-35496</MicrosoftAspNetCoreHostingPackageVersion>
-    <MicrosoftAspNetCoreHttpAbstractionsPackageVersion>2.2.0-preview3-35496</MicrosoftAspNetCoreHttpAbstractionsPackageVersion>
-    <MicrosoftAspNetCoreHttpExtensionsPackageVersion>2.2.0-preview3-35496</MicrosoftAspNetCoreHttpExtensionsPackageVersion>
-    <MicrosoftAspNetCoreHttpPackageVersion>2.2.0-preview3-35496</MicrosoftAspNetCoreHttpPackageVersion>
-    <MicrosoftAspNetCoreServerIISIntegrationPackageVersion>2.2.0-preview3-35496</MicrosoftAspNetCoreServerIISIntegrationPackageVersion>
-    <MicrosoftAspNetCoreServerKestrelPackageVersion>2.2.0-preview3-35496</MicrosoftAspNetCoreServerKestrelPackageVersion>
-    <MicrosoftAspNetCoreStaticFilesPackageVersion>2.2.0-preview3-35496</MicrosoftAspNetCoreStaticFilesPackageVersion>
-    <MicrosoftAspNetCoreTestHostPackageVersion>2.2.0-preview3-35496</MicrosoftAspNetCoreTestHostPackageVersion>
-    <MicrosoftAspNetCoreTestingPackageVersion>2.2.0-preview3-35496</MicrosoftAspNetCoreTestingPackageVersion>
-    <MicrosoftExtensionsCommandLineUtilsSourcesPackageVersion>2.2.0-preview3-35496</MicrosoftExtensionsCommandLineUtilsSourcesPackageVersion>
-    <MicrosoftExtensionsConfigurationCommandLinePackageVersion>2.2.0-preview3-35496</MicrosoftExtensionsConfigurationCommandLinePackageVersion>
-    <MicrosoftExtensionsConfigurationEnvironmentVariablesPackageVersion>2.2.0-preview3-35496</MicrosoftExtensionsConfigurationEnvironmentVariablesPackageVersion>
-    <MicrosoftExtensionsConfigurationPackageVersion>2.2.0-preview3-35496</MicrosoftExtensionsConfigurationPackageVersion>
-    <MicrosoftExtensionsDependencyInjectionAbstractionsPackageVersion>2.2.0-preview3-35496</MicrosoftExtensionsDependencyInjectionAbstractionsPackageVersion>
-    <MicrosoftExtensionsDependencyInjectionPackageVersion>2.2.0-preview3-35496</MicrosoftExtensionsDependencyInjectionPackageVersion>
-    <MicrosoftExtensionsHashCodeCombinerSourcesPackageVersion>2.2.0-preview3-35496</MicrosoftExtensionsHashCodeCombinerSourcesPackageVersion>
-    <MicrosoftExtensionsLoggingAbstractionsPackageVersion>2.2.0-preview3-35496</MicrosoftExtensionsLoggingAbstractionsPackageVersion>
-    <MicrosoftExtensionsLoggingConsolePackageVersion>2.2.0-preview3-35496</MicrosoftExtensionsLoggingConsolePackageVersion>
-    <MicrosoftExtensionsLoggingPackageVersion>2.2.0-preview3-35496</MicrosoftExtensionsLoggingPackageVersion>
-    <MicrosoftExtensionsLoggingTestingPackageVersion>2.2.0-preview3-35496</MicrosoftExtensionsLoggingTestingPackageVersion>
-    <MicrosoftExtensionsObjectPoolPackageVersion>2.2.0-preview3-35496</MicrosoftExtensionsObjectPoolPackageVersion>
-    <MicrosoftExtensionsOptionsPackageVersion>2.2.0-preview3-35496</MicrosoftExtensionsOptionsPackageVersion>
-    <MicrosoftExtensionsPropertyHelperSourcesPackageVersion>2.2.0-preview3-35496</MicrosoftExtensionsPropertyHelperSourcesPackageVersion>
-    <MicrosoftExtensionsWebEncodersPackageVersion>2.2.0-preview3-35496</MicrosoftExtensionsWebEncodersPackageVersion>
-    <MicrosoftNETCoreApp20PackageVersion>2.0.9</MicrosoftNETCoreApp20PackageVersion>
-    <MicrosoftNETCoreApp21PackageVersion>2.1.3</MicrosoftNETCoreApp21PackageVersion>
-    <MicrosoftNETCoreApp22PackageVersion>2.2.0-preview3-27008-03</MicrosoftNETCoreApp22PackageVersion>
+    <InternalAspNetCoreSdkPackageVersion>3.0.0-build-20181114.5</InternalAspNetCoreSdkPackageVersion>
+    <MicrosoftAspNetCoreAppPackageVersion>3.0.0-alpha1-10742</MicrosoftAspNetCoreAppPackageVersion>
+    <MicrosoftAspNetCoreBenchmarkRunnerSourcesPackageVersion>3.0.0-preview-181113-11</MicrosoftAspNetCoreBenchmarkRunnerSourcesPackageVersion>
+    <MicrosoftAspNetCoreHostingAbstractionsPackageVersion>3.0.0-alpha1-10742</MicrosoftAspNetCoreHostingAbstractionsPackageVersion>
+    <MicrosoftAspNetCoreHostingPackageVersion>3.0.0-alpha1-10742</MicrosoftAspNetCoreHostingPackageVersion>
+    <MicrosoftAspNetCoreHttpAbstractionsPackageVersion>3.0.0-alpha1-10742</MicrosoftAspNetCoreHttpAbstractionsPackageVersion>
+    <MicrosoftAspNetCoreHttpExtensionsPackageVersion>3.0.0-alpha1-10742</MicrosoftAspNetCoreHttpExtensionsPackageVersion>
+    <MicrosoftAspNetCoreHttpPackageVersion>3.0.0-alpha1-10742</MicrosoftAspNetCoreHttpPackageVersion>
+    <MicrosoftAspNetCoreServerIISIntegrationPackageVersion>3.0.0-alpha1-10742</MicrosoftAspNetCoreServerIISIntegrationPackageVersion>
+    <MicrosoftAspNetCoreServerKestrelPackageVersion>3.0.0-alpha1-10742</MicrosoftAspNetCoreServerKestrelPackageVersion>
+    <MicrosoftAspNetCoreStaticFilesPackageVersion>3.0.0-alpha1-10742</MicrosoftAspNetCoreStaticFilesPackageVersion>
+    <MicrosoftAspNetCoreTestHostPackageVersion>3.0.0-alpha1-10742</MicrosoftAspNetCoreTestHostPackageVersion>
+    <MicrosoftAspNetCoreTestingPackageVersion>3.0.0-preview-181113-11</MicrosoftAspNetCoreTestingPackageVersion>
+    <MicrosoftExtensionsCommandLineUtilsSourcesPackageVersion>3.0.0-preview-181113-11</MicrosoftExtensionsCommandLineUtilsSourcesPackageVersion>
+    <MicrosoftExtensionsConfigurationCommandLinePackageVersion>3.0.0-preview-181113-11</MicrosoftExtensionsConfigurationCommandLinePackageVersion>
+    <MicrosoftExtensionsConfigurationEnvironmentVariablesPackageVersion>3.0.0-preview-181113-11</MicrosoftExtensionsConfigurationEnvironmentVariablesPackageVersion>
+    <MicrosoftExtensionsConfigurationPackageVersion>3.0.0-preview-181113-11</MicrosoftExtensionsConfigurationPackageVersion>
+    <MicrosoftExtensionsDependencyInjectionAbstractionsPackageVersion>3.0.0-preview-181113-11</MicrosoftExtensionsDependencyInjectionAbstractionsPackageVersion>
+    <MicrosoftExtensionsDependencyInjectionPackageVersion>3.0.0-preview-181113-11</MicrosoftExtensionsDependencyInjectionPackageVersion>
+    <MicrosoftExtensionsHashCodeCombinerSourcesPackageVersion>3.0.0-preview-181113-11</MicrosoftExtensionsHashCodeCombinerSourcesPackageVersion>
+    <MicrosoftExtensionsLoggingAbstractionsPackageVersion>3.0.0-preview-181113-11</MicrosoftExtensionsLoggingAbstractionsPackageVersion>
+    <MicrosoftExtensionsLoggingConsolePackageVersion>3.0.0-preview-181113-11</MicrosoftExtensionsLoggingConsolePackageVersion>
+    <MicrosoftExtensionsLoggingPackageVersion>3.0.0-preview-181113-11</MicrosoftExtensionsLoggingPackageVersion>
+    <MicrosoftExtensionsLoggingTestingPackageVersion>3.0.0-preview-181113-11</MicrosoftExtensionsLoggingTestingPackageVersion>
+    <MicrosoftExtensionsObjectPoolPackageVersion>3.0.0-preview-181113-11</MicrosoftExtensionsObjectPoolPackageVersion>
+    <MicrosoftExtensionsOptionsPackageVersion>3.0.0-preview-181113-11</MicrosoftExtensionsOptionsPackageVersion>
+    <MicrosoftExtensionsPropertyHelperSourcesPackageVersion>3.0.0-preview-181113-11</MicrosoftExtensionsPropertyHelperSourcesPackageVersion>
+    <MicrosoftExtensionsWebEncodersPackageVersion>3.0.0-preview-181113-11</MicrosoftExtensionsWebEncodersPackageVersion>
+    <MicrosoftNETCoreAppPackageVersion>3.0.0-preview1-26907-05</MicrosoftNETCoreAppPackageVersion>
     <MicrosoftNETTestSdkPackageVersion>15.6.1</MicrosoftNETTestSdkPackageVersion>
     <MoqPackageVersion>4.10.0</MoqPackageVersion>
     <NETStandardLibrary20PackageVersion>2.0.3</NETStandardLibrary20PackageVersion>
     <NewtonsoftJsonPackageVersion>11.0.2</NewtonsoftJsonPackageVersion>
-    <SystemReflectionEmitLightweightPackageVersion>4.3.0</SystemReflectionEmitLightweightPackageVersion>
-    <SystemReflectionEmitPackageVersion>4.3.0</SystemReflectionEmitPackageVersion>
     <XunitAnalyzersPackageVersion>0.10.0</XunitAnalyzersPackageVersion>
     <XunitPackageVersion>2.3.1</XunitPackageVersion>
     <XunitRunnerVisualStudioPackageVersion>2.4.0</XunitRunnerVisualStudioPackageVersion>

+ 1 - 4
src/Routing/build/repo.props

@@ -4,14 +4,11 @@
   <PropertyGroup>
     <!-- These properties are use by the automation that updates dependencies.props -->
     <LineupPackageId>Internal.AspNetCore.Universe.Lineup</LineupPackageId>
-    <LineupPackageVersion>2.2.0-*</LineupPackageVersion>
     <LineupPackageRestoreSource>https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json</LineupPackageRestoreSource>
   </PropertyGroup>
 
   <ItemGroup>
-    <DotNetCoreRuntime Include="$(MicrosoftNETCoreApp20PackageVersion)" />
-    <DotNetCoreRuntime Include="$(MicrosoftNETCoreApp21PackageVersion)" />
-    <DotNetCoreRuntime Include="$(MicrosoftNETCoreApp22PackageVersion)" />
+    <DotNetCoreRuntime Include="$(MicrosoftNETCoreAppPackageVersion)" />
   </ItemGroup>
 
   <PropertyGroup>

+ 25 - 0
src/Routing/samples/RoutingSandbox/AuthorizationMiddleware/AuthorizationAppBuilderExtensions.cs

@@ -0,0 +1,25 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Builder;
+using RoutingSample.Web.AuthorizationMiddleware;
+
+namespace Microsoft.AspNetCore.Builder
+{
+    public static class AuthorizationAppBuilderExtensions
+    {
+        public static IApplicationBuilder UseAuthorization(this IApplicationBuilder app)
+        {
+            if (app == null)
+            {
+                throw new ArgumentNullException(nameof(app));
+            }
+
+            return app.UseMiddleware<AuthorizationMiddleware>();
+        }
+    }
+}

+ 25 - 0
src/Routing/samples/RoutingSandbox/AuthorizationMiddleware/AuthorizationMetadata.cs

@@ -0,0 +1,25 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace RoutingSample.Web.AuthorizationMiddleware
+{
+    public class AuthorizationMetadata
+    {
+        public AuthorizationMetadata(IEnumerable<string> roles)
+        {
+            if (roles == null)
+            {
+                throw new ArgumentNullException(nameof(roles));
+            }
+
+            Roles = roles.ToArray();
+        }
+
+        public IReadOnlyList<string> Roles { get; }
+    }
+}

+ 59 - 0
src/Routing/samples/RoutingSandbox/AuthorizationMiddleware/AuthorizationMiddleware.cs

@@ -0,0 +1,59 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Primitives;
+
+namespace RoutingSample.Web.AuthorizationMiddleware
+{
+    public class AuthorizationMiddleware
+    {
+        private readonly ILogger _logger;
+        private readonly RequestDelegate _next;
+
+        public AuthorizationMiddleware(ILogger<AuthorizationMiddleware> logger, RequestDelegate next)
+        {
+            if (logger == null)
+            {
+                throw new ArgumentNullException(nameof(logger));
+            }
+
+            if (next == null)
+            {
+                throw new ArgumentNullException(nameof(next));
+            }
+
+            _logger = logger;
+            _next = next;
+        }
+
+        public async Task Invoke(HttpContext httpContext)
+        {
+            var endpoint = httpContext.Features.Get<IEndpointFeature>()?.Endpoint;
+            if (endpoint != null)
+            {
+                var metadata = endpoint.Metadata.GetMetadata<AuthorizationMetadata>();
+                // Only run authorization if endpoint has metadata
+                if (metadata != null)
+                {
+                    if (!httpContext.Request.Query.TryGetValue("x-role", out var role) ||
+                        !metadata.Roles.Contains(role.ToString()))
+                    {
+                        httpContext.Response.StatusCode = 401;
+                        httpContext.Response.ContentType = "text/plain";
+                        await httpContext.Response.WriteAsync($"Unauthorized access to '{endpoint.DisplayName}'.");
+                        return;
+                    }
+                }
+            }
+
+            await _next(httpContext);
+        }
+    }
+}

+ 17 - 0
src/Routing/samples/RoutingSandbox/AuthorizationMiddleware/EndpointConventionBuilderExtensions .cs

@@ -0,0 +1,17 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using Microsoft.AspNetCore.Routing;
+using RoutingSample.Web.AuthorizationMiddleware;
+
+namespace Microsoft.AspNetCore.Builder
+{
+    public static class EndpointConventionBuilderExtensions
+    {
+        public static IEndpointConventionBuilder RequireAuthorization(this IEndpointConventionBuilder builder, params string[] roles)
+        {
+            builder.Apply(endpointBuilder => endpointBuilder.Metadata.Add(new AuthorizationMetadata(roles)));
+            return builder;
+        }
+    }
+}

+ 33 - 0
src/Routing/samples/RoutingSandbox/HelloExtension/EndpointRouteBuilderExtensions.cs

@@ -0,0 +1,33 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.AspNetCore.Routing.Matching;
+using Microsoft.AspNetCore.Routing.Patterns;
+
+namespace Microsoft.AspNetCore.Builder
+{
+    public static class EndpointRouteBuilderExtensions
+    {
+        public static IEndpointConventionBuilder MapHello(this IEndpointRouteBuilder builder, string template, string greeter)
+        {
+            if (builder == null)
+            {
+                throw new ArgumentNullException(nameof(builder));
+            }
+
+            var pipeline = builder.CreateApplicationBuilder()
+               .UseHello(greeter)
+               .Build();
+
+            return builder.Map(
+                template,
+                "Hello " + greeter,
+                pipeline);
+        }
+    }
+}

+ 25 - 0
src/Routing/samples/RoutingSandbox/HelloExtension/HelloAppBuilderExtensions.cs

@@ -0,0 +1,25 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using Microsoft.Extensions.Options;
+using RoutingSample.Web.HelloExtension;
+
+namespace Microsoft.AspNetCore.Builder
+{
+    public static class HelloAppBuilderExtensions
+    {
+        public static IApplicationBuilder UseHello(this IApplicationBuilder app, string greeter)
+        {
+            if (app == null)
+            {
+                throw new ArgumentNullException(nameof(app));
+            }
+
+            return app.UseMiddleware<HelloMiddleware>(Options.Create(new HelloOptions
+            {
+                Greeter = greeter
+            }));
+        }
+    }
+}

+ 45 - 0
src/Routing/samples/RoutingSandbox/HelloExtension/HelloMiddleware.cs

@@ -0,0 +1,45 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Options;
+
+namespace RoutingSample.Web.HelloExtension
+{
+    public class HelloMiddleware
+    {
+        private readonly RequestDelegate _next;
+        private readonly HelloOptions _helloOptions;
+        private readonly byte[] _helloPayload;
+
+        public HelloMiddleware(RequestDelegate next, IOptions<HelloOptions> helloOptions)
+        {
+            _next = next;
+            _helloOptions = helloOptions.Value;
+
+            var payload = new List<byte>();
+            payload.AddRange(Encoding.UTF8.GetBytes("Hello"));
+            if (!string.IsNullOrEmpty(_helloOptions.Greeter))
+            {
+                payload.Add((byte)' ');
+                payload.AddRange(Encoding.UTF8.GetBytes(_helloOptions.Greeter));
+            }
+            _helloPayload = payload.ToArray();
+        }
+
+        public Task InvokeAsync(HttpContext context)
+        {
+            var response = context.Response;
+            var payloadLength = _helloPayload.Length;
+            response.StatusCode = 200;
+            response.ContentType = "text/plain";
+            response.ContentLength = payloadLength;
+            return response.Body.WriteAsync(_helloPayload, 0, payloadLength);
+        }
+    }
+}

+ 10 - 0
src/Routing/samples/RoutingSandbox/HelloExtension/HelloOptions.cs

@@ -0,0 +1,10 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+namespace RoutingSample.Web.HelloExtension
+{
+    public class HelloOptions
+    {
+        public string Greeter { get; set; }
+    }
+}

+ 2 - 3
src/Routing/samples/RoutingSandbox/RoutingSandbox.csproj

@@ -1,8 +1,7 @@
-<Project Sdk="Microsoft.NET.Sdk.Web">
+<Project Sdk="Microsoft.NET.Sdk.Web">
 
   <PropertyGroup>
-    <TargetFrameworks>netcoreapp2.2</TargetFrameworks>
-    <TargetFrameworks Condition=" '$(OS)' == 'Windows_NT' ">$(TargetFrameworks);net461</TargetFrameworks>
+    <TargetFramework>netcoreapp3.0</TargetFramework>
   </PropertyGroup>
 
   <ItemGroup>

+ 56 - 51
src/Routing/samples/RoutingSandbox/UseEndpointRoutingStartup.cs

@@ -3,11 +3,11 @@
 
 using System;
 using System.IO;
+using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
 using Microsoft.AspNetCore.Builder;
 using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Internal;
 using Microsoft.AspNetCore.Routing;
 using Microsoft.AspNetCore.Routing.Internal;
 using Microsoft.AspNetCore.Routing.Patterns;
@@ -19,65 +19,70 @@ namespace RoutingSandbox
     public class UseEndpointRoutingStartup
     {
         private static readonly byte[] _homePayload = Encoding.UTF8.GetBytes("Endpoint Routing sample endpoints:" + Environment.NewLine + "/plaintext");
-        private static readonly byte[] _helloWorldPayload = Encoding.UTF8.GetBytes("Hello, World!");
+        private static readonly byte[] _plainTextPayload = Encoding.UTF8.GetBytes("Plain text!");
 
         public void ConfigureServices(IServiceCollection services)
         {
-            var endpointDataSource = new DefaultEndpointDataSource(new[]
-                {
-                    new RouteEndpoint((httpContext) =>
-                        {
-                            var response = httpContext.Response;
-                            var payloadLength = _homePayload.Length;
-                            response.StatusCode = 200;
-                            response.ContentType = "text/plain";
-                            response.ContentLength = payloadLength;
-                            return response.Body.WriteAsync(_homePayload, 0, payloadLength);
-                        },
-                        RoutePatternFactory.Parse("/"),
-                        0,
-                        EndpointMetadataCollection.Empty,
-                        "Home"),
-                    new RouteEndpoint((httpContext) =>
-                        {
-                            var response = httpContext.Response;
-                            var payloadLength = _helloWorldPayload.Length;
-                            response.StatusCode = 200;
-                            response.ContentType = "text/plain";
-                            response.ContentLength = payloadLength;
-                            return response.Body.WriteAsync(_helloWorldPayload, 0, payloadLength);
-                        },
-                         RoutePatternFactory.Parse("/plaintext"),
-                        0,
-                        EndpointMetadataCollection.Empty,
-                        "Plaintext"),
-                    new RouteEndpoint((httpContext) =>
-                        {
-                            using (var writer = new StreamWriter(httpContext.Response.Body, Encoding.UTF8, 1024, leaveOpen: true))
-                            {
-                                var graphWriter = httpContext.RequestServices.GetRequiredService<DfaGraphWriter>();
-                                var dataSource = httpContext.RequestServices.GetRequiredService<CompositeEndpointDataSource>();
-                                graphWriter.Write(dataSource, writer);
-                            }
-
-                            return Task.CompletedTask;
-                        },
-                        RoutePatternFactory.Parse("/graph"),
-                        0,
-                        new EndpointMetadataCollection(new HttpMethodMetadata(new[]{ "GET", })),
-                        "DFA Graph"),
-                });
-
-            services.TryAddEnumerable(ServiceDescriptor.Singleton<EndpointDataSource>(endpointDataSource));
+            services.AddRouting();
         }
 
         public void Configure(IApplicationBuilder app)
         {
-            app.UseEndpointRouting();
+            app.UseEndpointRouting(builder =>
+            {
+                builder.MapHello("/helloworld", "World");
 
-            app.UseStaticFiles();
+                builder.MapHello("/helloworld-secret", "Secret World")
+                    .RequireAuthorization("swordfish");
+
+                builder.MapGet(
+                    "/",
+                    (httpContext) =>
+                    {
+                        var dataSource = httpContext.RequestServices.GetRequiredService<EndpointDataSource>();
 
-            // Imagine some more stuff here...
+                        var sb = new StringBuilder();
+                        sb.AppendLine("Endpoints:");
+                        foreach (var endpoint in dataSource.Endpoints.OfType<RouteEndpoint>().OrderBy(e => e.RoutePattern.RawText, StringComparer.OrdinalIgnoreCase))
+                        {
+                            sb.AppendLine($"- {endpoint.RoutePattern.RawText}");
+                        }
+
+                        var response = httpContext.Response;
+                        response.StatusCode = 200;
+                        response.ContentType = "text/plain";
+                        return response.WriteAsync(sb.ToString());
+                    });
+                builder.MapGet(
+                    "/plaintext",
+                    (httpContext) =>
+                    {
+                        var response = httpContext.Response;
+                        var payloadLength = _plainTextPayload.Length;
+                        response.StatusCode = 200;
+                        response.ContentType = "text/plain";
+                        response.ContentLength = payloadLength;
+                        return response.Body.WriteAsync(_plainTextPayload, 0, payloadLength);
+                    });
+                builder.MapGet(
+                    "/graph",
+                    "DFA Graph",
+                    (httpContext) =>
+                    {
+                        using (var writer = new StreamWriter(httpContext.Response.Body, Encoding.UTF8, 1024, leaveOpen: true))
+                        {
+                            var graphWriter = httpContext.RequestServices.GetRequiredService<DfaGraphWriter>();
+                            var dataSource = httpContext.RequestServices.GetRequiredService<CompositeEndpointDataSource>();
+                            graphWriter.Write(dataSource, writer);
+                        }
+
+                        return Task.CompletedTask;
+                    });
+            });
+
+            app.UseStaticFiles();
+			
+			app.UseAuthorization();
 
             app.UseEndpoint();
         }

+ 0 - 49
src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/Endpoint.cs

@@ -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.
-
-namespace Microsoft.AspNetCore.Http
-{
-    /// <summary>
-    /// Respresents a logical endpoint in an application.
-    /// </summary>
-    public class Endpoint
-    {
-        /// <summary>
-        /// Creates a new instance of <see cref="Endpoint"/>.
-        /// </summary>
-        /// <param name="requestDelegate">The delegate used to process requests for the endpoint.</param>
-        /// <param name="metadata">
-        /// The endpoint <see cref="EndpointMetadataCollection"/>. May be null.
-        /// </param>
-        /// <param name="displayName">
-        /// The informational display name of the endpoint. May be null.
-        /// </param>
-        public Endpoint(
-            RequestDelegate requestDelegate,
-            EndpointMetadataCollection metadata,
-            string displayName)
-        {
-            // All are allowed to be null
-            RequestDelegate = requestDelegate;
-            Metadata = metadata ?? EndpointMetadataCollection.Empty;
-            DisplayName = displayName;
-        }
-
-        /// <summary>
-        /// Gets the informational display name of this endpoint.
-        /// </summary>
-        public string DisplayName { get; }
-
-        /// <summary>
-        /// Gets the collection of metadata associated with this endpoint.
-        /// </summary>
-        public EndpointMetadataCollection Metadata { get; }
-
-        /// <summary>
-        /// Gets the delegate used to process requests for the endpoint.
-        /// </summary>
-        public RequestDelegate RequestDelegate { get; }
-
-        public override string ToString() => DisplayName ?? base.ToString();
-    }
-}

+ 0 - 201
src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/EndpointMetadataCollection.cs

@@ -1,201 +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.Linq;
-using System.Runtime.CompilerServices;
-
-namespace Microsoft.AspNetCore.Http
-{
-    /// <summary>
-    /// A collection of arbitrary metadata associated with an endpoint.
-    /// </summary>
-    /// <remarks>
-    /// <see cref="EndpointMetadataCollection"/> instances contain a list of metadata items
-    /// of arbitrary types. The metadata items are stored as an ordered collection with
-    /// items arranged in ascending order of precedence.
-    /// </remarks>
-    public sealed class EndpointMetadataCollection : IReadOnlyList<object>
-    {
-        /// <summary>
-        /// An empty <see cref="EndpointMetadataCollection"/>.
-        /// </summary>
-        public static readonly EndpointMetadataCollection Empty = new EndpointMetadataCollection(Array.Empty<object>());
-
-        private readonly object[] _items;
-        private readonly ConcurrentDictionary<Type, object[]> _cache;
-
-        /// <summary>
-        /// Creates a new instance of <see cref="EndpointMetadataCollection"/>.
-        /// </summary>
-        /// <param name="items">The metadata items.</param>
-        public EndpointMetadataCollection(IEnumerable<object> items)
-        {
-            if (items == null)
-            {
-                throw new ArgumentNullException(nameof(items));
-            }
-
-            _items = items.ToArray();
-            _cache = new ConcurrentDictionary<Type, object[]>();
-        }
-
-        /// <summary>
-        /// Creates a new instance of <see cref="EndpointMetadataCollection"/>.
-        /// </summary>
-        /// <param name="items">The metadata items.</param>
-        public EndpointMetadataCollection(params object[] items)
-            : this((IEnumerable<object>)items)
-        {
-        }
-
-        /// <summary>
-        /// Gets the item at <paramref name="index"/>.
-        /// </summary>
-        /// <param name="index">The index of the item to retrieve.</param>
-        /// <returns>The item at <paramref name="index"/>.</returns>
-        public object this[int index] => _items[index];
-
-        /// <summary>
-        /// Gets the count of metadata items.
-        /// </summary>
-        public int Count => _items.Length;
-
-        /// <summary>
-        /// Gets the most significant metadata item of type <typeparamref name="T"/>.
-        /// </summary>
-        /// <typeparam name="T">The type of metadata to retrieve.</typeparam>
-        /// <returns>
-        /// The most significant metadata of type <typeparamref name="T"/> or <c>null</c>.
-        /// </returns>
-        [MethodImpl(MethodImplOptions.AggressiveInlining)]
-        public T GetMetadata<T>() where T : class
-        {
-            if (_cache.TryGetValue(typeof(T), out var result))
-            {
-                var length = result.Length;
-                return length > 0 ? (T)result[length - 1] : default;
-            }
-
-            return GetMetadataSlow<T>();
-        }
-
-        private T GetMetadataSlow<T>() where T : class
-        {
-            var array = GetOrderedMetadataSlow<T>();
-            var length = array.Length;
-            return length > 0 ? array[length - 1] : default;
-        }
-
-        /// <summary>
-        /// Gets the metadata items of type <typeparamref name="T"/> in ascending
-        /// order of precedence.
-        /// </summary>
-        /// <typeparam name="T">The type of metadata.</typeparam>
-        /// <returns>A sequence of metadata items of <typeparamref name="T"/>.</returns>
-        [MethodImpl(MethodImplOptions.AggressiveInlining)]
-        public IEnumerable<T> GetOrderedMetadata<T>() where T : class
-        {
-            if (_cache.TryGetValue(typeof(T), out var result))
-            {
-                return (T[])result;
-            }
-
-            return GetOrderedMetadataSlow<T>();
-        }
-
-        private T[] GetOrderedMetadataSlow<T>() where T : class
-        {
-            var items = new List<T>();
-            for (var i = 0; i < _items.Length; i++)
-            {
-                if (_items[i] is T item)
-                {
-                    items.Add(item);
-                }
-            }
-
-            var array = items.ToArray();
-            _cache.TryAdd(typeof(T), array);
-            return array;
-        }
-
-        /// <summary>
-        /// Gets an <see cref="IEnumerator"/> of all metadata items.
-        /// </summary>
-        /// <returns>An <see cref="IEnumerator"/> of all metadata items.</returns>
-        public Enumerator GetEnumerator() => new Enumerator(this);
-
-        /// <summary>
-        /// Gets an <see cref="IEnumerator{Object}"/> of all metadata items.
-        /// </summary>
-        /// <returns>An <see cref="IEnumerator{Object}"/> of all metadata items.</returns>
-        IEnumerator<object> IEnumerable<object>.GetEnumerator() => GetEnumerator();
-
-        /// <summary>
-        /// Gets an <see cref="IEnumerator"/> of all metadata items.
-        /// </summary>
-        /// <returns>An <see cref="IEnumerator"/> of all metadata items.</returns>
-        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
-
-        /// <summary>
-        /// Enumerates the elements of an <see cref="EndpointMetadataCollection"/>.
-        /// </summary>
-        public struct Enumerator : IEnumerator<object>
-        {
-            // Intentionally not readonly to prevent defensive struct copies
-            private object[] _items;
-            private int _index;
-
-            internal Enumerator(EndpointMetadataCollection collection)
-            {
-                _items = collection._items;
-                _index = 0;
-                Current = null;
-            }
-
-            /// <summary>
-            /// Gets the element at the current position of the enumerator
-            /// </summary>
-            public object Current { get; private set; }
-
-            /// <summary>
-            /// Releases all resources used by the <see cref="Enumerator"/>.
-            /// </summary>
-            public void Dispose()
-            {
-            }
-
-            /// <summary>
-            /// Advances the enumerator to the next element of the <see cref="Enumerator"/>.
-            /// </summary>
-            /// <returns>
-            /// <c>true</c> if the enumerator was successfully advanced to the next element;
-            /// <c>false</c> if the enumerator has passed the end of the collection.
-            /// </returns>
-            public bool MoveNext()
-            {
-                if (_index < _items.Length)
-                {
-                    Current = _items[_index++];
-                    return true;
-                }
-
-                Current = null;
-                return false;
-            }
-
-            /// <summary>
-            /// Sets the enumerator to its initial position, which is before the first element in the collection.
-            /// </summary>
-            public void Reset()
-            {
-                _index = 0;
-                Current = null;
-            }
-        }
-    }
-}

+ 0 - 18
src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/IEndpointFeature.cs

@@ -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.Http.Features
-{
-    /// <summary>
-    /// A feature interface for endpoint routing. Use <see cref="HttpContext.Features"/>
-    /// to access an instance associated with the current request.
-    /// </summary>
-    public interface IEndpointFeature
-    {
-        /// <summary>
-        /// Gets or sets the selected <see cref="Http.Endpoint"/> for the current
-        /// request.
-        /// </summary>
-        Endpoint Endpoint { get; set; }
-    }
-}

+ 0 - 16
src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/IRouteValuesFeature.cs

@@ -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.Routing;
-
-namespace Microsoft.AspNetCore.Http.Features
-{
-    public interface IRouteValuesFeature
-    {
-        /// <summary>
-        /// Gets or sets the <see cref="RouteValueDictionary"/> associated with the currrent
-        /// request.
-        /// </summary>
-        RouteValueDictionary RouteValues { get; set; }
-    }
-}

+ 1 - 1
src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/Microsoft.AspNetCore.Routing.Abstractions.csproj

@@ -5,7 +5,7 @@
 Commonly used types:
 Microsoft.AspNetCore.Routing.IRouter
 Microsoft.AspNetCore.Routing.RouteData</Description>
-    <TargetFramework>netstandard2.0</TargetFramework>
+    <TargetFramework>netcoreapp3.0</TargetFramework>
     <NoWarn>$(NoWarn);CS1591</NoWarn>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <PackageTags>aspnetcore;routing</PackageTags>

+ 9 - 0
src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/Properties/AssemblyInfo.cs

@@ -2,6 +2,15 @@
 // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 
 using System.Runtime.CompilerServices;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Features;
+
+[assembly: TypeForwardedTo(typeof(IEndpointFeature))]
+[assembly: TypeForwardedTo(typeof(IRouteValuesFeature))]
+[assembly: TypeForwardedTo(typeof(Endpoint))]
+[assembly: TypeForwardedTo(typeof(EndpointMetadataCollection))]
+[assembly: TypeForwardedTo(typeof(RouteValueDictionary))]
 
 [assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
 [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")]

+ 0 - 58
src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/Properties/Resources.Designer.cs

@@ -1,58 +0,0 @@
-// <auto-generated />
-namespace Microsoft.AspNetCore.Routing.Abstractions
-{
-    using System.Globalization;
-    using System.Reflection;
-    using System.Resources;
-
-    internal static class Resources
-    {
-        private static readonly ResourceManager _resourceManager
-            = new ResourceManager("Microsoft.AspNetCore.Routing.Abstractions.Resources", typeof(Resources).GetTypeInfo().Assembly);
-
-        /// <summary>
-        /// An element with the key '{0}' already exists in the {1}.
-        /// </summary>
-        internal static string RouteValueDictionary_DuplicateKey
-        {
-            get => GetString("RouteValueDictionary_DuplicateKey");
-        }
-
-        /// <summary>
-        /// An element with the key '{0}' already exists in the {1}.
-        /// </summary>
-        internal static string FormatRouteValueDictionary_DuplicateKey(object p0, object p1)
-            => string.Format(CultureInfo.CurrentCulture, GetString("RouteValueDictionary_DuplicateKey"), p0, p1);
-
-        /// <summary>
-        /// The type '{0}' defines properties '{1}' and '{2}' which differ only by casing. This is not supported by {3} which uses case-insensitive comparisons.
-        /// </summary>
-        internal static string RouteValueDictionary_DuplicatePropertyName
-        {
-            get => GetString("RouteValueDictionary_DuplicatePropertyName");
-        }
-
-        /// <summary>
-        /// The type '{0}' defines properties '{1}' and '{2}' which differ only by casing. This is not supported by {3} which uses case-insensitive comparisons.
-        /// </summary>
-        internal static string FormatRouteValueDictionary_DuplicatePropertyName(object p0, object p1, object p2, object p3)
-            => string.Format(CultureInfo.CurrentCulture, GetString("RouteValueDictionary_DuplicatePropertyName"), p0, p1, p2, p3);
-
-        private static string GetString(string name, params string[] formatterNames)
-        {
-            var value = _resourceManager.GetString(name);
-
-            System.Diagnostics.Debug.Assert(value != null);
-
-            if (formatterNames != null)
-            {
-                for (var i = 0; i < formatterNames.Length; i++)
-                {
-                    value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}");
-                }
-            }
-
-            return value;
-        }
-    }
-}

+ 0 - 126
src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/Resources.resx

@@ -1,126 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<root>
-  <!-- 
-    Microsoft ResX Schema 
-    
-    Version 2.0
-    
-    The primary goals of this format is to allow a simple XML format 
-    that is mostly human readable. The generation and parsing of the 
-    various data types are done through the TypeConverter classes 
-    associated with the data types.
-    
-    Example:
-    
-    ... ado.net/XML headers & schema ...
-    <resheader name="resmimetype">text/microsoft-resx</resheader>
-    <resheader name="version">2.0</resheader>
-    <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
-    <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
-    <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
-    <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
-    <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
-        <value>[base64 mime encoded serialized .NET Framework object]</value>
-    </data>
-    <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
-        <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
-        <comment>This is a comment</comment>
-    </data>
-                
-    There are any number of "resheader" rows that contain simple 
-    name/value pairs.
-    
-    Each data row contains a name, and value. The row also contains a 
-    type or mimetype. Type corresponds to a .NET class that support 
-    text/value conversion through the TypeConverter architecture. 
-    Classes that don't support this are serialized and stored with the 
-    mimetype set.
-    
-    The mimetype is used for serialized objects, and tells the 
-    ResXResourceReader how to depersist the object. This is currently not 
-    extensible. For a given mimetype the value must be set accordingly:
-    
-    Note - application/x-microsoft.net.object.binary.base64 is the format 
-    that the ResXResourceWriter will generate, however the reader can 
-    read any of the formats listed below.
-    
-    mimetype: application/x-microsoft.net.object.binary.base64
-    value   : The object must be serialized with 
-            : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
-            : and then encoded with base64 encoding.
-    
-    mimetype: application/x-microsoft.net.object.soap.base64
-    value   : The object must be serialized with 
-            : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
-            : and then encoded with base64 encoding.
-
-    mimetype: application/x-microsoft.net.object.bytearray.base64
-    value   : The object must be serialized into a byte array 
-            : using a System.ComponentModel.TypeConverter
-            : and then encoded with base64 encoding.
-    -->
-  <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
-    <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
-    <xsd:element name="root" msdata:IsDataSet="true">
-      <xsd:complexType>
-        <xsd:choice maxOccurs="unbounded">
-          <xsd:element name="metadata">
-            <xsd:complexType>
-              <xsd:sequence>
-                <xsd:element name="value" type="xsd:string" minOccurs="0" />
-              </xsd:sequence>
-              <xsd:attribute name="name" use="required" type="xsd:string" />
-              <xsd:attribute name="type" type="xsd:string" />
-              <xsd:attribute name="mimetype" type="xsd:string" />
-              <xsd:attribute ref="xml:space" />
-            </xsd:complexType>
-          </xsd:element>
-          <xsd:element name="assembly">
-            <xsd:complexType>
-              <xsd:attribute name="alias" type="xsd:string" />
-              <xsd:attribute name="name" type="xsd:string" />
-            </xsd:complexType>
-          </xsd:element>
-          <xsd:element name="data">
-            <xsd:complexType>
-              <xsd:sequence>
-                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
-                <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
-              </xsd:sequence>
-              <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
-              <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
-              <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
-              <xsd:attribute ref="xml:space" />
-            </xsd:complexType>
-          </xsd:element>
-          <xsd:element name="resheader">
-            <xsd:complexType>
-              <xsd:sequence>
-                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
-              </xsd:sequence>
-              <xsd:attribute name="name" type="xsd:string" use="required" />
-            </xsd:complexType>
-          </xsd:element>
-        </xsd:choice>
-      </xsd:complexType>
-    </xsd:element>
-  </xsd:schema>
-  <resheader name="resmimetype">
-    <value>text/microsoft-resx</value>
-  </resheader>
-  <resheader name="version">
-    <value>2.0</value>
-  </resheader>
-  <resheader name="reader">
-    <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
-  </resheader>
-  <resheader name="writer">
-    <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
-  </resheader>
-  <data name="RouteValueDictionary_DuplicateKey" xml:space="preserve">
-    <value>An element with the key '{0}' already exists in the {1}.</value>
-  </data>
-  <data name="RouteValueDictionary_DuplicatePropertyName" xml:space="preserve">
-    <value>The type '{0}' defines properties '{1}' and '{2}' which differ only by casing. This is not supported by {3} which uses case-insensitive comparisons.</value>
-  </data>
-</root>

+ 0 - 718
src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/RouteValueDictionary.cs

@@ -1,718 +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 System.Runtime.CompilerServices;
-using Microsoft.AspNetCore.Routing.Abstractions;
-using Microsoft.Extensions.Internal;
-
-namespace Microsoft.AspNetCore.Routing
-{
-    /// <summary>
-    /// An <see cref="IDictionary{String, Object}"/> type for route values.
-    /// </summary>
-    public class RouteValueDictionary : IDictionary<string, object>, IReadOnlyDictionary<string, object>
-    {
-        // 4 is a good default capacity here because that leaves enough space for area/controller/action/id
-        private const int DefaultCapacity = 4;
-
-        internal KeyValuePair<string, object>[] _arrayStorage;
-        internal PropertyStorage _propertyStorage;
-        private int _count;
-
-        /// <summary>
-        /// Creates a new instance of <see cref="RouteValueDictionary"/> from the provided array.
-        /// The new instance will take ownership of the array, and may mutate it.
-        /// </summary>
-        /// <param name="items">The items array.</param>
-        /// <returns>A new <see cref="RouteValueDictionary"/>.</returns>
-        public static RouteValueDictionary FromArray(KeyValuePair<string, object>[] items)
-        {
-            if (items == null)
-            {
-                throw new ArgumentNullException(nameof(items));
-            }
-
-            // We need to compress the array by removing non-contiguous items. We
-            // typically have a very small number of items to process. We don't need
-            // to preserve order.
-            var start = 0;
-            var end = items.Length - 1;
-
-            // We walk forwards from the beginning of the array and fill in 'null' slots.
-            // We walk backwards from the end of the array end move items in non-null' slots
-            // into whatever start is pointing to. O(n)
-            while (start <= end)
-            {
-                if (items[start].Key != null)
-                {
-                    start++;
-                }
-                else if (items[end].Key != null)
-                {
-                    // Swap this item into start and advance
-                    items[start] = items[end];
-                    items[end] = default;
-                    start++;
-                    end--;
-                }
-                else
-                {
-                    // Both null, we need to hold on 'start' since we
-                    // still need to fill it with something.
-                    end--;
-                }
-            }
-
-            return new RouteValueDictionary()
-            {
-                _arrayStorage = items,
-                _count = start,
-            };
-        }
-
-        /// <summary>
-        /// Creates an empty <see cref="RouteValueDictionary"/>.
-        /// </summary>
-        public RouteValueDictionary()
-        {
-            _arrayStorage = Array.Empty<KeyValuePair<string, object>>();
-        }
-
-        /// <summary>
-        /// Creates a <see cref="RouteValueDictionary"/> initialized with the specified <paramref name="values"/>.
-        /// </summary>
-        /// <param name="values">An object to initialize the dictionary. The value can be of type
-        /// <see cref="IDictionary{TKey, TValue}"/> or <see cref="IReadOnlyDictionary{TKey, TValue}"/>
-        /// or an object with public properties as key-value pairs.
-        /// </param>
-        /// <remarks>
-        /// If the value is a dictionary or other <see cref="IEnumerable{T}"/> of <see cref="KeyValuePair{String, Object}"/>,
-        /// 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.
-        /// </remarks>
-        public RouteValueDictionary(object values)
-            : this()
-        {
-            if (values is RouteValueDictionary dictionary)
-            {
-                if (dictionary._propertyStorage != null)
-                {
-                    // PropertyStorage is immutable so we can just copy it.
-                    _propertyStorage = dictionary._propertyStorage;
-                    _count = dictionary._count;
-                    return;
-                }
-
-                var other = dictionary._arrayStorage;
-                var storage = new KeyValuePair<string, object>[other.Length];
-                if (dictionary._count != 0)
-                {
-                    Array.Copy(other, 0, storage, 0, dictionary._count);
-                }
-
-                _arrayStorage = storage;
-                _count = dictionary._count;
-                return;
-            }
-
-            if (values is IEnumerable<KeyValuePair<string, object>> keyValueEnumerable)
-            {
-                foreach (var kvp in keyValueEnumerable)
-                {
-                    Add(kvp.Key, kvp.Value);
-                }
-
-                return;
-            }
-
-            if (values is IEnumerable<KeyValuePair<string, string>> stringValueEnumerable)
-            {
-                foreach (var kvp in stringValueEnumerable)
-                {
-                    Add(kvp.Key, kvp.Value);
-                }
-
-                return;
-            }
-
-            if (values != null)
-            {
-                var storage = new PropertyStorage(values);
-                _propertyStorage = storage;
-                _count = storage.Properties.Length;
-                return;
-            }
-        }
-
-        /// <inheritdoc />
-        public object this[string key]
-        {
-            get
-            {
-                if (key == null)
-                {
-                    ThrowArgumentNullExceptionForKey();
-                }
-
-                object value;
-                TryGetValue(key, out value);
-                return value;
-            }
-
-            set
-            {
-                if (key == null)
-                {
-                    ThrowArgumentNullExceptionForKey();
-                }
-
-                // We're calling this here for the side-effect of converting from properties
-                // to array. We need to create the array even if we just set an existing value since
-                // property storage is immutable. 
-                EnsureCapacity(_count);
-
-                var index = FindIndex(key);
-                if (index < 0)
-                {
-                    EnsureCapacity(_count + 1);
-                    _arrayStorage[_count++] = new KeyValuePair<string, object>(key, value);
-                }
-                else
-                {
-                    _arrayStorage[index] = new KeyValuePair<string, object>(key, value);
-                }
-            }
-        }
-
-        /// <summary>
-        /// Gets the comparer for this dictionary.
-        /// </summary>
-        /// <remarks>
-        /// This will always be a reference to <see cref="StringComparer.OrdinalIgnoreCase"/>
-        /// </remarks>
-        public IEqualityComparer<string> Comparer => StringComparer.OrdinalIgnoreCase;
-
-        /// <inheritdoc />
-        public int Count => _count;
-
-        /// <inheritdoc />
-        bool ICollection<KeyValuePair<string, object>>.IsReadOnly => false;
-
-        /// <inheritdoc />
-        public ICollection<string> Keys
-        {
-            get
-            {
-                EnsureCapacity(_count);
-
-                var array = _arrayStorage;
-                var keys = new string[_count];
-                for (var i = 0; i < keys.Length; i++)
-                {
-                    keys[i] = array[i].Key;
-                }
-
-                return keys;
-            }
-        }
-
-        IEnumerable<string> IReadOnlyDictionary<string, object>.Keys => Keys;
-
-        /// <inheritdoc />
-        public ICollection<object> Values
-        {
-            get
-            {
-                EnsureCapacity(_count);
-
-                var array = _arrayStorage;
-                var values = new object[_count];
-                for (var i = 0; i < values.Length; i++)
-                {
-                    values[i] = array[i].Value;
-                }
-
-                return values;
-            }
-        }
-
-        IEnumerable<object> IReadOnlyDictionary<string, object>.Values => Values;
-
-        /// <inheritdoc />
-        void ICollection<KeyValuePair<string, object>>.Add(KeyValuePair<string, object> item)
-        {
-            Add(item.Key, item.Value);
-        }
-
-        /// <inheritdoc />
-        public void Add(string key, object value)
-        {
-            if (key == null)
-            {
-                ThrowArgumentNullExceptionForKey();
-            }
-
-            EnsureCapacity(_count + 1);
-
-            var index = FindIndex(key);
-            if (index >= 0)
-            {
-                var message = Resources.FormatRouteValueDictionary_DuplicateKey(key, nameof(RouteValueDictionary));
-                throw new ArgumentException(message, nameof(key));
-            }
-
-            _arrayStorage[_count] = new KeyValuePair<string, object>(key, value);
-            _count++;
-        }
-
-        /// <inheritdoc />
-        public void Clear()
-        {
-            if (_count == 0)
-            {
-                return;
-            }
-
-            if (_propertyStorage != null)
-            {
-                _arrayStorage = Array.Empty<KeyValuePair<string, object>>();
-                _propertyStorage = null;
-                _count = 0;
-                return;
-            }
-
-            Array.Clear(_arrayStorage, 0, _count);
-            _count = 0;
-        }
-
-        /// <inheritdoc />
-        bool ICollection<KeyValuePair<string, object>>.Contains(KeyValuePair<string, object> item)
-        {
-            return TryGetValue(item.Key, out var value) && EqualityComparer<object>.Default.Equals(value, item.Value);
-        }
-
-        /// <inheritdoc />
-        public bool ContainsKey(string key)
-        {
-            if (key == null)
-            {
-                ThrowArgumentNullExceptionForKey();
-            }
-
-            return TryGetValue(key, out var _);
-        }
-
-        /// <inheritdoc />
-        void ICollection<KeyValuePair<string, object>>.CopyTo(
-            KeyValuePair<string, object>[] 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 (Count == 0)
-            {
-                return;
-            }
-
-            EnsureCapacity(Count);
-
-            var storage = _arrayStorage;
-            Array.Copy(storage, 0, array, arrayIndex, _count);
-        }
-
-        /// <inheritdoc />
-        public Enumerator GetEnumerator()
-        {
-            return new Enumerator(this);
-        }
-
-        /// <inheritdoc />
-        IEnumerator<KeyValuePair<string, object>> IEnumerable<KeyValuePair<string, object>>.GetEnumerator()
-        {
-            return GetEnumerator();
-        }
-
-        /// <inheritdoc />
-        IEnumerator IEnumerable.GetEnumerator()
-        {
-            return GetEnumerator();
-        }
-
-        /// <inheritdoc />
-        bool ICollection<KeyValuePair<string, object>>.Remove(KeyValuePair<string, object> item)
-        {
-            if (Count == 0)
-            {
-                return false;
-            }
-
-            EnsureCapacity(Count);
-
-            var index = FindIndex(item.Key);
-            var array = _arrayStorage;
-            if (index >= 0 && EqualityComparer<object>.Default.Equals(array[index].Value, item.Value))
-            {
-                Array.Copy(array, index + 1, array, index, _count - index);
-                _count--;
-                array[_count] = default;
-                return true;
-            }
-
-            return false;
-        }
-
-        /// <inheritdoc />
-        public bool Remove(string key)
-        {
-            if (key == null)
-            {
-                ThrowArgumentNullExceptionForKey();
-            }
-
-            if (Count == 0)
-            {
-                return false;
-            }
-
-            // Ensure property storage is converted to array storage as we'll be
-            // applying the lookup and removal on the array
-            EnsureCapacity(_count);
-
-            var index = FindIndex(key);
-            if (index >= 0)
-            {
-                _count--;
-                var array = _arrayStorage;
-                Array.Copy(array, index + 1, array, index, _count - index);
-                array[_count] = default;
-
-                return true;
-            }
-
-            return false;
-        }
-
-        /// <summary>
-        /// Attempts to remove and return the value that has the specified key from the <see cref="RouteValueDictionary"/>.
-        /// </summary>
-        /// <param name="key">The key of the element to remove and return.</param>
-        /// <param name="value">When this method returns, contains the object removed from the <see cref="RouteValueDictionary"/>, or <c>null</c> if key does not exist.</param>
-        /// <returns>
-        /// <c>true</c> if the object was removed successfully; otherwise, <c>false</c>.
-        /// </returns>
-        public bool Remove(string key, out object value)
-        {
-            if (key == null)
-            {
-                ThrowArgumentNullExceptionForKey();
-            }
-
-            if (_count == 0)
-            {
-                value = default;
-                return false;
-            }
-
-            // Ensure property storage is converted to array storage as we'll be
-            // applying the lookup and removal on the array
-            EnsureCapacity(_count);
-
-            var index = FindIndex(key);
-            if (index >= 0)
-            {
-                _count--;
-                var array = _arrayStorage;
-                value = array[index].Value;
-                Array.Copy(array, index + 1, array, index, _count - index);
-                array[_count] = default;
-
-                return true;
-            }
-
-            value = default;
-            return false;
-        }
-
-
-        /// <summary>
-        /// Attempts to the add the provided <paramref name="key"/> and <paramref name="value"/> to the dictionary.
-        /// </summary>
-        /// <param name="key">The key.</param>
-        /// <param name="value">The value.</param>
-        /// <returns>Returns <c>true</c> if the value was added. Returns <c>false</c> if the key was already present.</returns>
-        public bool TryAdd(string key, object value)
-        {
-            if (key == null)
-            {
-                ThrowArgumentNullExceptionForKey();
-            }
-
-            // Since this is an attempt to write to the dictionary, just make it an array if it isn't. If the code
-            // path we're on event tries to write to the dictionary, it will likely get 'upgraded' at some point,
-            // so we do it here to keep the code size and complexity down.
-            EnsureCapacity(Count);
-
-            var index = FindIndex(key);
-            if (index >= 0)
-            {
-                return false;
-            }
-
-            EnsureCapacity(Count + 1);
-            _arrayStorage[Count] = new KeyValuePair<string, object>(key, value);
-            _count++;
-            return true;
-        }
-
-        /// <inheritdoc />
-        public bool TryGetValue(string key, out object value)
-        {
-            if (key == null)
-            {
-                ThrowArgumentNullExceptionForKey();
-            }
-
-            if (_propertyStorage == null)
-            {
-                return TryFindItem(key, out value);
-            }
-
-            return TryGetValueSlow(key, out value);
-        }
-
-        private bool TryGetValueSlow(string key, out object value)
-        {
-            if (_propertyStorage != null)
-            {
-                var storage = _propertyStorage;
-                for (var i = 0; i < storage.Properties.Length; i++)
-                {
-                    if (string.Equals(storage.Properties[i].Name, key, StringComparison.OrdinalIgnoreCase))
-                    {
-                        value = storage.Properties[i].GetValue(storage.Value);
-                        return true;
-                    }
-                }
-            }
-
-            value = default;
-            return false;
-        }
-
-        private static void ThrowArgumentNullExceptionForKey()
-        {
-            throw new ArgumentNullException("key");
-        }
-
-        [MethodImpl(MethodImplOptions.AggressiveInlining)]
-        private void EnsureCapacity(int capacity)
-        {
-            if (_propertyStorage != null || _arrayStorage.Length < capacity)
-            {
-                EnsureCapacitySlow(capacity);
-            }
-        }
-
-        private void EnsureCapacitySlow(int capacity)
-        {
-            if (_propertyStorage != null)
-            {
-                var storage = _propertyStorage;
-                
-                // If we're converting from properties, it's likely due to an 'add' to make sure we have at least
-                // the default amount of space.
-                capacity = Math.Max(DefaultCapacity, Math.Max(storage.Properties.Length, capacity));
-                var array = new KeyValuePair<string, object>[capacity];
-
-                for (var i = 0; i < storage.Properties.Length; i++)
-                {
-                    var property = storage.Properties[i];
-                    array[i] = new KeyValuePair<string, object>(property.Name, property.GetValue(storage.Value));
-                }
-
-                _arrayStorage = array;
-                _propertyStorage = null;
-                return;
-            }
-
-            if (_arrayStorage.Length < capacity)
-            {
-                capacity = _arrayStorage.Length == 0 ? DefaultCapacity : _arrayStorage.Length * 2;
-                var array = new KeyValuePair<string, object>[capacity];
-                if (_count > 0)
-                {
-                    Array.Copy(_arrayStorage, 0, array, 0, _count);
-                }
-
-                _arrayStorage = array;
-            }
-        }
-
-        [MethodImpl(MethodImplOptions.AggressiveInlining)]
-        private int FindIndex(string key)
-        {
-            // Generally the bounds checking here will be elided by the JIT because this will be called
-            // on the same code path as EnsureCapacity.
-            var array = _arrayStorage;
-            var count = _count;
-
-            for (var i = 0; i < count; i++)
-            {
-                if (string.Equals(array[i].Key, key, StringComparison.OrdinalIgnoreCase))
-                {
-                    return i;
-                }
-            }
-
-            return -1;
-        }
-
-        [MethodImpl(MethodImplOptions.AggressiveInlining)]
-        private bool TryFindItem(string key, out object value)
-        {
-            var array = _arrayStorage;
-            var count = _count;
-
-            // Elide bounds check for indexing.
-            if ((uint)count <= (uint)array.Length)
-            {
-                for (var i = 0; i < count; i++)
-                {
-                    if (string.Equals(array[i].Key, key, StringComparison.OrdinalIgnoreCase))
-                    {
-                        value = array[i].Value;
-                        return true;
-                    }
-                }
-            }
-
-            value = null;
-            return false;
-        }
-
-        public struct Enumerator : IEnumerator<KeyValuePair<string, object>>
-        {
-            private readonly RouteValueDictionary _dictionary;
-            private int _index;
-
-            public Enumerator(RouteValueDictionary dictionary)
-            {
-                if (dictionary == null)
-                {
-                    throw new ArgumentNullException();
-                }
-
-                _dictionary = dictionary;
-
-                Current = default;
-                _index = 0;
-            }
-
-            public KeyValuePair<string, object> Current { get; private set; }
-
-            object IEnumerator.Current => Current;
-
-            public void Dispose()
-            {
-            }
-
-            // Similar to the design of List<T>.Enumerator - Split into fast path and slow path for inlining friendliness
-            [MethodImpl(MethodImplOptions.AggressiveInlining)]
-            public bool MoveNext()
-            {
-                var dictionary = _dictionary;
-
-                // The uncommon case is that the propertyStorage is in use
-                if (dictionary._propertyStorage == null && ((uint)_index < (uint)dictionary._count))
-                {
-                    Current = dictionary._arrayStorage[_index];
-                    _index++;
-                    return true;
-                }
-
-                return MoveNextRare();
-            }
-
-            private bool MoveNextRare()
-            {
-                var dictionary = _dictionary; 
-                if (dictionary._propertyStorage != null && ((uint)_index < (uint)dictionary._count))
-                {
-                    var storage = dictionary._propertyStorage;
-                    var property = storage.Properties[_index];
-                    Current = new KeyValuePair<string, object>(property.Name, property.GetValue(storage.Value));
-                    _index++;
-                    return true;
-                }
-
-                _index = dictionary._count;
-                Current = default;
-                return false;
-            }
-
-            public void Reset()
-            {
-                Current = default;
-                _index = 0;
-            }
-        }
-
-        internal class PropertyStorage
-        {
-            private static readonly ConcurrentDictionary<Type, PropertyHelper[]> _propertyCache = new ConcurrentDictionary<Type, PropertyHelper[]>();
-
-            public readonly object Value;
-            public 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);
-                }
-            }
-
-            private static void ValidatePropertyNames(Type type, PropertyHelper[] properties)
-            {
-                var names = new Dictionary<string, PropertyHelper>(StringComparer.OrdinalIgnoreCase);
-                for (var i = 0; i < properties.Length; i++)
-                {
-                    var property = properties[i];
-
-                    if (names.TryGetValue(property.Name, out var duplicate))
-                    {
-                        var message = Resources.FormatRouteValueDictionary_DuplicatePropertyName(
-                            type.FullName,
-                            property.Name,
-                            duplicate.Name,
-                            nameof(RouteValueDictionary));
-                        throw new InvalidOperationException(message);
-                    }
-
-                    names.Add(property.Name, property);
-                }
-            }
-        }
-    }
-}

+ 210 - 0
src/Routing/src/Microsoft.AspNetCore.Routing/Builder/EndpointRouteBuilderExtensions.cs

@@ -0,0 +1,210 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.AspNetCore.Routing.Patterns;
+
+namespace Microsoft.AspNetCore.Builder
+{
+    public static class EndpointRouteBuilderExtensions
+    {
+        // Avoid creating a new array every call
+        private static readonly string[] GetVerb = new[] { "GET" };
+        private static readonly string[] PostVerb = new[] { "POST" };
+        private static readonly string[] PutVerb = new[] { "PUT" };
+        private static readonly string[] DeleteVerb = new[] { "DELETE" };
+
+        #region MapVerbs
+        public static IEndpointConventionBuilder MapGet(
+            this IEndpointRouteBuilder builder,
+            string pattern,
+            RequestDelegate requestDelegate,
+            params object[] metadata)
+        {
+            return MapVerbs(builder, pattern, displayName: null, requestDelegate, GetVerb, metadata);
+        }
+
+        public static IEndpointConventionBuilder MapGet(
+            this IEndpointRouteBuilder builder,
+            string pattern,
+            string displayName,
+            RequestDelegate requestDelegate,
+            params object[] metadata)
+        {
+            return MapVerbs(builder, pattern, displayName, requestDelegate, GetVerb, metadata);
+        }
+
+        public static IEndpointConventionBuilder MapPost(
+            this IEndpointRouteBuilder builder,
+            string pattern,
+            RequestDelegate requestDelegate,
+            params object[] metadata)
+        {
+            return MapVerbs(builder, pattern, displayName: null, requestDelegate, PostVerb, metadata);
+        }
+
+        public static IEndpointConventionBuilder MapPost(
+            this IEndpointRouteBuilder builder,
+            string pattern,
+            string displayName,
+            RequestDelegate requestDelegate,
+            params object[] metadata)
+        {
+            return MapVerbs(builder, pattern, displayName, requestDelegate, PostVerb, metadata);
+        }
+
+        public static IEndpointConventionBuilder MapPut(
+            this IEndpointRouteBuilder builder,
+            string pattern,
+            RequestDelegate requestDelegate,
+            params object[] metadata)
+        {
+            return MapVerbs(builder, pattern, displayName: null, requestDelegate, PutVerb, metadata);
+        }
+
+        public static IEndpointConventionBuilder MapPut(
+            this IEndpointRouteBuilder builder,
+            string pattern,
+            string displayName,
+            RequestDelegate requestDelegate,
+            params object[] metadata)
+        {
+            return MapVerbs(builder, pattern, displayName, requestDelegate, PutVerb, metadata);
+        }
+
+        public static IEndpointConventionBuilder MapDelete(
+            this IEndpointRouteBuilder builder,
+            string pattern,
+            RequestDelegate requestDelegate,
+            params object[] metadata)
+        {
+            return MapVerbs(builder, pattern, displayName: null, requestDelegate, DeleteVerb, metadata);
+        }
+
+        public static IEndpointConventionBuilder MapDelete(
+            this IEndpointRouteBuilder builder,
+            string pattern,
+            string displayName,
+            RequestDelegate requestDelegate,
+            params object[] metadata)
+        {
+            return MapVerbs(builder, pattern, displayName, requestDelegate, DeleteVerb, metadata);
+        }
+
+        public static IEndpointConventionBuilder MapVerbs(
+           this IEndpointRouteBuilder builder,
+           string pattern,
+           RequestDelegate requestDelegate,
+           IList<string> httpMethods,
+           params object[] metadata)
+        {
+            return MapVerbs(builder, pattern, displayName: null, requestDelegate, httpMethods, metadata);
+        }
+
+        public static IEndpointConventionBuilder MapVerbs(
+           this IEndpointRouteBuilder builder,
+           string pattern,
+           string displayName,
+           RequestDelegate requestDelegate,
+           IList<string> httpMethods,
+           params object[] metadata)
+        {
+            if (httpMethods == null)
+            {
+                throw new ArgumentNullException(nameof(httpMethods));
+            }
+
+            var resolvedMetadata = new List<object>();
+            resolvedMetadata.Add(new HttpMethodMetadata(httpMethods));
+            if (metadata != null)
+            {
+                resolvedMetadata.AddRange(metadata);
+            }
+
+            return Map(builder, pattern, displayName ?? $"{pattern} HTTP: {string.Join(", ", httpMethods)}", requestDelegate, metadata: resolvedMetadata.ToArray());
+        }
+        #endregion
+
+        #region Map
+        public static IEndpointConventionBuilder Map(
+            this IEndpointRouteBuilder builder,
+            string pattern,
+            RequestDelegate requestDelegate,
+            params object[] metadata)
+        {
+            return Map(builder, RoutePatternFactory.Parse(pattern), pattern, requestDelegate, metadata);
+        }
+
+        public static IEndpointConventionBuilder Map(
+            this IEndpointRouteBuilder builder,
+            string pattern,
+            string displayName,
+            RequestDelegate requestDelegate,
+            params object[] metadata)
+        {
+            return Map(builder, RoutePatternFactory.Parse(pattern), displayName, requestDelegate, metadata);
+        }
+
+        public static IEndpointConventionBuilder Map(
+            this IEndpointRouteBuilder builder,
+            RoutePattern pattern,
+            RequestDelegate requestDelegate,
+            params object[] metadata)
+        {
+            return Map(builder, pattern, pattern.RawText ?? pattern.DebuggerToString(), requestDelegate, metadata);
+        }
+
+        public static IEndpointConventionBuilder Map(
+            this IEndpointRouteBuilder builder,
+            RoutePattern pattern,
+            string displayName,
+            RequestDelegate requestDelegate,
+            params object[] metadata)
+        {
+            if (builder == null)
+            {
+                throw new ArgumentNullException(nameof(builder));
+            }
+
+            if (pattern == null)
+            {
+                throw new ArgumentNullException(nameof(pattern));
+            }
+
+            if (requestDelegate == null)
+            {
+                throw new ArgumentNullException(nameof(requestDelegate));
+            }
+
+            const int defaultOrder = 0;
+
+            var routeEndpointModel = new RouteEndpointModel(
+                requestDelegate,
+                pattern,
+                defaultOrder);
+            routeEndpointModel.DisplayName = displayName;
+            if (metadata != null)
+            {
+                foreach (var item in metadata)
+                {
+                    routeEndpointModel.Metadata.Add(item);
+                }
+            }
+
+            var modelEndpointDataSource = builder.DataSources.OfType<ModelEndpointDataSource>().FirstOrDefault();
+
+            if (modelEndpointDataSource == null)
+            {
+                modelEndpointDataSource = new ModelEndpointDataSource();
+                builder.DataSources.Add(modelEndpointDataSource);
+            }
+
+            return modelEndpointDataSource.AddEndpointModel(routeEndpointModel);
+        }
+        #endregion
+    }
+}

+ 28 - 5
src/Routing/src/Microsoft.AspNetCore.Routing/Internal/EndpointRoutingApplicationBuilderExtensions.cs → src/Routing/src/Microsoft.AspNetCore.Routing/Builder/EndpointRoutingApplicationBuilderExtensions.cs

@@ -2,25 +2,48 @@
 // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 
 using System;
-using Microsoft.AspNetCore.Builder;
 using Microsoft.AspNetCore.Routing;
 using Microsoft.AspNetCore.Routing.Internal;
 using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
 
-namespace Microsoft.AspNetCore.Internal
+namespace Microsoft.AspNetCore.Builder
 {
     public static class EndpointRoutingApplicationBuilderExtensions
     {
         // Property key is used by MVC package to check that routing is registered
         private const string EndpointRoutingRegisteredKey = "__EndpointRoutingMiddlewareRegistered";
 
-        public static IApplicationBuilder UseEndpointRouting(this IApplicationBuilder builder)
+        public static IApplicationBuilder UseEndpointRouting(this IApplicationBuilder builder, Action<IEndpointRouteBuilder> configure)
         {
+            if (configure == null)
+            {
+                throw new ArgumentNullException(nameof(configure));
+            }
+
             VerifyRoutingIsRegistered(builder);
 
+            var routeOptions = builder.ApplicationServices.GetRequiredService<IOptions<RouteOptions>>();
+            EndpointDataSource middlewareEndpointDataSource;
+
+            var endpointRouteBuilder = builder.ApplicationServices.GetRequiredService<IEndpointRouteBuilder>();
+            if (endpointRouteBuilder is DefaultEndpointRouteBuilder defaultEndpointRouteBuilder)
+            {
+                defaultEndpointRouteBuilder.ApplicationBuilder = builder;
+            }
+            configure(endpointRouteBuilder);
+
+            foreach (var dataSource in endpointRouteBuilder.DataSources)
+            {
+                routeOptions.Value.EndpointDataSources.Add(dataSource);
+            }
+
+            // Create endpoint data source for data sources registered in configure
+            middlewareEndpointDataSource = new CompositeEndpointDataSource(endpointRouteBuilder.DataSources);
+
             builder.Properties[EndpointRoutingRegisteredKey] = true;
 
-            return builder.UseMiddleware<EndpointRoutingMiddleware>();
+            return builder.UseMiddleware<EndpointRoutingMiddleware>(middlewareEndpointDataSource);
         }
 
         public static IApplicationBuilder UseEndpoint(this IApplicationBuilder builder)
@@ -51,4 +74,4 @@ namespace Microsoft.AspNetCore.Internal
             }
         }
     }
-}
+}

+ 34 - 7
src/Routing/src/Microsoft.AspNetCore.Routing/CompositeEndpointDataSource.cs

@@ -3,6 +3,8 @@
 
 using System;
 using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Collections.Specialized;
 using System.Diagnostics;
 using System.Linq;
 using System.Text;
@@ -18,24 +20,49 @@ namespace Microsoft.AspNetCore.Routing
     [DebuggerDisplay("{DebuggerDisplayString,nq}")]
     public sealed class CompositeEndpointDataSource : EndpointDataSource
     {
-        private readonly EndpointDataSource[] _dataSources;
         private readonly object _lock;
+        private readonly ICollection<EndpointDataSource> _dataSources;
         private IReadOnlyList<Endpoint> _endpoints;
         private IChangeToken _consumerChangeToken;
         private CancellationTokenSource _cts;
 
-        internal CompositeEndpointDataSource(IEnumerable<EndpointDataSource> dataSources)
+        private CompositeEndpointDataSource()
         {
-            if (dataSources == null)
+            CreateChangeToken();
+            _lock = new object();
+        }
+
+        internal CompositeEndpointDataSource(ObservableCollection<EndpointDataSource> dataSources) : this()
+        {
+            dataSources.CollectionChanged += OnDataSourcesChanged;
+
+            _dataSources = dataSources;
+        }
+
+        public CompositeEndpointDataSource(IEnumerable<EndpointDataSource> endpointDataSources) : this()
+        {
+            _dataSources = new List<EndpointDataSource>();
+
+            foreach (var dataSource in endpointDataSources)
             {
-                throw new ArgumentNullException(nameof(dataSources));
+                _dataSources.Add(dataSource);
             }
+        }
 
-            CreateChangeToken();
-            _dataSources = dataSources.ToArray();
-            _lock = new object();
+        private void OnDataSourcesChanged(object sender, NotifyCollectionChangedEventArgs e)
+        {
+            lock (_lock)
+            {
+                // Only trigger changes if composite data source has already initialized endpoints
+                if (_endpoints != null)
+                {
+                    HandleChange();
+                }
+            }
         }
 
+        public IEnumerable<EndpointDataSource> DataSources => _dataSources;
+
         /// <summary>
         /// Gets a <see cref="IChangeToken"/> used to signal invalidation of cached <see cref="Endpoint"/>
         /// instances.

+ 6 - 8
src/Routing/src/Microsoft.AspNetCore.Routing/DependencyInjection/ConfigureEndpointOptions.cs → src/Routing/src/Microsoft.AspNetCore.Routing/ConfigureRouteOptions.cs

@@ -3,16 +3,17 @@
 
 using System;
 using System.Collections.Generic;
+using System.Collections.ObjectModel;
 using Microsoft.AspNetCore.Routing;
 using Microsoft.Extensions.Options;
 
 namespace Microsoft.Extensions.DependencyInjection
 {
-    internal class ConfigureEndpointOptions : IConfigureOptions<EndpointOptions>
+    internal class ConfigureRouteOptions : IConfigureOptions<RouteOptions>
     {
-        private readonly IEnumerable<EndpointDataSource> _dataSources;
+        private readonly ICollection<EndpointDataSource> _dataSources;
 
-        public ConfigureEndpointOptions(IEnumerable<EndpointDataSource> dataSources)
+        public ConfigureRouteOptions(ICollection<EndpointDataSource> dataSources)
         {
             if (dataSources == null)
             {
@@ -22,17 +23,14 @@ namespace Microsoft.Extensions.DependencyInjection
             _dataSources = dataSources;
         }
 
-        public void Configure(EndpointOptions options)
+        public void Configure(RouteOptions options)
         {
             if (options == null)
             {
                 throw new ArgumentNullException(nameof(options));
             }
 
-            foreach (var dataSource in _dataSources)
-            {
-                options.DataSources.Add(dataSource);
-            }
+            options.EndpointDataSources = _dataSources;
         }
     }
 }

+ 25 - 0
src/Routing/src/Microsoft.AspNetCore.Routing/DefaultEndpointRouteBuilder.cs

@@ -0,0 +1,25 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using Microsoft.AspNetCore.Builder;
+
+namespace Microsoft.AspNetCore.Routing
+{
+    internal class DefaultEndpointRouteBuilder : IEndpointRouteBuilder
+    {
+        public DefaultEndpointRouteBuilder()
+        {
+            DataSources = new List<EndpointDataSource>();
+        }
+
+        public IApplicationBuilder ApplicationBuilder { get; set; }
+
+        public IApplicationBuilder CreateApplicationBuilder() => ApplicationBuilder.New();
+
+        public ICollection<EndpointDataSource> DataSources { get; }
+
+        public IServiceProvider ServiceProvider => ApplicationBuilder.ApplicationServices;
+    }
+}

+ 1 - 1
src/Routing/src/Microsoft.AspNetCore.Routing/DefaultLinkGenerator.cs

@@ -38,7 +38,7 @@ namespace Microsoft.AspNetCore.Routing
 
         public DefaultLinkGenerator(
             ParameterPolicyFactory parameterPolicyFactory,
-            CompositeEndpointDataSource dataSource,
+            EndpointDataSource dataSource,
             ObjectPool<UriBuildingContext> uriBuildingContextPool,
             IOptions<RouteOptions> routeOptions,
             ILogger<DefaultLinkGenerator> logger,

+ 19 - 5
src/Routing/src/Microsoft.AspNetCore.Routing/DependencyInjection/RoutingServiceCollectionExtensions.cs

@@ -2,9 +2,11 @@
 // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 
 using System;
+using System.Collections.ObjectModel;
 using Microsoft.AspNetCore.Routing;
 using Microsoft.AspNetCore.Routing.Internal;
 using Microsoft.AspNetCore.Routing.Matching;
+using Microsoft.AspNetCore.Routing.Patterns;
 using Microsoft.AspNetCore.Routing.Tree;
 using Microsoft.Extensions.DependencyInjection.Extensions;
 using Microsoft.Extensions.Logging;
@@ -49,16 +51,23 @@ namespace Microsoft.Extensions.DependencyInjection
 
             services.TryAddSingleton(typeof(RoutingMarkerService));
 
-            // Collect all data sources from DI.
-            services.TryAddEnumerable(ServiceDescriptor.Transient<IConfigureOptions<EndpointOptions>, ConfigureEndpointOptions>());
+            // Setup global collection of endpoint data sources
+            var dataSources = new ObservableCollection<EndpointDataSource>();
+            services.TryAddEnumerable(ServiceDescriptor.Transient<IConfigureOptions<RouteOptions>, ConfigureRouteOptions>(
+                serviceProvider => new ConfigureRouteOptions(dataSources)));
 
             // Allow global access to the list of endpoints.
-            services.TryAddSingleton<CompositeEndpointDataSource>(s =>
+            services.TryAddSingleton<EndpointDataSource>(s =>
             {
-                var options = s.GetRequiredService<IOptions<EndpointOptions>>();
-                return new CompositeEndpointDataSource(options.Value.DataSources);
+                // Call internal ctor and pass global collection
+                return new CompositeEndpointDataSource(dataSources);
             });
 
+            //
+            // Endpoint Infrastructure
+            //
+            services.TryAddTransient<IEndpointRouteBuilder, DefaultEndpointRouteBuilder>();
+
             //
             // Default matcher implementation
             //
@@ -78,6 +87,11 @@ namespace Microsoft.Extensions.DependencyInjection
             services.TryAddSingleton<EndpointSelector, DefaultEndpointSelector>();
             services.TryAddEnumerable(ServiceDescriptor.Singleton<MatcherPolicy, HttpMethodMatcherPolicy>());
 
+            //
+            // Misc infrastructure
+            // 
+            services.TryAddSingleton<RoutePatternTransformer, DefaultRoutePatternTransformer>();
+
             return services;
         }
 

+ 19 - 0
src/Routing/src/Microsoft.AspNetCore.Routing/EndpointModel.cs

@@ -0,0 +1,19 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Collections.Generic;
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Routing
+{
+    public abstract class EndpointModel
+    {
+        public RequestDelegate RequestDelegate { get; set; }
+
+        public string DisplayName { get; set; }
+
+        public IList<object> Metadata { get; } = new List<object>();
+
+        public abstract Endpoint Build();
+    }
+}

+ 1 - 1
src/Routing/src/Microsoft.AspNetCore.Routing/EndpointNameAddressScheme.cs

@@ -13,7 +13,7 @@ namespace Microsoft.AspNetCore.Routing
     {
         private readonly DataSourceDependentCache<Dictionary<string, Endpoint[]>> _cache;
 
-        public EndpointNameAddressScheme(CompositeEndpointDataSource dataSource)
+        public EndpointNameAddressScheme(EndpointDataSource dataSource)
         {
             _cache = new DataSourceDependentCache<Dictionary<string, Endpoint[]>>(dataSource, Initialize);
         }

+ 0 - 13
src/Routing/src/Microsoft.AspNetCore.Routing/EndpointOptions.cs

@@ -1,13 +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.Routing
-{
-    // Internal for 2.2. Public API for configuring endpoints will be added in 3.0
-    internal class EndpointOptions
-    {
-        public IList<EndpointDataSource> DataSources { get; } = new List<EndpointDataSource>();
-    }
-}

+ 2 - 2
src/Routing/src/Microsoft.AspNetCore.Routing/EndpointRoutingMiddleware.cs

@@ -16,14 +16,14 @@ namespace Microsoft.AspNetCore.Routing
     {
         private readonly MatcherFactory _matcherFactory;
         private readonly ILogger _logger;
-        private readonly CompositeEndpointDataSource _endpointDataSource;
+        private readonly EndpointDataSource _endpointDataSource;
         private readonly RequestDelegate _next;
 
         private Task<Matcher> _initializationTask;
 
         public EndpointRoutingMiddleware(
             MatcherFactory matcherFactory,
-            CompositeEndpointDataSource endpointDataSource,
+            EndpointDataSource endpointDataSource,
             ILogger<EndpointRoutingMiddleware> logger,
             RequestDelegate next)
         {

+ 12 - 0
src/Routing/src/Microsoft.AspNetCore.Routing/IEndpointModelConvention.cs

@@ -0,0 +1,12 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+
+namespace Microsoft.AspNetCore.Routing
+{
+    public interface IEndpointConventionBuilder
+    {
+        void Apply(Action<EndpointModel> convention);
+    }
+}

+ 18 - 0
src/Routing/src/Microsoft.AspNetCore.Routing/IEndpointRouteBuilder.cs

@@ -0,0 +1,18 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using Microsoft.AspNetCore.Builder;
+
+namespace Microsoft.AspNetCore.Routing
+{
+    public interface IEndpointRouteBuilder
+    {
+        IApplicationBuilder CreateApplicationBuilder();
+
+        IServiceProvider ServiceProvider { get; }
+
+        ICollection<EndpointDataSource> DataSources { get; }
+    }
+}

+ 169 - 0
src/Routing/src/Microsoft.AspNetCore.Routing/Internal/ArrayBuilder.cs

@@ -0,0 +1,169 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+// LICENSING NOTE: This file is from the dotnet corefx repository.
+// 
+// See https://github.com/dotnet/corefx/blob/143df51926f2ad397fef9c9ca7ede88e2721e801/src/Common/src/System/Collections/Generic/ArrayBuilder.cs 
+
+
+using System;
+using System.Diagnostics;
+
+namespace Microsoft.AspNetCore.Routing.Internal
+{
+    /// <summary>
+    /// Helper type for avoiding allocations while building arrays.
+    /// </summary>
+    /// <typeparam name="T">The element type.</typeparam>
+    internal struct ArrayBuilder<T>
+    {
+        private const int DefaultCapacity = 4;
+        private const int MaxCoreClrArrayLength = 0x7fefffff; // For byte arrays the limit is slightly larger
+
+        private T[] _array; // Starts out null, initialized on first Add.
+        private int _count; // Number of items into _array we're using.
+
+        /// <summary>
+        /// Initializes the <see cref="ArrayBuilder{T}"/> with a specified capacity.
+        /// </summary>
+        /// <param name="capacity">The capacity of the array to allocate.</param>
+        public ArrayBuilder(int capacity) : this()
+        {
+            Debug.Assert(capacity >= 0);
+            if (capacity > 0)
+            {
+                _array = new T[capacity];
+            }
+        }
+
+        /// <summary>
+        /// Gets the number of items this instance can store without re-allocating,
+        /// or 0 if the backing array is <c>null</c>.
+        /// </summary>
+        public int Capacity => _array?.Length ?? 0;
+
+        /// <summary>Gets the current underlying array.</summary>
+        public T[] Buffer => _array;
+
+        /// <summary>
+        /// Gets the number of items in the array currently in use.
+        /// </summary>
+        public int Count => _count;
+
+        /// <summary>
+        /// Gets or sets the item at a certain index in the array.
+        /// </summary>
+        /// <param name="index">The index into the array.</param>
+        public T this[int index]
+        {
+            get
+            {
+                Debug.Assert(index >= 0 && index < _count);
+                return _array[index];
+            }
+        }
+
+        /// <summary>
+        /// Adds an item to the backing array, resizing it if necessary.
+        /// </summary>
+        /// <param name="item">The item to add.</param>
+        public void Add(T item)
+        {
+            if (_count == Capacity)
+            {
+                EnsureCapacity(_count + 1);
+            }
+
+            UncheckedAdd(item);
+        }
+
+        /// <summary>
+        /// Gets the first item in this builder.
+        /// </summary>
+        public T First()
+        {
+            Debug.Assert(_count > 0);
+            return _array[0];
+        }
+
+        /// <summary>
+        /// Gets the last item in this builder.
+        /// </summary>
+        public T Last()
+        {
+            Debug.Assert(_count > 0);
+            return _array[_count - 1];
+        }
+
+        /// <summary>
+        /// Creates an array from the contents of this builder.
+        /// </summary>
+        /// <remarks>
+        /// Do not call this method twice on the same builder.
+        /// </remarks>
+        public T[] ToArray()
+        {
+            if (_count == 0)
+            {
+                return Array.Empty<T>();
+            }
+
+            Debug.Assert(_array != null); // Nonzero _count should imply this
+
+            T[] result = _array;
+            if (_count < result.Length)
+            {
+                // Avoid a bit of overhead (method call, some branches, extra codegen)
+                // which would be incurred by using Array.Resize
+                result = new T[_count];
+                Array.Copy(_array, 0, result, 0, _count);
+            }
+
+#if DEBUG
+            // Try to prevent callers from using the ArrayBuilder after ToArray, if _count != 0.
+            _count = -1;
+            _array = null;
+#endif
+
+            return result;
+        }
+
+        /// <summary>
+        /// Adds an item to the backing array, without checking if there is room.
+        /// </summary>
+        /// <param name="item">The item to add.</param>
+        /// <remarks>
+        /// Use this method if you know there is enough space in the <see cref="ArrayBuilder{T}"/>
+        /// for another item, and you are writing performance-sensitive code.
+        /// </remarks>
+        public void UncheckedAdd(T item)
+        {
+            Debug.Assert(_count < Capacity);
+
+            _array[_count++] = item;
+        }
+
+        private void EnsureCapacity(int minimum)
+        {
+            Debug.Assert(minimum > Capacity);
+
+            int capacity = Capacity;
+            int nextCapacity = capacity == 0 ? DefaultCapacity : 2 * capacity;
+
+            if ((uint)nextCapacity > (uint)MaxCoreClrArrayLength)
+            {
+                nextCapacity = Math.Max(capacity + 1, MaxCoreClrArrayLength);
+            }
+
+            nextCapacity = Math.Max(nextCapacity, minimum);
+
+            T[] next = new T[nextCapacity];
+            if (_count > 0)
+            {
+                Array.Copy(_array, 0, next, 0, _count);
+            }
+            _array = next;
+        }
+    }
+}

+ 1 - 6
src/Routing/src/Microsoft.AspNetCore.Routing/Internal/LinkGenerationDecisionTree.cs

@@ -119,12 +119,7 @@ namespace Microsoft.AspNetCore.Routing.Internal
 
         private class OutboundMatchClassifier : IClassifier<OutboundMatch>
         {
-            public OutboundMatchClassifier()
-            {
-                ValueComparer = new RouteValueEqualityComparer();
-            }
-
-            public IEqualityComparer<object> ValueComparer { get; private set; }
+            public IEqualityComparer<object> ValueComparer => RouteValueEqualityComparer.Default;
 
             public IDictionary<string, DecisionCriterionValue> GetCriteria(OutboundMatch item)
             {

+ 0 - 3
src/Routing/src/Microsoft.AspNetCore.Routing/Matching/ILEmitTrieFactory.cs

@@ -1,7 +1,6 @@
 // Copyright (c) .NET Foundation. All rights reserved.
 // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 
-#if IL_EMIT
 using System;
 using System.Diagnostics;
 using System.Linq;
@@ -596,5 +595,3 @@ namespace Microsoft.AspNetCore.Routing.Matching
         }
     }
 }
-
-#endif

+ 0 - 2
src/Routing/src/Microsoft.AspNetCore.Routing/Matching/ILEmitTrieJumpTable.cs

@@ -1,6 +1,5 @@
 // Copyright (c) .NET Foundation. All rights reserved.
 // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
-#if IL_EMIT
 
 using System;
 using System.Threading;
@@ -101,4 +100,3 @@ namespace Microsoft.AspNetCore.Routing.Matching
         }
     }
 }
-#endif

+ 0 - 4
src/Routing/src/Microsoft.AspNetCore.Routing/Matching/JumpTableBuilder.cs

@@ -84,11 +84,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
                 fallback = new DictionaryJumpTable(defaultDestination, exitDestination, pathEntries);
             }
 
-#if IL_EMIT
             return new ILEmitTrieJumpTable(defaultDestination, exitDestination, pathEntries, vectorize: null, fallback);
-#else
-            return fallback;
-#endif
         }
     }
 }

+ 2 - 6
src/Routing/src/Microsoft.AspNetCore.Routing/Microsoft.AspNetCore.Routing.csproj

@@ -4,7 +4,7 @@
 Commonly used types:
 Microsoft.AspNetCore.Routing.Route
 Microsoft.AspNetCore.Routing.RouteCollection</Description>
-    <TargetFrameworks>netstandard2.0;netcoreapp2.2</TargetFrameworks>
+    <TargetFramework>netcoreapp3.0</TargetFramework>
     <NoWarn>$(NoWarn);CS1591</NoWarn>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <PackageTags>aspnetcore;routing</PackageTags>
@@ -12,14 +12,10 @@ Microsoft.AspNetCore.Routing.RouteCollection</Description>
   </PropertyGroup>
 
   <PropertyGroup>
-    <!-- 
-      RefEmit is supported in netcoreapp. 
-      
+    <!--      
       The ability to save compiled assemblies is for testing and debugging, not shipped in the product.
     -->
-    <ILEmit Condition="'$(TargetFramework)'!='netstandard2.0'">true</ILEmit>
     <ILEmitSaveAssemblies Condition="'$(ILEmitSaveAssemblies)'==''">false</ILEmitSaveAssemblies>
-    <DefineConstants Condition="'$(ILEmit)'=='true'">IL_EMIT;$(DefineConstants)</DefineConstants>
     <DefineConstants Condition="'$(ILEmitSaveAssemblies)'=='true'">IL_EMIT_SAVE_ASSEMBLIES;$(DefineConstants)</DefineConstants>
   </PropertyGroup>
 

+ 68 - 0
src/Routing/src/Microsoft.AspNetCore.Routing/ModelEndpointDataSource.cs

@@ -0,0 +1,68 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.FileProviders;
+using Microsoft.Extensions.Primitives;
+
+namespace Microsoft.AspNetCore.Routing
+{
+    internal class ModelEndpointDataSource : EndpointDataSource
+    {
+        private List<EndpointConventionBuilder> _endpointConventionBuilders;
+
+        public ModelEndpointDataSource()
+        {
+            _endpointConventionBuilders = new List<EndpointConventionBuilder>();
+        }
+
+        public IEndpointConventionBuilder AddEndpointModel(EndpointModel endpointModel)
+        {
+            var builder = new EndpointConventionBuilder(endpointModel);
+            _endpointConventionBuilders.Add(builder);
+
+            return builder;
+        }
+
+        public override IChangeToken GetChangeToken()
+        {
+            return NullChangeToken.Singleton;
+        }
+
+        public override IReadOnlyList<Endpoint> Endpoints => _endpointConventionBuilders.Select(e => e.Build()).ToArray();
+
+        // for testing
+        internal IEnumerable<EndpointModel> EndpointModels => _endpointConventionBuilders.Select(b => b.EndpointModel);
+
+        private class EndpointConventionBuilder : IEndpointConventionBuilder
+        {
+            internal EndpointModel EndpointModel { get; }
+
+            private readonly List<Action<EndpointModel>> _conventions;
+
+            public EndpointConventionBuilder(EndpointModel endpointModel)
+            {
+                EndpointModel = endpointModel;
+                _conventions = new List<Action<EndpointModel>>();
+            }
+
+            public void Apply(Action<EndpointModel> convention)
+            {
+                _conventions.Add(convention);
+            }
+
+            public Endpoint Build()
+            {
+                foreach (var convention in _conventions)
+                {
+                    convention(EndpointModel);
+                }
+
+                return EndpointModel.Build();
+            }
+        }
+    }
+}

+ 227 - 0
src/Routing/src/Microsoft.AspNetCore.Routing/Patterns/DefaultRoutePatternTransformer.cs

@@ -0,0 +1,227 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+
+namespace Microsoft.AspNetCore.Routing.Patterns
+{
+    internal class DefaultRoutePatternTransformer : RoutePatternTransformer
+    {
+        private readonly ParameterPolicyFactory _policyFactory;
+
+        public DefaultRoutePatternTransformer(ParameterPolicyFactory policyFactory)
+        {
+            if (policyFactory == null)
+            {
+                throw new ArgumentNullException(nameof(policyFactory));
+            }
+
+            _policyFactory = policyFactory;
+        }
+
+        public override RoutePattern SubstituteRequiredValues(RoutePattern original, object requiredValues)
+        {
+            if (original == null)
+            {
+                throw new ArgumentNullException(nameof(original));
+            }
+
+            return SubstituteRequiredValuesCore(original, new RouteValueDictionary(requiredValues));
+        }
+
+        private RoutePattern SubstituteRequiredValuesCore(RoutePattern original, RouteValueDictionary requiredValues)
+        {
+            // Process each required value in sequence. Bail if we find any rejection criteria. The goal
+            // of rejection is to avoid creating RoutePattern instances that can't *ever* match.
+            //
+            // If we succeed, then we need to create a new RoutePattern with the provided required values.
+            //
+            // Substitution can merge with existing RequiredValues already on the RoutePattern as long
+            // as all of the success criteria are still met at the end.
+            foreach (var kvp in requiredValues)
+            {
+                // There are three possible cases here:
+                // 1. Required value is null-ish
+                // 2. Required value corresponds to a parameter
+                // 3. Required value corresponds to a matching default value
+                //
+                // If none of these are true then we can reject this substitution.
+                RoutePatternParameterPart parameter;
+                if (RouteValueEqualityComparer.Default.Equals(kvp.Value, string.Empty))
+                {
+                    // 1. Required value is null-ish - check to make sure that this route doesn't have a
+                    // parameter or filter-like default.
+
+                    if (original.GetParameter(kvp.Key) != null)
+                    {
+                        // Fail: we can't 'require' that a parameter be null. In theory this would be possible
+                        // for an optional parameter, but that's not really in line with the usage of this feature
+                        // so we don't handle it.
+                        //
+                        // Ex: {controller=Home}/{action=Index}/{id?} - with required values: { controller = "" }
+                        return null;
+                    }
+                    else if (original.Defaults.TryGetValue(kvp.Key, out var defaultValue) &&
+                        !RouteValueEqualityComparer.Default.Equals(kvp.Value, defaultValue))
+                    {
+                        // Fail: this route has a non-parameter default that doesn't match.
+                        //
+                        // Ex: Admin/{controller=Home}/{action=Index}/{id?} defaults: { area = "Admin" } - with required values: { area = "" }
+                        return null;
+                    }
+
+                    // Success: (for this parameter at least)
+                    //
+                    // Ex: {controller=Home}/{action=Index}/{id?} - with required values: { area = "", ... }
+                    continue;
+                }
+                else if ((parameter = original.GetParameter(kvp.Key)) != null)
+                {
+                    // 2. Required value corresponds to a parameter - check to make sure that this value matches
+                    // any IRouteConstraint implementations.
+                    if (!MatchesConstraints(original, parameter, kvp.Key, requiredValues))
+                    {
+                        // Fail: this route has a constraint that failed.
+                        //
+                        // Ex: Admin/{controller:regex(Home|Login)}/{action=Index}/{id?} - with required values: { controller = "Store" }
+                        return null;
+                    }
+
+                    // Success: (for this parameter at least)
+                    //
+                    // Ex: {area}/{controller=Home}/{action=Index}/{id?} - with required values: { area = "", ... }
+                    continue;
+                }
+                else if (original.Defaults.TryGetValue(kvp.Key, out var defaultValue) &&
+                    RouteValueEqualityComparer.Default.Equals(kvp.Value, defaultValue))
+                {
+                    // 3. Required value corresponds to a matching default value - check to make sure that this value matches
+                    // any IRouteConstraint implementations. It's unlikely that this would happen in practice but it doesn't
+                    // hurt for us to check.
+                    if (!MatchesConstraints(original, parameter: null, kvp.Key, requiredValues))
+                    {
+                        // Fail: this route has a constraint that failed.
+                        //
+                        // Ex: 
+                        //  Admin/Home/{action=Index}/{id?} 
+                        //  defaults: { area = "Admin" }
+                        //  constraints: { area = "Blog" }
+                        //  with required values: { area = "Admin" }
+                        return null;
+                    }
+
+                    // Success: (for this parameter at least)
+                    //
+                    // Ex: Admin/{controller=Home}/{action=Index}/{id?} defaults: { area = "Admin" }- with required values: { area = "Admin", ... }
+                    continue;
+                }
+                else
+                {
+                    // Fail: this is a required value for a key that doesn't appear in the templates, or the route
+                    // pattern has a different default value for a non-parameter.
+                    //
+                    // Ex: Admin/{controller=Home}/{action=Index}/{id?} defaults: { area = "Admin" }- with required values: { area = "Blog", ... }
+                    // OR (less likely)
+                    // Ex: Admin/{controller=Home}/{action=Index}/{id?} with required values: { page = "/Index", ... }
+                    return null;
+                }
+            }
+
+            List<RoutePatternParameterPart> updatedParameters = null;
+            List<RoutePatternPathSegment> updatedSegments = null;
+            RouteValueDictionary updatedDefaults = null;
+
+            // So if we get here, we're ready to update the route pattern. We need to update two things:
+            // 1. Remove any default values that conflict with the required values.
+            // 2. Merge any existing required values
+            foreach (var kvp in requiredValues)
+            {
+                var parameter = original.GetParameter(kvp.Key);
+
+                // We only need to handle the case where the required value maps to a parameter. That's the only
+                // case where we allow a default and a required value to disagree, and we already validated the
+                // other cases.
+                if (parameter != null && 
+                    original.Defaults.TryGetValue(kvp.Key, out var defaultValue) && 
+                    !RouteValueEqualityComparer.Default.Equals(kvp.Value, defaultValue))
+                {
+                    if (updatedDefaults == null && updatedSegments == null && updatedParameters == null)
+                    {
+                        updatedDefaults = new RouteValueDictionary(original.Defaults);
+                        updatedSegments = new List<RoutePatternPathSegment>(original.PathSegments);
+                        updatedParameters = new List<RoutePatternParameterPart>(original.Parameters);
+                    }
+
+                    updatedDefaults.Remove(kvp.Key);
+                    RemoveParameterDefault(updatedSegments, updatedParameters, parameter);
+                }
+            }
+
+            foreach (var kvp in original.RequiredValues)
+            {
+                requiredValues.TryAdd(kvp.Key, kvp.Value);
+            }
+
+            return new RoutePattern(
+                original.RawText,
+                updatedDefaults ?? original.Defaults,
+                original.ParameterPolicies,
+                requiredValues,
+                updatedParameters ?? original.Parameters, 
+                updatedSegments ?? original.PathSegments);
+        }
+
+        private bool MatchesConstraints(RoutePattern pattern, RoutePatternParameterPart parameter, string key, RouteValueDictionary requiredValues)
+        {
+            if (pattern.ParameterPolicies.TryGetValue(key, out var policies))
+            {
+                for (var i = 0; i < policies.Count; i++)
+                {
+                    var policy = _policyFactory.Create(parameter, policies[i]);
+                    if (policy is IRouteConstraint constraint)
+                    {
+                        if (!constraint.Match(httpContext: null, NullRouter.Instance, key, requiredValues, RouteDirection.IncomingRequest))
+                        {
+                            return false;
+                        }
+                    }
+                }
+            }
+
+            return true;
+        }
+
+        private void RemoveParameterDefault(List<RoutePatternPathSegment> segments, List<RoutePatternParameterPart> parameters, RoutePatternParameterPart parameter)
+        {
+            // We know that a parameter can only appear once, so we only need to rewrite one segment and one parameter.
+            for (var i = 0; i < segments.Count; i++)
+            {
+                var segment = segments[i];
+                for (var j = 0; j < segment.Parts.Count; j++)
+                {
+                    if (object.ReferenceEquals(parameter, segment.Parts[j]))
+                    {
+                        // Found it!
+                        var updatedParameter = RoutePatternFactory.ParameterPart(parameter.Name, @default: null, parameter.ParameterKind, parameter.ParameterPolicies);
+
+                        var updatedParts = new List<RoutePatternPart>(segment.Parts);
+                        updatedParts[j] = updatedParameter;
+                        segments[i] = RoutePatternFactory.Segment(updatedParts);
+
+                        for (var k = 0; k < parameters.Count; k++)
+                        {
+                            if (ReferenceEquals(parameter, parameters[k]))
+                            {
+                                parameters[k] = updatedParameter;
+                                break;
+                            }
+                        }
+
+                        return;
+                    }
+                }
+            }
+        }
+    }
+}

+ 7 - 9
src/Routing/src/Microsoft.AspNetCore.Routing/Patterns/RouteParameterParser.cs

@@ -2,8 +2,7 @@
 // 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.Routing.Internal;
 
 namespace Microsoft.AspNetCore.Routing.Patterns
 {
@@ -77,7 +76,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns
                 currentIndex++;
             }
 
-            var parseResults = ParseConstraints(parameter, parameterName, currentIndex, endIndex);
+            var parseResults = ParseConstraints(parameter, currentIndex, endIndex);
             currentIndex = parseResults.CurrentIndex;
 
             string defaultValue = null;
@@ -91,17 +90,16 @@ namespace Microsoft.AspNetCore.Routing.Patterns
                 parameterName,
                 defaultValue,
                 parameterKind,
-                parseResults.ParameterPolicies.ToArray(),
+                parseResults.ParameterPolicies,
                 encodeSlashes);
         }
 
         private static ParameterPolicyParseResults ParseConstraints(
             string text,
-            string parameterName,
             int currentIndex,
             int endIndex)
         {
-            var constraints = new List<RoutePatternParameterPolicyReference>();
+            var constraints = new ArrayBuilder<RoutePatternParameterPolicyReference>(0);
             var state = ParseState.Start;
             var startIndex = currentIndex;
             do
@@ -234,7 +232,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns
 
             } while (state != ParseState.End);
 
-            return new ParameterPolicyParseResults(currentIndex, constraints);
+            return new ParameterPolicyParseResults(currentIndex, constraints.ToArray());
         }
 
         private enum ParseState
@@ -249,9 +247,9 @@ namespace Microsoft.AspNetCore.Routing.Patterns
         {
             public readonly int CurrentIndex;
 
-            public readonly IReadOnlyList<RoutePatternParameterPolicyReference> ParameterPolicies;
+            public readonly RoutePatternParameterPolicyReference[] ParameterPolicies;
 
-            public ParameterPolicyParseResults(int currentIndex, IReadOnlyList<RoutePatternParameterPolicyReference> parameterPolicies)
+            public ParameterPolicyParseResults(int currentIndex, RoutePatternParameterPolicyReference[] parameterPolicies)
             {
                 CurrentIndex = currentIndex;
                 ParameterPolicies = parameterPolicies;

+ 27 - 1
src/Routing/src/Microsoft.AspNetCore.Routing/Patterns/RoutePattern.cs

@@ -23,17 +23,20 @@ namespace Microsoft.AspNetCore.Routing.Patterns
             string rawText,
             IReadOnlyDictionary<string, object> defaults,
             IReadOnlyDictionary<string, IReadOnlyList<RoutePatternParameterPolicyReference>> parameterPolicies,
+            IReadOnlyDictionary<string, object> requiredValues,
             IReadOnlyList<RoutePatternParameterPart> parameters,
             IReadOnlyList<RoutePatternPathSegment> pathSegments)
         {
             Debug.Assert(defaults != null);
             Debug.Assert(parameterPolicies != null);
             Debug.Assert(parameters != null);
+            Debug.Assert(requiredValues != null);
             Debug.Assert(pathSegments != null);
 
             RawText = rawText;
             Defaults = defaults;
             ParameterPolicies = parameterPolicies;
+            RequiredValues = requiredValues;
             Parameters = parameters;
             PathSegments = pathSegments;
 
@@ -53,6 +56,29 @@ namespace Microsoft.AspNetCore.Routing.Patterns
         /// </summary>
         public IReadOnlyDictionary<string, IReadOnlyList<RoutePatternParameterPolicyReference>> ParameterPolicies { get; }
 
+        /// <summary>
+        /// Gets a collection of route values that must be provided for this route pattern to be considered
+        /// applicable.
+        /// </summary>
+        /// <remarks>
+        /// <para>
+        /// <see cref="RequiredValues"/> allows a framework to substitute route values into a parameterized template
+        /// so that the same route template specification can be used to create multiple route patterns.
+        /// <example>
+        /// This example shows how a route template can be used with required values to substitute known
+        /// route values for parameters.
+        /// <code>
+        /// Route Template: "{controller=Home}/{action=Index}/{id?}"
+        /// Route Values: { controller = "Store", action = "Index" }
+        /// </code>
+        /// 
+        /// A route pattern produced in this way will match and generate URL paths like: <c>/Store</c>, 
+        /// <c>/Store/Index</c>, and <c>/Store/Index/17</c>.
+        /// </example>
+        /// </para>
+        /// </remarks>
+        public IReadOnlyDictionary<string, object> RequiredValues { get; }
+
         /// <summary>
         /// Gets the precedence value of the route pattern for URL matching.
         /// </summary>
@@ -110,7 +136,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns
             return null;
         }
 
-        private string DebuggerToString()
+        internal string DebuggerToString()
         {
             return RawText ?? string.Join(SeparatorString, PathSegments.Select(s => s.DebuggerToString()));
         }

+ 94 - 14
src/Routing/src/Microsoft.AspNetCore.Routing/Patterns/RoutePatternFactory.cs

@@ -16,7 +16,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns
     /// </summary>
     public static class RoutePatternFactory
     {
-        private static readonly IReadOnlyDictionary<string, object> EmptyDefaultsDictionary =
+        private static readonly IReadOnlyDictionary<string, object> EmptyDictionary =
             new ReadOnlyDictionary<string, object>(new Dictionary<string, object>());
 
         private static readonly IReadOnlyDictionary<string, IReadOnlyList<RoutePatternParameterPolicyReference>> EmptyPoliciesDictionary =
@@ -61,7 +61,37 @@ namespace Microsoft.AspNetCore.Routing.Patterns
             }
 
             var original = RoutePatternParser.Parse(pattern);
-            return Pattern(original.RawText, defaults, parameterPolicies, original.PathSegments);
+            return PatternCore(original.RawText, Wrap(defaults), Wrap(parameterPolicies), requiredValues: null, original.PathSegments);
+        }
+
+        /// <summary>
+        /// Creates a <see cref="RoutePattern"/> from its string representation along
+        /// with provided default values and parameter policies.
+        /// </summary>
+        /// <param name="pattern">The route pattern string to parse.</param>
+        /// <param name="defaults">
+        /// Additional default values to associated with the route pattern. May be null.
+        /// The provided object will be converted to key-value pairs using <see cref="RouteValueDictionary"/>
+        /// and then merged into the parsed route pattern.
+        /// </param>
+        /// <param name="parameterPolicies">
+        /// Additional parameter policies to associated with the route pattern. May be null.
+        /// The provided object will be converted to key-value pairs using <see cref="RouteValueDictionary"/>
+        /// and then merged into the parsed route pattern.
+        /// </param>
+        /// <param name="requiredValues">
+        /// Route values that can be substituted for parameters in the route pattern. See remarks on <see cref="RoutePattern.RequiredValues"/>.
+        /// </param>
+        /// <returns>The <see cref="RoutePattern"/>.</returns>
+        public static RoutePattern Parse(string pattern, object defaults, object parameterPolicies, object requiredValues)
+        {
+            if (pattern == null)
+            {
+                throw new ArgumentNullException(nameof(pattern));
+            }
+
+            var original = RoutePatternParser.Parse(pattern);
+            return PatternCore(original.RawText, Wrap(defaults), Wrap(parameterPolicies), Wrap(requiredValues), original.PathSegments);
         }
 
         /// <summary>
@@ -76,7 +106,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns
                 throw new ArgumentNullException(nameof(segments));
             }
 
-            return PatternCore(null, null, null, segments);
+            return PatternCore(null, null, null, null, segments);
         }
 
         /// <summary>
@@ -92,7 +122,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns
                 throw new ArgumentNullException(nameof(segments));
             }
 
-            return PatternCore(rawText, null, null, segments);
+            return PatternCore(rawText, null, null, null, segments);
         }
 
         /// <summary>
@@ -121,14 +151,14 @@ namespace Microsoft.AspNetCore.Routing.Patterns
                 throw new ArgumentNullException(nameof(segments));
             }
 
-            return PatternCore(null, new RouteValueDictionary(defaults), new RouteValueDictionary(parameterPolicies), segments);
+            return PatternCore(null, new RouteValueDictionary(defaults), new RouteValueDictionary(parameterPolicies), requiredValues: null, segments);
         }
 
         /// <summary>
         /// Creates a <see cref="RoutePattern"/> from a collection of segments along
         /// with provided default values and parameter policies.
         /// </summary>
-        /// <param name="rawText">The raw text to associate with the route pattern.</param>
+        /// <param name="rawText">The raw text to associate with the route pattern. May be null.</param>
         /// <param name="defaults">
         /// Additional default values to associated with the route pattern. May be null.
         /// The provided object will be converted to key-value pairs using <see cref="RouteValueDictionary"/>
@@ -152,7 +182,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns
                 throw new ArgumentNullException(nameof(segments));
             }
 
-            return PatternCore(rawText, new RouteValueDictionary(defaults), new RouteValueDictionary(parameterPolicies), segments);
+            return PatternCore(rawText, new RouteValueDictionary(defaults), new RouteValueDictionary(parameterPolicies), requiredValues: null, segments);
         }
 
         /// <summary>
@@ -167,7 +197,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns
                 throw new ArgumentNullException(nameof(segments));
             }
 
-            return PatternCore(null, null, null, segments);
+            return PatternCore(null, null, null, requiredValues: null, segments);
         }
 
         /// <summary>
@@ -183,7 +213,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns
                 throw new ArgumentNullException(nameof(segments));
             }
 
-            return PatternCore(rawText, null, null, segments);
+            return PatternCore(rawText, null, null, requiredValues: null, segments);
         }
 
         /// <summary>
@@ -212,7 +242,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns
                 throw new ArgumentNullException(nameof(segments));
             }
 
-            return PatternCore(null, new RouteValueDictionary(defaults), new RouteValueDictionary(parameterPolicies), segments);
+            return PatternCore(null, new RouteValueDictionary(defaults), new RouteValueDictionary(parameterPolicies), requiredValues: null, segments);
         }
 
         /// <summary>
@@ -243,13 +273,14 @@ namespace Microsoft.AspNetCore.Routing.Patterns
                 throw new ArgumentNullException(nameof(segments));
             }
 
-            return PatternCore(rawText, new RouteValueDictionary(defaults), new RouteValueDictionary(parameterPolicies), segments);
+            return PatternCore(rawText, new RouteValueDictionary(defaults), new RouteValueDictionary(parameterPolicies), requiredValues: null, segments);
         }
 
         private static RoutePattern PatternCore(
             string rawText,
             RouteValueDictionary defaults,
             RouteValueDictionary parameterPolicies,
+            RouteValueDictionary requiredValues,
             IEnumerable<RoutePatternPathSegment> segments)
         {
             // We want to merge the segment data with the 'out of line' defaults and parameter policies.
@@ -311,12 +342,56 @@ namespace Microsoft.AspNetCore.Routing.Patterns
                 }
             }
 
+            // Each Required Value either needs to either:
+            // 1. be null-ish
+            // 2. have a corresponding parameter
+            // 3. have a corrsponding default that matches both key and value
+            if (requiredValues != null)
+            {
+                foreach (var kvp in requiredValues)
+                {
+                    // 1.be null-ish
+                    var found = RouteValueEqualityComparer.Default.Equals(string.Empty, kvp.Value);
+
+                    // 2. have a corresponding parameter
+                    if (!found && parameters != null)
+                    {
+                        for (var i = 0; i < parameters.Count; i++)
+                        {
+                            if (string.Equals(kvp.Key, parameters[i].Name, StringComparison.OrdinalIgnoreCase))
+                            {
+                                found = true;
+                                break;
+                            }
+                        }
+                    }
+
+                    // 3. have a corrsponding default that matches both key and value
+                    if (!found &&
+                        updatedDefaults != null &&
+                        updatedDefaults.TryGetValue(kvp.Key, out var defaultValue) &&
+                        RouteValueEqualityComparer.Default.Equals(kvp.Value, defaultValue))
+                    {
+                        found = true;
+                    }
+
+                    if (!found)
+                    {
+                        throw new InvalidOperationException(
+                            $"No corresponding parameter or default value could be found for the required value " +
+                            $"'{kvp.Key}={kvp.Value}'. A non-null required value must correspond to a route parameter or the " +
+                            $"route pattern must have a matching default value.");
+                    }
+                }
+            }
+
             return new RoutePattern(
                 rawText,
-                updatedDefaults ?? EmptyDefaultsDictionary,
+                updatedDefaults ?? EmptyDictionary,
                 updatedParameterPolicies != null
                     ? updatedParameterPolicies.ToDictionary(kvp => kvp.Key, kvp => (IReadOnlyList<RoutePatternParameterPolicyReference>)kvp.Value.ToArray())
                     : EmptyPoliciesDictionary,
+                requiredValues ?? EmptyDictionary,
                 (IReadOnlyList<RoutePatternParameterPart>)parameters ?? Array.Empty<RoutePatternParameterPart>(),
                 updatedSegments);
 
@@ -449,7 +524,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns
                 throw new ArgumentNullException(nameof(parts));
             }
 
-            return SegmentCore((RoutePatternPart[]) parts.Clone());
+            return SegmentCore((RoutePatternPart[])parts.Clone());
         }
 
         private static RoutePatternPathSegment SegmentCore(RoutePatternPart[] parts)
@@ -670,7 +745,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns
                 parameterName: parameterName,
                 @default: @default,
                 parameterKind: parameterKind,
-                parameterPolicies: (RoutePatternParameterPolicyReference[]) parameterPolicies.Clone());
+                parameterPolicies: (RoutePatternParameterPolicyReference[])parameterPolicies.Clone());
         }
 
         private static RoutePatternParameterPart ParameterPartCore(
@@ -802,5 +877,10 @@ namespace Microsoft.AspNetCore.Routing.Patterns
         {
             return new RoutePatternParameterPolicyReference(parameterPolicy);
         }
+
+        private static RouteValueDictionary Wrap(object values)
+        {
+            return values == null ? null : new RouteValueDictionary(values);
+        }
     }
 }

+ 35 - 0
src/Routing/src/Microsoft.AspNetCore.Routing/Patterns/RoutePatternTransformer.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.
+
+namespace Microsoft.AspNetCore.Routing.Patterns
+{
+    /// <summary>
+    /// A singleton service that provides transformations on <see cref="RoutePattern"/>.
+    /// </summary>
+    public abstract class RoutePatternTransformer
+    {
+        /// <summary>
+        /// Attempts to substitute the provided <paramref name="requiredValues"/> into the provided
+        /// <paramref name="original"/>.
+        /// </summary>
+        /// <param name="original">The original <see cref="RoutePattern"/>.</param>
+        /// <param name="requiredValues">The required values to substitute.</param>
+        /// <returns>
+        /// A new <see cref="RoutePattern"/> if substitution succeeds, otherwise <c>null</c>.
+        /// </returns>
+        /// <remarks>
+        /// <para>
+        /// Substituting required values into a route pattern is intended for us with a general-purpose
+        /// parameterize route specification that can match many logical endpoints. Calling 
+        /// <see cref="SubstituteRequiredValues(RoutePattern, object)"/> can produce a derived route pattern
+        /// for each set of route values that corresponds to an endpoint.
+        /// </para>
+        /// <para>
+        /// The substitution process considers default values and <see cref="IRouteConstraint"/> implementations
+        /// when examining a required value. <see cref="SubstituteRequiredValues(RoutePattern, object)"/> will
+        /// return <c>null</c> if any required value cannot be substituted.
+        /// </para>
+        /// </remarks>
+        public abstract RoutePattern SubstituteRequiredValues(RoutePattern original, object requiredValues);
+    }
+}

+ 37 - 0
src/Routing/src/Microsoft.AspNetCore.Routing/RouteEndpointModel.cs

@@ -0,0 +1,37 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Routing.Patterns;
+
+namespace Microsoft.AspNetCore.Routing
+{
+    public sealed class RouteEndpointModel : EndpointModel
+    {
+        public RoutePattern RoutePattern { get; set; }
+
+        public int Order { get; set; }
+
+        public RouteEndpointModel(
+           RequestDelegate requestDelegate,
+           RoutePattern routePattern,
+           int order)
+        {
+            RequestDelegate = requestDelegate;
+            RoutePattern = routePattern;
+            Order = order;
+        }
+
+        public override Endpoint Build()
+        {
+            var routeEndpoint = new RouteEndpoint(
+                RequestDelegate,
+                RoutePattern,
+                Order,
+                new EndpointMetadataCollection(Metadata),
+                DisplayName);
+
+            return routeEndpoint;
+        }
+    }
+}

+ 2 - 0
src/Routing/src/Microsoft.AspNetCore.Routing/RouteOptions.cs

@@ -9,6 +9,8 @@ namespace Microsoft.AspNetCore.Routing
 {
     public class RouteOptions
     {
+        public ICollection<EndpointDataSource> EndpointDataSources { get; internal set; }
+
         /// <summary>
         /// Gets or sets a value indicating whether all generated paths URLs are lower-case. 
         /// Use <see cref="LowercaseQueryStrings" /> to configure the behavior for query strings.

+ 2 - 0
src/Routing/src/Microsoft.AspNetCore.Routing/RouteValueEqualityComparer.cs

@@ -20,6 +20,8 @@ namespace Microsoft.AspNetCore.Routing
     /// </remarks>
     public class RouteValueEqualityComparer : IEqualityComparer<object>
     {
+        public static readonly RouteValueEqualityComparer Default = new RouteValueEqualityComparer();
+
         /// <inheritdoc />
         public new bool Equals(object x, object y)
         {

+ 18 - 7
src/Routing/src/Microsoft.AspNetCore.Routing/RouteValuesAddressScheme.cs

@@ -14,11 +14,11 @@ namespace Microsoft.AspNetCore.Routing
 {
     internal class RouteValuesAddressScheme : IEndpointAddressScheme<RouteValuesAddress>
     {
-        private readonly CompositeEndpointDataSource _dataSource;
+        private readonly EndpointDataSource _dataSource;
         private LinkGenerationDecisionTree _allMatchesLinkGenerationTree;
         private Dictionary<string, List<OutboundMatchResult>> _namedMatchResults;
 
-        public RouteValuesAddressScheme(CompositeEndpointDataSource dataSource)
+        public RouteValuesAddressScheme(EndpointDataSource dataSource)
         {
             _dataSource = dataSource;
 
@@ -125,12 +125,21 @@ namespace Microsoft.AspNetCore.Routing
                     continue;
                 }
 
+                var metadata = endpoint.Metadata.GetMetadata<IRouteValuesAddressMetadata>();
+                if (metadata == null && routeEndpoint.RoutePattern.RequiredValues.Count == 0)
+                {
+                    continue;
+                }
+
                 if (endpoint.Metadata.GetMetadata<ISuppressLinkGenerationMetadata>()?.SuppressLinkGeneration == true)
                 {
                     continue;
                 }
 
-                var entry = CreateOutboundRouteEntry(routeEndpoint);
+                var entry = CreateOutboundRouteEntry(
+                    routeEndpoint, 
+                    metadata?.RequiredValues ?? routeEndpoint.RoutePattern.RequiredValues, 
+                    metadata?.RouteName);
 
                 var outboundMatch = new OutboundMatch() { Entry = entry };
                 allOutboundMatches.Add(outboundMatch);
@@ -151,18 +160,20 @@ namespace Microsoft.AspNetCore.Routing
             return (allOutboundMatches, namedOutboundMatchResults);
         }
 
-        private OutboundRouteEntry CreateOutboundRouteEntry(RouteEndpoint endpoint)
+        private OutboundRouteEntry CreateOutboundRouteEntry(
+            RouteEndpoint endpoint, 
+            IReadOnlyDictionary<string, object> requiredValues,
+            string routeName)
         {
-            var routeValuesAddressMetadata = endpoint.Metadata.GetMetadata<IRouteValuesAddressMetadata>();
             var entry = new OutboundRouteEntry()
             {
                 Handler = NullRouter.Instance,
                 Order = endpoint.Order,
                 Precedence = RoutePrecedence.ComputeOutbound(endpoint.RoutePattern),
-                RequiredLinkValues = new RouteValueDictionary(routeValuesAddressMetadata?.RequiredValues),
+                RequiredLinkValues = new RouteValueDictionary(requiredValues),
                 RouteTemplate = new RouteTemplate(endpoint.RoutePattern),
                 Data = endpoint,
-                RouteName = routeValuesAddressMetadata?.RouteName,
+                RouteName = routeName,
             };
             entry.Defaults = new RouteValueDictionary(endpoint.RoutePattern.Defaults);
             return entry;

+ 17 - 0
src/Routing/src/Microsoft.AspNetCore.Routing/Template/RoutePrecedence.cs

@@ -20,6 +20,8 @@ namespace Microsoft.AspNetCore.Routing.Template
         //       /api/template/{id:int} == 1.12
         public static decimal ComputeInbound(RouteTemplate template)
         {
+            ValidateSegementLength(template.Segments.Count);
+
             // 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;
@@ -40,6 +42,8 @@ namespace Microsoft.AspNetCore.Routing.Template
         // See description on ComputeInbound(RouteTemplate)
         internal static decimal ComputeInbound(RoutePattern routePattern)
         {
+            ValidateSegementLength(routePattern.PathSegments.Count);
+
             var precedence = 0m;
 
             for (var i = 0; i < routePattern.PathSegments.Count; i++)
@@ -62,6 +66,8 @@ namespace Microsoft.AspNetCore.Routing.Template
         //       /api/template/{id:int} == 5.54
         public static decimal ComputeOutbound(RouteTemplate template)
         {
+            ValidateSegementLength(template.Segments.Count);
+
             // 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;
@@ -82,6 +88,8 @@ namespace Microsoft.AspNetCore.Routing.Template
         // see description on ComputeOutbound(RouteTemplate)
         internal static decimal ComputeOutbound(RoutePattern routePattern)
         {
+            ValidateSegementLength(routePattern.PathSegments.Count);
+
             // 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;
@@ -99,6 +107,15 @@ namespace Microsoft.AspNetCore.Routing.Template
             return precedence;
         }
 
+        private static void ValidateSegementLength(int length)
+        {
+            if (length > 28)
+            {
+                // An OverflowException will be thrown by Math.Pow when greater than 28
+                throw new InvalidOperationException("Route exceeds the maximum number of allowed segments of 28 and is unable to be processed.");
+            }
+        }
+
         // Segments have the following order:
         // 5 - Literal segments
         // 4 - Multi-part segments && Constrained parameter segments

+ 7 - 0
src/Routing/src/Microsoft.AspNetCore.Routing/Template/RouteTemplate.cs

@@ -16,6 +16,13 @@ namespace Microsoft.AspNetCore.Routing.Template
 
         public RouteTemplate(RoutePattern other)
         {
+            if (other == null)
+            {
+                throw new ArgumentNullException(nameof(other));
+            }
+
+            // RequiredValues will be ignored. RouteTemplate doesn't support them.
+
             TemplateText = other.RawText;
             Segments = new List<TemplateSegment>(other.PathSegments.Select(p => new TemplateSegment(p)));
             Parameters = new List<TemplatePart>();

+ 0 - 7
src/Routing/test/Directory.Build.props

@@ -1,13 +1,6 @@
 <Project>
   <Import Project="..\Directory.Build.props" />
 
-  <PropertyGroup>
-    <DeveloperBuildTestTfms>netcoreapp2.2</DeveloperBuildTestTfms>
-    <StandardTestTfms>$(DeveloperBuildTestTfms)</StandardTestTfms>
-    <StandardTestTfms Condition=" '$(DeveloperBuild)' != 'true' ">$(StandardTestTfms)</StandardTestTfms>
-    <StandardTestTfms Condition=" '$(DeveloperBuild)' != 'true' AND '$(OS)' == 'Windows_NT' ">$(StandardTestTfms);net461</StandardTestTfms>
-  </PropertyGroup>
-
   <PropertyGroup>
     <IsPackable>false</IsPackable>
   </PropertyGroup>

+ 0 - 142
src/Routing/test/Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests/EndpointMetadataCollectionTests.cs

@@ -1,142 +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.Text;
-using Microsoft.AspNetCore.Http;
-using Xunit;
-
-namespace Microsoft.AspNetCore.Routing
-{
-    public class EndpointMetadataCollectionTests
-    {
-        [Fact]
-        public void Constructor_Enumeration_ContainsValues()
-        {
-            // Arrange & Act
-            var metadata = new EndpointMetadataCollection(new List<object>
-            {
-                1,
-                2,
-                3,
-            });
-
-            // Assert
-            Assert.Equal(3, metadata.Count);
-
-            Assert.Collection(metadata,
-                value => Assert.Equal(1, value),
-                value => Assert.Equal(2, value),
-                value => Assert.Equal(3, value));
-        }
-
-        [Fact]
-        public void Constructor_ParamsArray_ContainsValues()
-        {
-            // Arrange & Act
-            var metadata = new EndpointMetadataCollection(1, 2, 3);
-
-            // Assert
-            Assert.Equal(3, metadata.Count);
-
-            Assert.Collection(metadata,
-                value => Assert.Equal(1, value),
-                value => Assert.Equal(2, value),
-                value => Assert.Equal(3, value));
-        }
-
-        [Fact]
-        public void GetMetadata_Match_ReturnsLastMatchingEntry()
-        {
-            // Arrange
-            var items = new object[]
-            {
-                new Metadata1(),
-                new Metadata2(),
-                new Metadata3(),
-            };
-
-            var metadata = new EndpointMetadataCollection(items);
-
-            // Act
-            var result = metadata.GetMetadata<IMetadata5>();
-
-            // Assert
-            Assert.Same(items[1], result);
-        }
-
-        [Fact]
-        public void GetMetadata_NoMatch_ReturnsNull()
-        {
-            // Arrange
-            var items = new object[]
-            {
-                new Metadata3(),
-                new Metadata3(),
-                new Metadata3(),
-            };
-
-            var metadata = new EndpointMetadataCollection(items);
-
-            // Act
-            var result = metadata.GetMetadata<IMetadata5>();
-
-            // Assert
-            Assert.Null(result);
-        }
-
-        [Fact]
-        public void GetOrderedMetadata_Match_ReturnsItemsInAscendingOrder()
-        {
-            // Arrange
-            var items = new object[]
-            {
-                new Metadata1(),
-                new Metadata2(),
-                new Metadata3(),
-            };
-
-            var metadata = new EndpointMetadataCollection(items);
-
-            // Act
-            var result = metadata.GetOrderedMetadata<IMetadata5>();
-
-            // Assert
-            Assert.Collection(
-                result,
-                i => Assert.Same(items[0], i),
-                i => Assert.Same(items[1], i));
-        }
-
-        [Fact]
-        public void GetOrderedMetadata_NoMatch_ReturnsEmpty()
-        {
-            // Arrange
-            var items = new object[]
-            {
-                new Metadata3(),
-                new Metadata3(),
-                new Metadata3(),
-            };
-
-            var metadata = new EndpointMetadataCollection(items);
-
-            // Act
-            var result = metadata.GetOrderedMetadata<IMetadata5>();
-
-            // Assert
-            Assert.Empty(result);
-        }
-
-        private interface IMetadata1 { }
-        private interface IMetadata2 { }
-        private interface IMetadata3 { }
-        private interface IMetadata4 { }
-        private interface IMetadata5 { }
-        private class Metadata1 : IMetadata1, IMetadata4, IMetadata5 { }
-        private class Metadata2 : IMetadata2, IMetadata5 { }
-        private class Metadata3 : IMetadata3 { }
-
-    }
-}

+ 1 - 1
src/Routing/test/Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests/Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests.csproj

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
+    <TargetFramework>netcoreapp3.0</TargetFramework>
   </PropertyGroup>
 
   <ItemGroup>

+ 0 - 2063
src/Routing/test/Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests/RouteValueDictionaryTests.cs

@@ -1,2063 +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.Testing;
-using Xunit;
-
-namespace Microsoft.AspNetCore.Routing.Tests
-{
-    public class RouteValueDictionaryTests
-    {
-        [Fact]
-        public void DefaultCtor_UsesEmptyStorage()
-        {
-            // Arrange
-            // Act
-            var dict = new RouteValueDictionary();
-
-            // Assert
-            Assert.Empty(dict);
-            Assert.Empty(dict._arrayStorage);
-            Assert.Null(dict._propertyStorage);
-        }
-
-        [Fact]
-        public void CreateFromNull_UsesEmptyStorage()
-        {
-            // Arrange
-            // Act
-            var dict = new RouteValueDictionary(null);
-
-            // Assert
-            Assert.Empty(dict);
-            Assert.Empty(dict._arrayStorage);
-            Assert.Null(dict._propertyStorage);
-        }
-
-        [Fact]
-        public void CreateFromRouteValueDictionary_WithArrayStorage_CopiesStorage()
-        {
-            // Arrange
-            var other = new RouteValueDictionary()
-            {
-                { "1", 1 }
-            };
-
-            // Act
-            var dict = new RouteValueDictionary(other);
-
-            // Assert
-            Assert.Equal(other, dict);
-
-            var storage = Assert.IsType<KeyValuePair<string, object>[]>(dict._arrayStorage);
-            var otherStorage = Assert.IsType<KeyValuePair<string, object>[]>(other._arrayStorage);
-            Assert.NotSame(otherStorage, storage);
-        }
-
-        [Fact]
-        public void CreateFromRouteValueDictionary_WithPropertyStorage_CopiesStorage()
-        {
-            // Arrange
-            var other = new RouteValueDictionary(new { key = "value" });
-
-            // Act
-            var dict = new RouteValueDictionary(other);
-
-            // Assert
-            Assert.Equal(other, dict);
-
-            var storage = dict._propertyStorage;
-            var otherStorage = other._propertyStorage;
-            Assert.Same(otherStorage, storage);
-        }
-
-        public static IEnumerable<object[]> IEnumerableKeyValuePairData
-        {
-            get
-            {
-                var routeValues = new[]
-                {
-                    new KeyValuePair<string, object>("Name", "James"),
-                    new KeyValuePair<string, object>("Age", 30),
-                    new KeyValuePair<string, object>("Address", new Address() { City = "Redmond", State = "WA" })
-                };
-
-                yield return new object[] { routeValues.ToDictionary(kvp => kvp.Key, kvp => kvp.Value) };
-
-                yield return new object[] { routeValues.ToList() };
-
-                yield return new object[] { routeValues };
-            }
-        }
-
-        public static IEnumerable<object[]> IEnumerableStringValuePairData
-        {
-            get
-            {
-                var routeValues = new[]
-                {
-                    new KeyValuePair<string, string>("First Name", "James"),
-                    new KeyValuePair<string, string>("Last Name", "Henrik"),
-                    new KeyValuePair<string, string>("Middle Name", "Bob")
-                };
-
-                yield return new object[] { routeValues.ToDictionary(kvp => kvp.Key, kvp => kvp.Value) };
-
-                yield return new object[] { routeValues.ToList() };
-
-                yield return new object[] { routeValues };
-            }
-        }
-
-        [Theory]
-        [MemberData(nameof(IEnumerableKeyValuePairData))]
-        public void CreateFromIEnumerableKeyValuePair_CopiesValues(object values)
-        {
-            // Arrange & Act
-            var dict = new RouteValueDictionary(values);
-
-            // Assert
-            Assert.IsType<KeyValuePair<string, object>[]>(dict._arrayStorage);
-            Assert.Collection(
-                dict.OrderBy(kvp => kvp.Key),
-                kvp =>
-                {
-                    Assert.Equal("Address", kvp.Key);
-                    var address = Assert.IsType<Address>(kvp.Value);
-                    Assert.Equal("Redmond", address.City);
-                    Assert.Equal("WA", address.State);
-                },
-                kvp => { Assert.Equal("Age", kvp.Key); Assert.Equal(30, kvp.Value); },
-                kvp => { Assert.Equal("Name", kvp.Key); Assert.Equal("James", kvp.Value); });
-        }
-
-        [Theory]
-        [MemberData(nameof(IEnumerableStringValuePairData))]
-        public void CreateFromIEnumerableStringValuePair_CopiesValues(object values)
-        {
-            // Arrange & Act
-            var dict = new RouteValueDictionary(values);
-
-            // Assert
-            Assert.IsType<KeyValuePair<string, object>[]>(dict._arrayStorage);
-            Assert.Collection(
-                dict.OrderBy(kvp => kvp.Key),
-                kvp => { Assert.Equal("First Name", kvp.Key); Assert.Equal("James", kvp.Value); },
-                kvp => { Assert.Equal("Last Name", kvp.Key); Assert.Equal("Henrik", kvp.Value); },
-                kvp => { Assert.Equal("Middle Name", kvp.Key); Assert.Equal("Bob", kvp.Value); });
-        }
-
-        [Fact]
-        public void CreateFromIEnumerableKeyValuePair_ThrowsExceptionForDuplicateKey()
-        {
-            // Arrange
-            var values = new List<KeyValuePair<string, object>>()
-            {
-                new KeyValuePair<string, object>("name", "Billy"),
-                new KeyValuePair<string, object>("Name", "Joey"),
-            };
-
-            // Act & Assert
-            ExceptionAssert.ThrowsArgument(
-                () => new RouteValueDictionary(values),
-                "key",
-                $"An element with the key 'Name' already exists in the {nameof(RouteValueDictionary)}.");
-        }
-
-        [Fact]
-        public void CreateFromIEnumerableStringValuePair_ThrowsExceptionForDuplicateKey()
-        {
-            // Arrange
-            var values = new List<KeyValuePair<string, string>>()
-            {
-                new KeyValuePair<string, string>("name", "Billy"),
-                new KeyValuePair<string, string>("Name", "Joey"),
-            };
-
-            // Act & Assert
-            ExceptionAssert.ThrowsArgument(
-                () => new RouteValueDictionary(values),
-                "key",
-                $"An element with the key 'Name' already exists in the {nameof(RouteValueDictionary)}.");
-        }
-
-        [Fact]
-        public void CreateFromObject_CopiesPropertiesFromAnonymousType()
-        {
-            // Arrange
-            var obj = new { cool = "beans", awesome = 123 };
-
-            // Act
-            var dict = new RouteValueDictionary(obj);
-
-            // Assert
-            Assert.NotNull(dict._propertyStorage);
-            Assert.Collection(
-                dict.OrderBy(kvp => kvp.Key),
-                kvp => { Assert.Equal("awesome", kvp.Key); Assert.Equal(123, kvp.Value); },
-                kvp => { Assert.Equal("cool", kvp.Key); Assert.Equal("beans", kvp.Value); });
-        }
-
-        [Fact]
-        public void CreateFromObject_CopiesPropertiesFromRegularType()
-        {
-            // Arrange
-            var obj = new RegularType() { CoolnessFactor = 73 };
-
-            // Act
-            var dict = new RouteValueDictionary(obj);
-
-            // Assert
-            Assert.NotNull(dict._propertyStorage);
-            Assert.Collection(
-                dict.OrderBy(kvp => kvp.Key),
-                kvp =>
-                {
-                    Assert.Equal("CoolnessFactor", kvp.Key);
-                    Assert.Equal(73, kvp.Value);
-                },
-                kvp =>
-                {
-                    Assert.Equal("IsAwesome", kvp.Key);
-                    var value = Assert.IsType<bool>(kvp.Value);
-                    Assert.False(value);
-                });
-        }
-
-        [Fact]
-        public void CreateFromObject_CopiesPropertiesFromRegularType_PublicOnly()
-        {
-            // Arrange
-            var obj = new Visibility() { IsPublic = true, ItsInternalDealWithIt = 5 };
-
-            // Act
-            var dict = new RouteValueDictionary(obj);
-
-            // Assert
-            Assert.NotNull(dict._propertyStorage);
-            Assert.Collection(
-                dict.OrderBy(kvp => kvp.Key),
-                kvp =>
-                {
-                    Assert.Equal("IsPublic", kvp.Key);
-                    var value = Assert.IsType<bool>(kvp.Value);
-                    Assert.True(value);
-                });
-        }
-
-        [Fact]
-        public void CreateFromObject_CopiesPropertiesFromRegularType_IgnoresStatic()
-        {
-            // Arrange
-            var obj = new StaticProperty();
-
-            // Act
-            var dict = new RouteValueDictionary(obj);
-
-            // Assert
-            Assert.NotNull(dict._propertyStorage);
-            Assert.Empty(dict);
-        }
-
-        [Fact]
-        public void CreateFromObject_CopiesPropertiesFromRegularType_IgnoresSetOnly()
-        {
-            // Arrange
-            var obj = new SetterOnly() { CoolSetOnly = false };
-
-            // Act
-            var dict = new RouteValueDictionary(obj);
-
-            // Assert
-            Assert.NotNull(dict._propertyStorage);
-            Assert.Empty(dict);
-        }
-
-        [Fact]
-        public void CreateFromObject_CopiesPropertiesFromRegularType_IncludesInherited()
-        {
-            // Arrange
-            var obj = new Derived() { TotallySweetProperty = true, DerivedProperty = false };
-
-            // Act
-            var dict = new RouteValueDictionary(obj);
-
-            // Assert
-            Assert.NotNull(dict._propertyStorage);
-            Assert.Collection(
-                dict.OrderBy(kvp => kvp.Key),
-                kvp =>
-                {
-                    Assert.Equal("DerivedProperty", kvp.Key);
-                    var value = Assert.IsType<bool>(kvp.Value);
-                    Assert.False(value);
-                },
-                kvp =>
-                {
-                    Assert.Equal("TotallySweetProperty", kvp.Key);
-                    var value = Assert.IsType<bool>(kvp.Value);
-                    Assert.True(value);
-                });
-        }
-
-        [Fact]
-        public void CreateFromObject_CopiesPropertiesFromRegularType_WithHiddenProperty()
-        {
-            // Arrange
-            var obj = new DerivedHiddenProperty() { DerivedProperty = 5 };
-
-            // Act
-            var dict = new RouteValueDictionary(obj);
-
-            // Assert
-            Assert.NotNull(dict._propertyStorage);
-            Assert.Collection(
-                dict.OrderBy(kvp => kvp.Key),
-                kvp => { Assert.Equal("DerivedProperty", kvp.Key); Assert.Equal(5, kvp.Value); });
-        }
-
-        [Fact]
-        public void CreateFromObject_CopiesPropertiesFromRegularType_WithIndexerProperty()
-        {
-            // Arrange
-            var obj = new IndexerProperty();
-
-            // Act
-            var dict = new RouteValueDictionary(obj);
-
-            // Assert
-            Assert.NotNull(dict._propertyStorage);
-            Assert.Empty(dict);
-        }
-
-        [Fact]
-        public void CreateFromObject_MixedCaseThrows()
-        {
-            // Arrange
-            var obj = new { controller = "Home", Controller = "Home" };
-
-            var message =
-                $"The type '{obj.GetType().FullName}' defines properties 'controller' and 'Controller' which differ " +
-                $"only by casing. This is not supported by {nameof(RouteValueDictionary)} which uses " +
-                $"case-insensitive comparisons.";
-
-            // Act & Assert
-            var exception = Assert.Throws<InvalidOperationException>(() =>
-            {
-                var dictionary = new RouteValueDictionary(obj);
-            });
-
-            // Ignoring case to make sure we're not testing reflection's ordering.
-            Assert.Equal(message, exception.Message, ignoreCase: true);
-        }
-
-        // Our comparer is hardcoded to be OrdinalIgnoreCase no matter what.
-        [Fact]
-        public void Comparer_IsOrdinalIgnoreCase()
-        {
-            // Arrange
-            // Act
-            var dict = new RouteValueDictionary();
-
-            // Assert
-            Assert.Same(StringComparer.OrdinalIgnoreCase, dict.Comparer);
-        }
-
-        // Our comparer is hardcoded to be IsReadOnly==false no matter what.
-        [Fact]
-        public void IsReadOnly_False()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary();
-
-            // Act
-            var result = ((ICollection<KeyValuePair<string, object>>)dict).IsReadOnly;
-
-            // Assert
-            Assert.False(result);
-        }
-
-        [Fact]
-        public void IndexGet_EmptyStringIsAllowed()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary();
-
-            // Act
-            var value = dict[""];
-
-            // Assert
-            Assert.Null(value);
-        }
-
-        [Fact]
-        public void IndexGet_EmptyStorage_ReturnsNull()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary();
-
-            // Act
-            var value = dict["key"];
-
-            // Assert
-            Assert.Null(value);
-        }
-
-        [Fact]
-        public void IndexGet_PropertyStorage_NoMatch_ReturnsNull()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary(new { age = 30 });
-
-            // Act
-            var value = dict["key"];
-
-            // Assert
-            Assert.Null(value);
-            Assert.NotNull(dict._propertyStorage);
-        }
-
-        [Fact]
-        public void IndexGet_PropertyStorage_Match_ReturnsValue()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary(new { key = "value" });
-
-            // Act
-            var value = dict["key"];
-
-            // Assert
-            Assert.Equal("value", value);
-            Assert.NotNull(dict._propertyStorage);
-        }
-
-        [Fact]
-        public void IndexGet_PropertyStorage_MatchIgnoreCase_ReturnsValue()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary(new { key = "value" });
-
-            // Act
-            var value = dict["kEy"];
-
-            // Assert
-            Assert.Equal("value", value);
-            Assert.NotNull(dict._propertyStorage);
-        }
-
-        [Fact]
-        public void IndexGet_ArrayStorage_NoMatch_ReturnsNull()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary()
-            {
-                { "age", 30 },
-            };
-
-            // Act
-            var value = dict["key"];
-
-            // Assert
-            Assert.Null(value);
-            Assert.IsType<KeyValuePair<string, object>[]>(dict._arrayStorage);
-        }
-
-        [Fact]
-        public void IndexGet_ListStorage_Match_ReturnsValue()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary()
-            {
-                { "key", "value" },
-            };
-
-            // Act
-            var value = dict["key"];
-
-            // Assert
-            Assert.Equal("value", value);
-            Assert.IsType<KeyValuePair<string, object>[]>(dict._arrayStorage);
-        }
-
-        [Fact]
-        public void IndexGet_ListStorage_MatchIgnoreCase_ReturnsValue()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary()
-            {
-                { "key", "value" },
-            };
-
-            // Act
-            var value = dict["kEy"];
-
-            // Assert
-            Assert.Equal("value", value);
-            Assert.IsType<KeyValuePair<string, object>[]>(dict._arrayStorage);
-        }
-
-        [Fact]
-        public void IndexSet_EmptyStringIsAllowed()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary();
-
-            // Act
-            dict[""] = "foo";
-
-            // Assert
-            Assert.Equal("foo", dict[""]);
-        }
-
-        [Fact]
-        public void IndexSet_EmptyStorage_UpgradesToList()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary();
-
-            // Act
-            dict["key"] = "value";
-
-            // Assert
-            Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); });
-            Assert.IsType<KeyValuePair<string, object>[]>(dict._arrayStorage);
-        }
-
-        [Fact]
-        public void IndexSet_PropertyStorage_NoMatch_AddsValue()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary(new { age = 30 });
-
-            // Act
-            dict["key"] = "value";
-
-            // Assert
-            Assert.Collection(
-                dict.OrderBy(kvp => kvp.Key),
-                kvp => { Assert.Equal("age", kvp.Key); Assert.Equal(30, kvp.Value); },
-                kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); });
-            Assert.IsType<KeyValuePair<string, object>[]>(dict._arrayStorage);
-        }
-
-        [Fact]
-        public void IndexSet_PropertyStorage_Match_SetsValue()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary(new { key = "value" });
-
-            // Act
-            dict["key"] = "value";
-
-            // Assert
-            Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); });
-            Assert.IsType<KeyValuePair<string, object>[]>(dict._arrayStorage);
-        }
-
-        [Fact]
-        public void IndexSet_PropertyStorage_MatchIgnoreCase_SetsValue()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary(new { key = "value" });
-
-            // Act
-            dict["kEy"] = "value";
-
-            // Assert
-            Assert.Collection(dict, kvp => { Assert.Equal("kEy", kvp.Key); Assert.Equal("value", kvp.Value); });
-            Assert.IsType<KeyValuePair<string, object>[]>(dict._arrayStorage);
-        }
-
-        [Fact]
-        public void IndexSet_ListStorage_NoMatch_AddsValue()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary()
-            {
-                { "age", 30 },
-            };
-
-            // Act
-            dict["key"] = "value";
-
-            // Assert
-            Assert.Collection(
-                dict.OrderBy(kvp => kvp.Key),
-                kvp => { Assert.Equal("age", kvp.Key); Assert.Equal(30, kvp.Value); },
-                kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); });
-            Assert.IsType<KeyValuePair<string, object>[]>(dict._arrayStorage);
-        }
-
-        [Fact]
-        public void IndexSet_ListStorage_Match_SetsValue()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary()
-            {
-                { "key", "value" },
-            };
-
-            // Act
-            dict["key"] = "value";
-
-            // Assert
-            Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); });
-            Assert.IsType<KeyValuePair<string, object>[]>(dict._arrayStorage);
-        }
-
-        [Fact]
-        public void IndexSet_ListStorage_MatchIgnoreCase_SetsValue()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary()
-            {
-                { "key", "value" },
-            };
-
-            // Act
-            dict["key"] = "value";
-
-            // Assert
-            Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); });
-            Assert.IsType<KeyValuePair<string, object>[]>(dict._arrayStorage);
-        }
-
-        [Fact]
-        public void Count_EmptyStorage()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary();
-
-            // Act
-            var count = dict.Count;
-
-            // Assert
-            Assert.Equal(0, count);
-        }
-
-        [Fact]
-        public void Count_PropertyStorage()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary(new { key = "value", });
-
-            // Act
-            var count = dict.Count;
-
-            // Assert
-            Assert.Equal(1, count);
-            Assert.NotNull(dict._propertyStorage);
-        }
-
-        [Fact]
-        public void Count_ListStorage()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary()
-            {
-                { "key", "value" },
-            };
-
-            // Act
-            var count = dict.Count;
-
-            // Assert
-            Assert.Equal(1, count);
-            Assert.IsType<KeyValuePair<string, object>[]>(dict._arrayStorage);
-        }
-
-        [Fact]
-        public void Keys_EmptyStorage()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary();
-
-            // Act
-            var keys = dict.Keys;
-
-            // Assert
-            Assert.Empty(keys);
-            Assert.IsType<KeyValuePair<string, object>[]>(dict._arrayStorage);
-        }
-
-        [Fact]
-        public void Keys_PropertyStorage()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary(new { key = "value", });
-
-            // Act
-            var keys = dict.Keys;
-
-            // Assert
-            Assert.Equal(new[] { "key" }, keys);
-            Assert.IsType<KeyValuePair<string, object>[]>(dict._arrayStorage);
-        }
-
-        [Fact]
-        public void Keys_ListStorage()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary()
-            {
-                { "key", "value" },
-            };
-
-            // Act
-            var keys = dict.Keys;
-
-            // Assert
-            Assert.Equal(new[] { "key" }, keys);
-            Assert.IsType<KeyValuePair<string, object>[]>(dict._arrayStorage);
-        }
-
-        [Fact]
-        public void Values_EmptyStorage()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary();
-
-            // Act
-            var values = dict.Values;
-
-            // Assert
-            Assert.Empty(values);
-            Assert.IsType<KeyValuePair<string, object>[]>(dict._arrayStorage);
-        }
-
-        [Fact]
-        public void Values_PropertyStorage()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary(new { key = "value", });
-
-            // Act
-            var values = dict.Values;
-
-            // Assert
-            Assert.Equal(new object[] { "value" }, values);
-            Assert.IsType<KeyValuePair<string, object>[]>(dict._arrayStorage);
-        }
-
-        [Fact]
-        public void Values_ListStorage()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary()
-            {
-                { "key", "value" },
-            };
-
-            // Act
-            var values = dict.Values;
-
-            // Assert
-            Assert.Equal(new object[] { "value" }, values);
-            Assert.IsType<KeyValuePair<string, object>[]>(dict._arrayStorage);
-        }
-
-        [Fact]
-        public void Add_EmptyStorage()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary();
-
-            // Act
-            dict.Add("key", "value");
-
-            // Assert
-            Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); });
-            Assert.IsType<KeyValuePair<string, object>[]>(dict._arrayStorage);
-        }
-
-        [Fact]
-        public void Add_EmptyStringIsAllowed()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary();
-
-            // Act
-            dict.Add("", "foo");
-
-            // Assert
-            Assert.Equal("foo", dict[""]);
-        }
-
-        [Fact]
-        public void Add_PropertyStorage()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary(new { age = 30 });
-
-            // Act
-            dict.Add("key", "value");
-
-            // Assert
-            Assert.Collection(
-                dict.OrderBy(kvp => kvp.Key),
-                kvp => { Assert.Equal("age", kvp.Key); Assert.Equal(30, kvp.Value); },
-                kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); });
-            Assert.IsType<KeyValuePair<string, object>[]>(dict._arrayStorage);
-
-            // The upgrade from property -> array should make space for at least 4 entries
-            Assert.Collection(
-                dict._arrayStorage,
-                kvp => Assert.Equal(new KeyValuePair<string, object>("age", 30), kvp),
-                kvp => Assert.Equal(new KeyValuePair<string, object>("key", "value"), kvp),
-                kvp => Assert.Equal(default, kvp),
-                kvp => Assert.Equal(default, kvp));
-        }
-
-        [Fact]
-        public void Add_ListStorage()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary()
-            {
-                { "age", 30 },
-            };
-
-            // Act
-            dict.Add("key", "value");
-
-            // Assert
-            Assert.Collection(
-                dict.OrderBy(kvp => kvp.Key),
-                kvp => { Assert.Equal("age", kvp.Key); Assert.Equal(30, kvp.Value); },
-                kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); });
-            Assert.IsType<KeyValuePair<string, object>[]>(dict._arrayStorage);
-        }
-
-        [Fact]
-        public void Add_DuplicateKey()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary()
-            {
-                { "key", "value" },
-            };
-
-            var message = $"An element with the key 'key' already exists in the {nameof(RouteValueDictionary)}";
-
-            // Act & Assert
-            ExceptionAssert.ThrowsArgument(() => dict.Add("key", "value2"), "key", message);
-
-            // Assert
-            Assert.Collection(
-                dict.OrderBy(kvp => kvp.Key),
-                kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); });
-            Assert.IsType<KeyValuePair<string, object>[]>(dict._arrayStorage);
-        }
-
-        [Fact]
-        public void Add_DuplicateKey_CaseInsensitive()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary()
-            {
-                { "key", "value" },
-            };
-
-            var message = $"An element with the key 'kEy' already exists in the {nameof(RouteValueDictionary)}";
-
-            // Act & Assert
-            ExceptionAssert.ThrowsArgument(() => dict.Add("kEy", "value2"), "key", message);
-
-            // Assert
-            Assert.Collection(
-                dict.OrderBy(kvp => kvp.Key),
-                kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); });
-            Assert.IsType<KeyValuePair<string, object>[]>(dict._arrayStorage);
-        }
-
-        [Fact]
-        public void Add_KeyValuePair()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary()
-            {
-                { "age", 30 },
-            };
-
-            // Act
-            ((ICollection<KeyValuePair<string, object>>)dict).Add(new KeyValuePair<string, object>("key", "value"));
-
-            // Assert
-            Assert.Collection(
-                dict.OrderBy(kvp => kvp.Key),
-                kvp => { Assert.Equal("age", kvp.Key); Assert.Equal(30, kvp.Value); },
-                kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); });
-            Assert.IsType<KeyValuePair<string, object>[]>(dict._arrayStorage);
-        }
-
-        [Fact]
-        public void Clear_EmptyStorage()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary();
-
-            // Act
-            dict.Clear();
-
-            // Assert
-            Assert.Empty(dict);
-        }
-
-        [Fact]
-        public void Clear_PropertyStorage_AlreadyEmpty()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary(new { });
-
-            // Act
-            dict.Clear();
-
-            // Assert
-            Assert.Empty(dict);
-            Assert.NotNull(dict._propertyStorage);
-        }
-
-        [Fact]
-        public void Clear_PropertyStorage()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary(new { key = "value" });
-
-            // Act
-            dict.Clear();
-
-            // Assert
-            Assert.Empty(dict);
-            Assert.Null(dict._propertyStorage);
-        }
-
-        [Fact]
-        public void Clear_ListStorage()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary()
-            {
-                { "key", "value" },
-            };
-
-            // Act
-            dict.Clear();
-
-            // Assert
-            Assert.Empty(dict);
-            Assert.IsType<KeyValuePair<string, object>[]>(dict._arrayStorage);
-        }
-
-        [Fact]
-        public void Contains_KeyValuePair_True()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary()
-            {
-                { "key", "value" },
-            };
-
-            var input = new KeyValuePair<string, object>("key", "value");
-
-            // Act
-            var result = ((ICollection<KeyValuePair<string, object>>)dict).Contains(input);
-
-            // Assert
-            Assert.True(result);
-            Assert.IsType<KeyValuePair<string, object>[]>(dict._arrayStorage);
-        }
-
-        [Fact]
-        public void Contains_KeyValuePair_True_CaseInsensitive()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary()
-            {
-                { "key", "value" },
-            };
-
-            var input = new KeyValuePair<string, object>("KEY", "value");
-
-            // Act
-            var result = ((ICollection<KeyValuePair<string, object>>)dict).Contains(input);
-
-            // Assert
-            Assert.True(result);
-            Assert.IsType<KeyValuePair<string, object>[]>(dict._arrayStorage);
-        }
-
-        [Fact]
-        public void Contains_KeyValuePair_False()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary()
-            {
-                { "key", "value" },
-            };
-
-            var input = new KeyValuePair<string, object>("other", "value");
-
-            // Act
-            var result = ((ICollection<KeyValuePair<string, object>>)dict).Contains(input);
-
-            // Assert
-            Assert.False(result);
-            Assert.IsType<KeyValuePair<string, object>[]>(dict._arrayStorage);
-        }
-
-        // Value comparisons use the default equality comparer.
-        [Fact]
-        public void Contains_KeyValuePair_False_ValueComparisonIsDefault()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary()
-            {
-                { "key", "value" },
-            };
-
-            var input = new KeyValuePair<string, object>("key", "valUE");
-
-            // Act
-            var result = ((ICollection<KeyValuePair<string, object>>)dict).Contains(input);
-
-            // Assert
-            Assert.False(result);
-            Assert.IsType<KeyValuePair<string, object>[]>(dict._arrayStorage);
-        }
-
-        [Fact]
-        public void ContainsKey_EmptyStorage()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary();
-
-            // Act
-            var result = dict.ContainsKey("key");
-
-            // Assert
-            Assert.False(result);
-        }
-
-        [Fact]
-        public void ContainsKey_EmptyStringIsAllowed()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary();
-
-            // Act
-            var result = dict.ContainsKey("");
-
-            // Assert
-            Assert.False(result);
-        }
-
-        [Fact]
-        public void ContainsKey_PropertyStorage_False()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary(new { key = "value" });
-
-            // Act
-            var result = dict.ContainsKey("other");
-
-            // Assert
-            Assert.False(result);
-            Assert.NotNull(dict._propertyStorage);
-        }
-
-        [Fact]
-        public void ContainsKey_PropertyStorage_True()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary(new { key = "value" });
-
-            // Act
-            var result = dict.ContainsKey("key");
-
-            // Assert
-            Assert.True(result);
-            Assert.NotNull(dict._propertyStorage);
-        }
-
-        [Fact]
-        public void ContainsKey_PropertyStorage_True_CaseInsensitive()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary(new { key = "value" });
-
-            // Act
-            var result = dict.ContainsKey("kEy");
-
-            // Assert
-            Assert.True(result);
-            Assert.NotNull(dict._propertyStorage);
-        }
-
-        [Fact]
-        public void ContainsKey_ListStorage_False()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary()
-            {
-                { "key", "value" },
-            };
-
-            // Act
-            var result = dict.ContainsKey("other");
-
-            // Assert
-            Assert.False(result);
-            Assert.IsType<KeyValuePair<string, object>[]>(dict._arrayStorage);
-        }
-
-        [Fact]
-        public void ContainsKey_ListStorage_True()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary()
-            {
-                { "key", "value" },
-            };
-
-            // Act
-            var result = dict.ContainsKey("key");
-
-            // Assert
-            Assert.True(result);
-            Assert.IsType<KeyValuePair<string, object>[]>(dict._arrayStorage);
-        }
-
-        [Fact]
-        public void ContainsKey_ListStorage_True_CaseInsensitive()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary()
-            {
-                { "key", "value" },
-            };
-
-            // Act
-            var result = dict.ContainsKey("kEy");
-
-            // Assert
-            Assert.True(result);
-            Assert.IsType<KeyValuePair<string, object>[]>(dict._arrayStorage);
-        }
-
-        [Fact]
-        public void CopyTo()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary()
-            {
-                { "key", "value" },
-            };
-
-            var array = new KeyValuePair<string, object>[2];
-
-            // Act
-            ((ICollection<KeyValuePair<string, object>>)dict).CopyTo(array, 1);
-
-            // Assert
-            Assert.Equal(
-                new KeyValuePair<string, object>[]
-                {
-                    default(KeyValuePair<string, object>),
-                    new KeyValuePair<string, object>("key", "value")
-                },
-                array);
-            Assert.IsType<KeyValuePair<string, object>[]>(dict._arrayStorage);
-        }
-
-        [Fact]
-        public void Remove_KeyValuePair_True()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary()
-            {
-                { "key", "value" },
-            };
-
-            var input = new KeyValuePair<string, object>("key", "value");
-
-            // Act
-            var result = ((ICollection<KeyValuePair<string, object>>)dict).Remove(input);
-
-            // Assert
-            Assert.True(result);
-            Assert.Empty(dict);
-            Assert.IsType<KeyValuePair<string, object>[]>(dict._arrayStorage);
-        }
-
-        [Fact]
-        public void Remove_KeyValuePair_True_CaseInsensitive()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary()
-            {
-                { "key", "value" },
-            };
-
-            var input = new KeyValuePair<string, object>("KEY", "value");
-
-            // Act
-            var result = ((ICollection<KeyValuePair<string, object>>)dict).Remove(input);
-
-            // Assert
-            Assert.True(result);
-            Assert.Empty(dict);
-            Assert.IsType<KeyValuePair<string, object>[]>(dict._arrayStorage);
-        }
-
-        [Fact]
-        public void Remove_KeyValuePair_False()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary()
-            {
-                { "key", "value" },
-            };
-
-            var input = new KeyValuePair<string, object>("other", "value");
-
-            // Act
-            var result = ((ICollection<KeyValuePair<string, object>>)dict).Remove(input);
-
-            // Assert
-            Assert.False(result);
-            Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); });
-            Assert.IsType<KeyValuePair<string, object>[]>(dict._arrayStorage);
-        }
-
-        // Value comparisons use the default equality comparer.
-        [Fact]
-        public void Remove_KeyValuePair_False_ValueComparisonIsDefault()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary()
-            {
-                { "key", "value" },
-            };
-
-            var input = new KeyValuePair<string, object>("key", "valUE");
-
-            // Act
-            var result = ((ICollection<KeyValuePair<string, object>>)dict).Remove(input);
-
-            // Assert
-            Assert.False(result);
-            Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); });
-            Assert.IsType<KeyValuePair<string, object>[]>(dict._arrayStorage);
-        }
-
-        [Fact]
-        public void Remove_EmptyStorage()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary();
-
-            // Act
-            var result = dict.Remove("key");
-
-            // Assert
-            Assert.False(result);
-        }
-
-        [Fact]
-        public void Remove_EmptyStringIsAllowed()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary();
-
-            // Act
-            var result = dict.Remove("");
-
-            // Assert
-            Assert.False(result);
-        }
-
-        [Fact]
-        public void Remove_PropertyStorage_Empty()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary(new { });
-
-            // Act
-            var result = dict.Remove("other");
-
-            // Assert
-            Assert.False(result);
-            Assert.Empty(dict);
-            Assert.NotNull(dict._propertyStorage);
-        }
-
-        [Fact]
-        public void Remove_PropertyStorage_False()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary(new { key = "value" });
-
-            // Act
-            var result = dict.Remove("other");
-
-            // Assert
-            Assert.False(result);
-            Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); });
-            Assert.IsType<KeyValuePair<string, object>[]>(dict._arrayStorage);
-        }
-
-        [Fact]
-        public void Remove_PropertyStorage_True()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary(new { key = "value" });
-
-            // Act
-            var result = dict.Remove("key");
-
-            // Assert
-            Assert.True(result);
-            Assert.Empty(dict);
-            Assert.IsType<KeyValuePair<string, object>[]>(dict._arrayStorage);
-        }
-
-        [Fact]
-        public void Remove_PropertyStorage_True_CaseInsensitive()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary(new { key = "value" });
-
-            // Act
-            var result = dict.Remove("kEy");
-
-            // Assert
-            Assert.True(result);
-            Assert.Empty(dict);
-            Assert.IsType<KeyValuePair<string, object>[]>(dict._arrayStorage);
-        }
-
-        [Fact]
-        public void Remove_ListStorage_False()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary()
-            {
-                { "key", "value" },
-            };
-
-            // Act
-            var result = dict.Remove("other");
-
-            // Assert
-            Assert.False(result);
-            Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); });
-            Assert.IsType<KeyValuePair<string, object>[]>(dict._arrayStorage);
-        }
-
-        [Fact]
-        public void Remove_ListStorage_True()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary()
-            {
-                { "key", "value" },
-            };
-
-            // Act
-            var result = dict.Remove("key");
-
-            // Assert
-            Assert.True(result);
-            Assert.Empty(dict);
-            Assert.IsType<KeyValuePair<string, object>[]>(dict._arrayStorage);
-        }
-
-        [Fact]
-        public void Remove_ListStorage_True_CaseInsensitive()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary()
-            {
-                { "key", "value" },
-            };
-
-            // Act
-            var result = dict.Remove("kEy");
-
-            // Assert
-            Assert.True(result);
-            Assert.Empty(dict);
-            Assert.IsType<KeyValuePair<string, object>[]>(dict._arrayStorage);
-        }
-
-
-        [Fact]
-        public void Remove_KeyAndOutValue_EmptyStorage()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary();
-
-            // Act
-            var result = dict.Remove("key", out var removedValue);
-
-            // Assert
-            Assert.False(result);
-            Assert.Null(removedValue);
-        }
-
-        [Fact]
-        public void Remove_KeyAndOutValue_EmptyStringIsAllowed()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary();
-
-            // Act
-            var result = dict.Remove("", out var removedValue);
-
-            // Assert
-            Assert.False(result);
-            Assert.Null(removedValue);
-        }
-
-        [Fact]
-        public void Remove_KeyAndOutValue_PropertyStorage_Empty()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary(new { });
-
-            // Act
-            var result = dict.Remove("other", out var removedValue);
-
-            // Assert
-            Assert.False(result);
-            Assert.Null(removedValue);
-            Assert.Empty(dict);
-            Assert.NotNull(dict._propertyStorage);
-        }
-
-        [Fact]
-        public void Remove_KeyAndOutValue_PropertyStorage_False()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary(new { key = "value" });
-
-            // Act
-            var result = dict.Remove("other", out var removedValue);
-
-            // Assert
-            Assert.False(result);
-            Assert.Null(removedValue);
-            Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); });
-            Assert.IsType<KeyValuePair<string, object>[]>(dict._arrayStorage);
-        }
-
-        [Fact]
-        public void Remove_KeyAndOutValue_PropertyStorage_True()
-        {
-            // Arrange
-            object value = "value";
-            var dict = new RouteValueDictionary(new { key = value });
-
-            // Act
-            var result = dict.Remove("key", out var removedValue);
-
-            // Assert
-            Assert.True(result);
-            Assert.Same(value, removedValue);
-            Assert.Empty(dict);
-            Assert.IsType<KeyValuePair<string, object>[]>(dict._arrayStorage);
-        }
-
-        [Fact]
-        public void Remove_KeyAndOutValue_PropertyStorage_True_CaseInsensitive()
-        {
-            // Arrange
-            object value = "value";
-            var dict = new RouteValueDictionary(new { key = value });
-
-            // Act
-            var result = dict.Remove("kEy", out var removedValue);
-
-            // Assert
-            Assert.True(result);
-            Assert.Same(value, removedValue);
-            Assert.Empty(dict);
-            Assert.IsType<KeyValuePair<string, object>[]>(dict._arrayStorage);
-        }
-
-        [Fact]
-        public void Remove_KeyAndOutValue_ListStorage_False()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary()
-            {
-                { "key", "value" },
-            };
-
-            // Act
-            var result = dict.Remove("other", out var removedValue);
-
-            // Assert
-            Assert.False(result);
-            Assert.Null(removedValue);
-            Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); });
-            Assert.IsType<KeyValuePair<string, object>[]>(dict._arrayStorage);
-        }
-
-        [Fact]
-        public void Remove_KeyAndOutValue_ListStorage_True()
-        {
-            // Arrange
-            object value = "value";
-            var dict = new RouteValueDictionary()
-            {
-                { "key", value }
-            };
-
-            // Act
-            var result = dict.Remove("key", out var removedValue);
-
-            // Assert
-            Assert.True(result);
-            Assert.Same(value, removedValue);
-            Assert.Empty(dict);
-            Assert.IsType<KeyValuePair<string, object>[]>(dict._arrayStorage);
-        }
-
-        [Fact]
-        public void Remove_KeyAndOutValue_ListStorage_True_CaseInsensitive()
-        {
-            // Arrange
-            object value = "value";
-            var dict = new RouteValueDictionary()
-            {
-                { "key", value }
-            };
-
-            // Act
-            var result = dict.Remove("kEy", out var removedValue);
-
-            // Assert
-            Assert.True(result);
-            Assert.Same(value, removedValue);
-            Assert.Empty(dict);
-            Assert.IsType<KeyValuePair<string, object>[]>(dict._arrayStorage);
-        }
-
-        [Fact]
-        public void Remove_KeyAndOutValue_ListStorage_KeyExists_First()
-        {
-            // Arrange
-            object value = "value";
-            var dict = new RouteValueDictionary()
-            {
-                { "key", value },
-                { "other", 5 },
-                { "dotnet", "rocks" }
-            };
-
-            // Act
-            var result = dict.Remove("key", out var removedValue);
-
-            // Assert
-            Assert.True(result);
-            Assert.Same(value, removedValue);
-            Assert.Equal(2, dict.Count);
-            Assert.False(dict.ContainsKey("key"));
-            Assert.True(dict.ContainsKey("other"));
-            Assert.True(dict.ContainsKey("dotnet"));
-            Assert.IsType<KeyValuePair<string, object>[]>(dict._arrayStorage);
-        }
-
-        [Fact]
-        public void Remove_KeyAndOutValue_ListStorage_KeyExists_Middle()
-        {
-            // Arrange
-            object value = "value";
-            var dict = new RouteValueDictionary()
-            {
-                { "other", 5 },
-                { "key", value },
-                { "dotnet", "rocks" }
-            };
-
-            // Act
-            var result = dict.Remove("key", out var removedValue);
-
-            // Assert
-            Assert.True(result);
-            Assert.Same(value, removedValue);
-            Assert.Equal(2, dict.Count);
-            Assert.False(dict.ContainsKey("key"));
-            Assert.True(dict.ContainsKey("other"));
-            Assert.True(dict.ContainsKey("dotnet"));
-            Assert.IsType<KeyValuePair<string, object>[]>(dict._arrayStorage);
-        }
-
-        [Fact]
-        public void Remove_KeyAndOutValue_ListStorage_KeyExists_Last()
-        {
-            // Arrange
-            object value = "value";
-            var dict = new RouteValueDictionary()
-            {
-                { "other", 5 },
-                { "dotnet", "rocks" },
-                { "key", value }
-            };
-
-            // Act
-            var result = dict.Remove("key", out var removedValue);
-
-            // Assert
-            Assert.True(result);
-            Assert.Same(value, removedValue);
-            Assert.Equal(2, dict.Count);
-            Assert.False(dict.ContainsKey("key"));
-            Assert.True(dict.ContainsKey("other"));
-            Assert.True(dict.ContainsKey("dotnet"));
-            Assert.IsType<KeyValuePair<string, object>[]>(dict._arrayStorage);
-        }
-
-        [Fact]
-        public void TryAdd_EmptyStringIsAllowed()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary();
-
-            // Act
-            var result = dict.TryAdd("", "foo");
-
-            // Assert
-            Assert.True(result);
-        }
-
-        // We always 'upgrade' if you are trying to write to the dictionary.
-        [Fact]
-        public void TryAdd_ConvertsPropertyStorage_ToArrayStorage()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary(new { key = "value", });
-
-            // Act
-            var result = dict.TryAdd("key", "value");
-
-            // Assert
-            Assert.False(result);
-            Assert.Null(dict._propertyStorage);
-            Assert.Collection(
-                dict._arrayStorage,
-                kvp => Assert.Equal(new KeyValuePair<string, object>("key", "value"), kvp),
-                kvp => Assert.Equal(default, kvp),
-                kvp => Assert.Equal(default, kvp),
-                kvp => Assert.Equal(default, kvp));
-        }
-
-        [Fact]
-        public void TryAdd_EmptyStorage_CanAdd()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary();
-
-            // Act
-            var result = dict.TryAdd("key", "value");
-
-            // Assert
-            Assert.True(result);
-            Assert.Collection(
-                dict._arrayStorage,
-                kvp => Assert.Equal(new KeyValuePair<string, object>("key", "value"), kvp),
-                kvp => Assert.Equal(default, kvp),
-                kvp => Assert.Equal(default, kvp),
-                kvp => Assert.Equal(default, kvp));
-        }
-
-        [Fact]
-        public void TryAdd_ArrayStorage_CanAdd()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary()
-            {
-                { "key0", "value0" },
-            };
-
-            // Act
-            var result = dict.TryAdd("key1", "value1");
-
-            // Assert
-            Assert.True(result);
-            Assert.Collection(
-                dict._arrayStorage,
-                kvp => Assert.Equal(new KeyValuePair<string, object>("key0", "value0"), kvp),
-                kvp => Assert.Equal(new KeyValuePair<string, object>("key1", "value1"), kvp),
-                kvp => Assert.Equal(default, kvp),
-                kvp => Assert.Equal(default, kvp));
-        }
-
-        [Fact]
-        public void TryAdd_ArrayStorage_CanAddWithResize()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary()
-            {
-                { "key0", "value0" },
-                { "key1", "value1" },
-                { "key2", "value2" },
-                { "key3", "value3" },
-            };
-
-            // Act
-            var result = dict.TryAdd("key4", "value4");
-
-            // Assert
-            Assert.True(result);
-            Assert.Collection(
-                dict._arrayStorage,
-                kvp => Assert.Equal(new KeyValuePair<string, object>("key0", "value0"), kvp),
-                kvp => Assert.Equal(new KeyValuePair<string, object>("key1", "value1"), kvp),
-                kvp => Assert.Equal(new KeyValuePair<string, object>("key2", "value2"), kvp),
-                kvp => Assert.Equal(new KeyValuePair<string, object>("key3", "value3"), kvp),
-                kvp => Assert.Equal(new KeyValuePair<string, object>("key4", "value4"), kvp),
-                kvp => Assert.Equal(default, kvp),
-                kvp => Assert.Equal(default, kvp),
-                kvp => Assert.Equal(default, kvp));
-        }
-
-        [Fact]
-        public void TryAdd_ArrayStorage_DoesNotAddWhenKeyIsPresent()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary()
-            {
-                { "key0", "value0" },
-            };
-
-            // Act
-            var result = dict.TryAdd("key0", "value1");
-
-            // Assert
-            Assert.False(result);
-            Assert.Collection(
-                dict._arrayStorage,
-                kvp => Assert.Equal(new KeyValuePair<string, object>("key0", "value0"), kvp),
-                kvp => Assert.Equal(default, kvp),
-                kvp => Assert.Equal(default, kvp),
-                kvp => Assert.Equal(default, kvp));
-        }
-
-        [Fact]
-        public void TryGetValue_EmptyStorage()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary();
-
-            // Act
-            object value;
-            var result = dict.TryGetValue("key", out value);
-
-            // Assert
-            Assert.False(result);
-            Assert.Null(value);
-        }
-
-        [Fact]
-        public void TryGetValue_EmptyStringIsAllowed()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary();
-
-            // Act
-            var result = dict.TryGetValue("", out var value);
-
-            // Assert
-            Assert.False(result);
-            Assert.Null(value);
-        }
-
-        [Fact]
-        public void TryGetValue_PropertyStorage_False()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary(new { key = "value" });
-
-            // Act
-            object value;
-            var result = dict.TryGetValue("other", out value);
-
-            // Assert
-            Assert.False(result);
-            Assert.Null(value);
-            Assert.NotNull(dict._propertyStorage);
-        }
-
-        [Fact]
-        public void TryGetValue_PropertyStorage_True()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary(new { key = "value" });
-
-            // Act
-            object value;
-            var result = dict.TryGetValue("key", out value);
-
-            // Assert
-            Assert.True(result);
-            Assert.Equal("value", value);
-            Assert.NotNull(dict._propertyStorage);
-        }
-
-        [Fact]
-        public void TryGetValue_PropertyStorage_True_CaseInsensitive()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary(new { key = "value" });
-
-            // Act
-            object value;
-            var result = dict.TryGetValue("kEy", out value);
-
-            // Assert
-            Assert.True(result);
-            Assert.Equal("value", value);
-            Assert.NotNull(dict._propertyStorage);
-        }
-
-        [Fact]
-        public void TryGetValue_ListStorage_False()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary()
-            {
-                { "key", "value" },
-            };
-
-            // Act
-            object value;
-            var result = dict.TryGetValue("other", out value);
-
-            // Assert
-            Assert.False(result);
-            Assert.Null(value);
-            Assert.IsType<KeyValuePair<string, object>[]>(dict._arrayStorage);
-        }
-
-        [Fact]
-        public void TryGetValue_ListStorage_True()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary()
-            {
-                { "key", "value" },
-            };
-
-            // Act
-            object value;
-            var result = dict.TryGetValue("key", out value);
-
-            // Assert
-            Assert.True(result);
-            Assert.Equal("value", value);
-            Assert.IsType<KeyValuePair<string, object>[]>(dict._arrayStorage);
-        }
-
-        [Fact]
-        public void TryGetValue_ListStorage_True_CaseInsensitive()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary()
-            {
-                { "key", "value" },
-            };
-
-            // Act
-            object value;
-            var result = dict.TryGetValue("kEy", out value);
-
-            // Assert
-            Assert.True(result);
-            Assert.Equal("value", value);
-            Assert.IsType<KeyValuePair<string, object>[]>(dict._arrayStorage);
-        }
-
-        [Fact]
-        public void ListStorage_DynamicallyAdjustsCapacity()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary();
-
-            // Act 1
-            dict.Add("key", "value");
-
-            // Assert 1
-            var storage = Assert.IsType<KeyValuePair<string, object>[]>(dict._arrayStorage);
-            Assert.Equal(4, storage.Length);
-
-            // Act 2
-            dict.Add("key2", "value2");
-            dict.Add("key3", "value3");
-            dict.Add("key4", "value4");
-            dict.Add("key5", "value5");
-
-            // Assert 2
-            storage = Assert.IsType<KeyValuePair<string, object>[]>(dict._arrayStorage);
-            Assert.Equal(8, storage.Length);
-        }
-
-        [Fact]
-        public void ListStorage_RemoveAt_RearrangesInnerArray()
-        {
-            // Arrange
-            var dict = new RouteValueDictionary();
-            dict.Add("key", "value");
-            dict.Add("key2", "value2");
-            dict.Add("key3", "value3");
-
-            // Assert 1
-            var storage = Assert.IsType<KeyValuePair<string, object>[]>(dict._arrayStorage);
-            Assert.Equal(3, dict.Count);
-
-            // Act
-            dict.Remove("key2");
-
-            // Assert 2
-            storage = Assert.IsType<KeyValuePair<string, object>[]>(dict._arrayStorage);
-            Assert.Equal(2, dict.Count);
-            Assert.Equal("key", storage[0].Key);
-            Assert.Equal("value", storage[0].Value);
-            Assert.Equal("key3", storage[1].Key);
-            Assert.Equal("value3", storage[1].Value);
-        }
-
-        [Fact]
-        public void FromArray_TakesOwnershipOfArray()
-        {
-            // Arrange
-            var array = new KeyValuePair<string, object>[]
-            {
-                new KeyValuePair<string, object>("a", 0),
-                new KeyValuePair<string, object>("b", 1),
-                new KeyValuePair<string, object>("c", 2),
-            };
-
-            var dictionary = RouteValueDictionary.FromArray(array);
-
-            // Act - modifying the array should modify the dictionary
-            array[0] = new KeyValuePair<string, object>("aa", 10);
-
-            // Assert
-            Assert.Equal(3, dictionary.Count);
-            Assert.Equal(10, dictionary["aa"]);
-        }
-
-        [Fact]
-        public void FromArray_EmptyArray()
-        {
-            // Arrange
-            var array = Array.Empty<KeyValuePair<string, object>>();
-
-            // Act
-            var dictionary = RouteValueDictionary.FromArray(array);
-
-            // Assert
-            Assert.Empty(dictionary);
-        }
-
-        [Fact]
-        public void FromArray_RemovesGapsInArray()
-        {
-            // Arrange
-            var array = new KeyValuePair<string, object>[]
-            {
-                new KeyValuePair<string, object>(null, null),
-                new KeyValuePair<string, object>("a", 0),
-                new KeyValuePair<string, object>(null, null),
-                new KeyValuePair<string, object>(null, null),
-                new KeyValuePair<string, object>("b", 1),
-                new KeyValuePair<string, object>("c", 2),
-                new KeyValuePair<string, object>("d", 3),
-                new KeyValuePair<string, object>(null, null),
-            };
-
-            // Act - calling From should modify the array
-            var dictionary = RouteValueDictionary.FromArray(array);
-
-            // Assert
-            Assert.Equal(4, dictionary.Count);
-            Assert.Equal(
-                new KeyValuePair<string, object>[]
-                {
-                    new KeyValuePair<string, object>("d", 3),
-                    new KeyValuePair<string, object>("a", 0),
-                    new KeyValuePair<string, object>("c", 2),
-                    new KeyValuePair<string, object>("b", 1),
-                    new KeyValuePair<string, object>(null, null),
-                    new KeyValuePair<string, object>(null, null),
-                    new KeyValuePair<string, object>(null, null),
-                    new KeyValuePair<string, object>(null, null),
-                },
-                array);
-        }
-
-        private class RegularType
-        {
-            public bool IsAwesome { get; set; }
-
-            public int CoolnessFactor { get; set; }
-        }
-
-        private class Visibility
-        {
-            private string PrivateYo { get; set; }
-
-            internal int ItsInternalDealWithIt { get; set; }
-
-            public bool IsPublic { get; set; }
-        }
-
-        private class StaticProperty
-        {
-            public static bool IsStatic { get; set; }
-        }
-
-        private class SetterOnly
-        {
-            private bool _coolSetOnly;
-
-            public bool CoolSetOnly { set { _coolSetOnly = value; } }
-        }
-
-        private class Base
-        {
-            public bool DerivedProperty { get; set; }
-        }
-
-        private class Derived : Base
-        {
-            public bool TotallySweetProperty { get; set; }
-        }
-
-        private class DerivedHiddenProperty : Base
-        {
-            public new int DerivedProperty { get; set; }
-        }
-
-        private class IndexerProperty
-        {
-            public bool this[string key]
-            {
-                get { return false; }
-                set { }
-            }
-        }
-
-        private class Address
-        {
-            public string City { get; set; }
-
-            public string State { get; set; }
-        }
-    }
-}

+ 1 - 7
src/Routing/test/Microsoft.AspNetCore.Routing.DecisionTree.Sources.Tests/DecisionTreeBuilderTest.cs

@@ -207,13 +207,7 @@ namespace Microsoft.AspNetCore.Routing.DecisionTree
 
         private class ItemClassifier : IClassifier<Item>
         {
-            public IEqualityComparer<object> ValueComparer
-            {
-                get
-                {
-                    return new RouteValueEqualityComparer();
-                }
-            }
+            public IEqualityComparer<object> ValueComparer => RouteValueEqualityComparer.Default;
 
             public IDictionary<string, DecisionCriterionValue> GetCriteria(Item item)
             {

+ 1 - 1
src/Routing/test/Microsoft.AspNetCore.Routing.DecisionTree.Sources.Tests/Microsoft.AspNetCore.Routing.DecisionTree.Sources.Tests.csproj

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
+    <TargetFramework>netcoreapp3.0</TargetFramework>
   </PropertyGroup>
 
   <ItemGroup>

+ 0 - 2
src/Routing/test/Microsoft.AspNetCore.Routing.FunctionalTests/Benchmarks/EndpointRoutingBenchmarkTest.cs

@@ -1,7 +1,6 @@
 // Copyright (c) .NET Foundation. All rights reserved.
 // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 
-#if NETCOREAPP2_2
 using System;
 using System.Net;
 using System.Net.Http;
@@ -57,4 +56,3 @@ namespace Microsoft.AspNetCore.Routing.FunctionalTests
         }
     }
 }
-#endif

+ 1 - 3
src/Routing/test/Microsoft.AspNetCore.Routing.FunctionalTests/Benchmarks/RouterBenchmarkTest.cs

@@ -1,7 +1,6 @@
 // Copyright (c) .NET Foundation. All rights reserved.
 // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 
-#if NETCOREAPP2_2
 using System;
 using System.Net;
 using System.Net.Http;
@@ -56,5 +55,4 @@ namespace Microsoft.AspNetCore.Routing.FunctionalTests
             _client.Dispose();
         }
     }
-}
-#endif
+}

+ 36 - 4
src/Routing/test/Microsoft.AspNetCore.Routing.FunctionalTests/EndpointRoutingSampleTest.cs

@@ -24,12 +24,27 @@ namespace Microsoft.AspNetCore.Routing.FunctionalTests
             _client.BaseAddress = new Uri("http://localhost");
         }
 
+        [Theory]
+        [InlineData("Branch1")]
+        [InlineData("Branch2")]
+        public async Task Routing_CanRouteRequest_ToBranchRouter(string branch)
+        {
+            // Arrange
+            var message = new HttpRequestMessage(HttpMethod.Get, $"{branch}/api/get/5");
+
+            // Act
+            var response = await _client.SendAsync(message);
+
+            // Assert
+            Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+            Assert.Equal($"{branch} - API Get 5", await response.Content.ReadAsStringAsync());
+        }
+
         [Fact]
         public async Task MatchesRootPath_AndReturnsPlaintext()
         {
             // Arrange
             var expectedContentType = "text/plain";
-            var expectedContent = "Endpoint Routing sample endpoints:" + Environment.NewLine + "/plaintext";
 
             // Act
             var response = await _client.GetAsync("/");
@@ -39,8 +54,6 @@ namespace Microsoft.AspNetCore.Routing.FunctionalTests
             Assert.NotNull(response.Content);
             Assert.NotNull(response.Content.Headers.ContentType);
             Assert.Equal(expectedContentType, response.Content.Headers.ContentType.MediaType);
-            var actualContent = await response.Content.ReadAsStringAsync();
-            Assert.Equal(expectedContent, actualContent);
         }
 
         [Fact]
@@ -48,7 +61,7 @@ namespace Microsoft.AspNetCore.Routing.FunctionalTests
         {
             // Arrange
             var expectedContentType = "text/plain";
-            var expectedContent = "Hello, World!";
+            var expectedContent = "Plain text!";
 
             // Act
             var response = await _client.GetAsync("/plaintext");
@@ -62,6 +75,25 @@ namespace Microsoft.AspNetCore.Routing.FunctionalTests
             Assert.Equal(expectedContent, actualContent);
         }
 
+        [Fact]
+        public async Task MatchesHelloMiddleware_AndReturnsPlaintext()
+        {
+            // Arrange
+            var expectedContentType = "text/plain";
+            var expectedContent = "Hello World";
+
+            // Act
+            var response = await _client.GetAsync("/helloworld");
+
+            // Assert
+            Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+            Assert.NotNull(response.Content);
+            Assert.NotNull(response.Content.Headers.ContentType);
+            Assert.Equal(expectedContentType, response.Content.Headers.ContentType.MediaType);
+            var actualContent = await response.Content.ReadAsStringAsync();
+            Assert.Equal(expectedContent, actualContent);
+        }
+
         [Fact]
         public async Task MatchesEndpoint_WithSuccessfulConstraintMatch()
         {

+ 2 - 2
src/Routing/test/Microsoft.AspNetCore.Routing.FunctionalTests/Microsoft.AspNetCore.Routing.FunctionalTests.csproj

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
+    <TargetFramework>netcoreapp3.0</TargetFramework>
   </PropertyGroup>
 
   <ItemGroup>
@@ -10,7 +10,7 @@
   </ItemGroup>
 
   <ItemGroup>
-    <ProjectReference Include="..\..\benchmarkapps\Benchmarks\Benchmarks.csproj" Condition="'$(TargetFramework)'=='netcoreapp2.2'" />
+    <ProjectReference Include="..\..\benchmarkapps\Benchmarks\Benchmarks.csproj" />
   </ItemGroup>
 
   <ItemGroup>

+ 16 - 0
src/Routing/test/Microsoft.AspNetCore.Routing.FunctionalTests/RouterSampleTest.cs

@@ -24,6 +24,22 @@ namespace Microsoft.AspNetCore.Routing.FunctionalTests
             _client.BaseAddress = new Uri("http://localhost");
         }
 
+        [Theory]
+        [InlineData("Branch1")]
+        [InlineData("Branch2")]
+        public async Task Routing_CanRouteRequest_ToBranchRouter(string branch)
+        {
+            // Arrange
+            var message = new HttpRequestMessage(HttpMethod.Get, $"{branch}/api/get/5");
+
+            // Act
+            var response = await _client.SendAsync(message);
+
+            // Assert
+            Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+            Assert.Equal($"{branch} - API Get 5", await response.Content.ReadAsStringAsync());
+        }
+
         [Fact]
         public async Task Routing_CanRouteRequestDelegate_ToSpecificHttpVerb()
         {

+ 88 - 15
src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Builder/EndpointRoutingBuilderExtensionsTest.cs → src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Builder/EndpointRoutingApplicationBuilderExtensionsTest.cs

@@ -2,20 +2,23 @@
 // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 
 using System;
+using System.Collections.Generic;
 using System.Threading.Tasks;
 using Microsoft.AspNetCore.Builder.Internal;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Http.Features;
-using Microsoft.AspNetCore.Internal;
 using Microsoft.AspNetCore.Routing;
+using Microsoft.AspNetCore.Routing.Matching;
 using Microsoft.AspNetCore.Routing.Patterns;
+using Microsoft.AspNetCore.Routing.TestObjects;
 using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
 using Moq;
 using Xunit;
 
 namespace Microsoft.AspNetCore.Builder
 {
-    public class EndpointRoutingBuilderExtensionsTest
+    public class EndpointRoutingApplicationBuilderExtensionsTest
     {
         [Fact]
         public void UseEndpointRouting_ServicesNotRegistered_Throws()
@@ -24,7 +27,7 @@ namespace Microsoft.AspNetCore.Builder
             var app = new ApplicationBuilder(Mock.Of<IServiceProvider>());
 
             // Act
-            var ex = Assert.Throws<InvalidOperationException>(() => app.UseEndpointRouting());
+            var ex = Assert.Throws<InvalidOperationException>(() => app.UseEndpointRouting(builder => { }));
 
             // Assert
             Assert.Equal(
@@ -59,7 +62,7 @@ namespace Microsoft.AspNetCore.Builder
 
             var app = new ApplicationBuilder(services);
 
-            app.UseEndpointRouting();
+            app.UseEndpointRouting(builder => { });
 
             var appFunc = app.Build();
             var httpContext = new DefaultHttpContext();
@@ -76,17 +79,20 @@ namespace Microsoft.AspNetCore.Builder
         {
             // Arrange
             var endpoint = new RouteEndpoint(
-                TestConstants.EmptyRequestDelegate,
-                RoutePatternFactory.Parse("{*p}"),
-                0,
-                EndpointMetadataCollection.Empty,
-                "Test");
+               TestConstants.EmptyRequestDelegate,
+               RoutePatternFactory.Parse("{*p}"),
+               0,
+               EndpointMetadataCollection.Empty,
+               "Test");
 
-            var services = CreateServices(endpoint);
+            var services = CreateServices();
 
             var app = new ApplicationBuilder(services);
 
-            app.UseEndpointRouting();
+            app.UseEndpointRouting(builder =>
+            {
+                builder.DataSources.Add(new DefaultEndpointDataSource(endpoint));
+            });
 
             var appFunc = app.Build();
             var httpContext = new DefaultHttpContext();
@@ -127,7 +133,7 @@ namespace Microsoft.AspNetCore.Builder
 
             var app = new ApplicationBuilder(services);
 
-            app.UseEndpointRouting();
+            app.UseEndpointRouting(builder => { });
             app.UseEndpoint();
 
             var appFunc = app.Build();
@@ -140,17 +146,84 @@ namespace Microsoft.AspNetCore.Builder
             Assert.Null(httpContext.Features.Get<IEndpointFeature>());
         }
 
-        private IServiceProvider CreateServices(params Endpoint[] endpoints)
+        [Fact]
+        public void UseEndpointRouting_CallWithBuilder_SetsEndpointDataSource()
+        {
+            // Arrange
+            var matcherEndpointDataSources = new List<EndpointDataSource>();
+            var matcherFactoryMock = new Mock<MatcherFactory>();
+            matcherFactoryMock
+                .Setup(m => m.CreateMatcher(It.IsAny<EndpointDataSource>()))
+                .Callback((EndpointDataSource arg) =>
+                {
+                    matcherEndpointDataSources.Add(arg);
+                })
+                .Returns(new TestMatcher(false));
+
+            var services = CreateServices(matcherFactoryMock.Object);
+
+            var app = new ApplicationBuilder(services);
+
+            // Act
+            app.UseEndpointRouting(builder =>
+            {
+                builder.Map("/1", "Test endpoint 1", d => null);
+                builder.Map("/2", "Test endpoint 2", d => null);
+            });
+
+            app.UseEndpointRouting(builder =>
+            {
+                builder.Map("/3", "Test endpoint 3", d => null);
+                builder.Map("/4", "Test endpoint 4", d => null);
+            });
+
+            // This triggers the middleware to be created and the matcher factory to be called
+            // with the datasource we want to test
+            var requestDelegate = app.Build();
+            requestDelegate(new DefaultHttpContext());
+
+            // Assert
+            Assert.Equal(2, matcherEndpointDataSources.Count);
+
+            // Each middleware has its own endpoints
+            Assert.Collection(matcherEndpointDataSources[0].Endpoints,
+                e => Assert.Equal("Test endpoint 1", e.DisplayName),
+                e => Assert.Equal("Test endpoint 2", e.DisplayName));
+            Assert.Collection(matcherEndpointDataSources[1].Endpoints,
+                e => Assert.Equal("Test endpoint 3", e.DisplayName),
+                e => Assert.Equal("Test endpoint 4", e.DisplayName));
+
+            var compositeEndpointBuilder = services.GetRequiredService<EndpointDataSource>();
+
+            // Global middleware has all endpoints
+            Assert.Collection(compositeEndpointBuilder.Endpoints,
+                e => Assert.Equal("Test endpoint 1", e.DisplayName),
+                e => Assert.Equal("Test endpoint 2", e.DisplayName),
+                e => Assert.Equal("Test endpoint 3", e.DisplayName),
+                e => Assert.Equal("Test endpoint 4", e.DisplayName));
+        }
+
+        private IServiceProvider CreateServices()
+        {
+            return CreateServices(matcherFactory: null);
+        }
+
+        private IServiceProvider CreateServices(MatcherFactory matcherFactory)
         {
             var services = new ServiceCollection();
 
+            if (matcherFactory != null)
+            {
+                services.AddSingleton<MatcherFactory>(matcherFactory);
+            }
+
             services.AddLogging();
             services.AddOptions();
             services.AddRouting();
 
-            services.AddSingleton<EndpointDataSource>(new DefaultEndpointDataSource(endpoints));
+            var serviceProvder = services.BuildServiceProvider();
 
-            return services.BuildServiceProvider();
+            return serviceProvder;
         }
     }
 }

+ 101 - 0
src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Builder/MapEndpointEndpointDataSourceBuilderExtensionsTest.cs

@@ -0,0 +1,101 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.AspNetCore.Routing.Matching;
+using Microsoft.AspNetCore.Routing.Patterns;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Builder
+{
+    public class MapEndpointEndpointDataSourceBuilderExtensionsTest
+    {
+        private ModelEndpointDataSource GetBuilderEndpointDataSource(IEndpointRouteBuilder endpointRouteBuilder)
+        {
+            return Assert.IsType<ModelEndpointDataSource>(Assert.Single(endpointRouteBuilder.DataSources));
+        }
+
+        private RouteEndpointModel GetRouteEndpointBuilder(IEndpointRouteBuilder endpointRouteBuilder)
+        {
+            return Assert.IsType<RouteEndpointModel>(Assert.Single(GetBuilderEndpointDataSource(endpointRouteBuilder).EndpointModels));
+        }
+
+        [Fact]
+        public void MapEndpoint_StringPattern_BuildsEndpoint()
+        {
+            // Arrange
+            var builder = new DefaultEndpointRouteBuilder();
+            RequestDelegate requestDelegate = (d) => null;
+
+            // Act
+            var endpointBuilder = builder.Map("/", "Display name!", requestDelegate);
+
+            // Assert
+            var endpointBuilder1 = GetRouteEndpointBuilder(builder);
+
+            Assert.Equal(requestDelegate, endpointBuilder1.RequestDelegate);
+            Assert.Equal("Display name!", endpointBuilder1.DisplayName);
+            Assert.Equal("/", endpointBuilder1.RoutePattern.RawText);
+        }
+
+        [Fact]
+        public void MapEndpoint_TypedPattern_BuildsEndpoint()
+        {
+            // Arrange
+            var builder = new DefaultEndpointRouteBuilder();
+            RequestDelegate requestDelegate = (d) => null;
+
+            // Act
+            var endpointBuilder = builder.Map(RoutePatternFactory.Parse("/"), "Display name!", requestDelegate);
+
+            // Assert
+            var endpointBuilder1 = GetRouteEndpointBuilder(builder);
+
+            Assert.Equal(requestDelegate, endpointBuilder1.RequestDelegate);
+            Assert.Equal("Display name!", endpointBuilder1.DisplayName);
+            Assert.Equal("/", endpointBuilder1.RoutePattern.RawText);
+        }
+
+        [Fact]
+        public void MapEndpoint_StringPatternAndMetadata_BuildsEndpoint()
+        {
+            // Arrange
+            var metadata = new object();
+            var builder = new DefaultEndpointRouteBuilder();
+            RequestDelegate requestDelegate = (d) => null;
+
+            // Act
+            var endpointBuilder = builder.Map("/", "Display name!", requestDelegate, new[] { metadata });
+
+            // Assert
+            var endpointBuilder1 = GetRouteEndpointBuilder(builder);
+            Assert.Equal(requestDelegate, endpointBuilder1.RequestDelegate);
+            Assert.Equal("Display name!", endpointBuilder1.DisplayName);
+            Assert.Equal("/", endpointBuilder1.RoutePattern.RawText);
+            Assert.Equal(metadata, Assert.Single(endpointBuilder1.Metadata));
+        }
+
+        [Fact]
+        public void MapEndpoint_TypedPatternAndMetadata_BuildsEndpoint()
+        {
+            // Arrange
+            var metadata = new object();
+            var builder = new DefaultEndpointRouteBuilder();
+            RequestDelegate requestDelegate = (d) => null;
+
+            // Act
+            var endpointBuilder = builder.Map(RoutePatternFactory.Parse("/"), "Display name!", requestDelegate, new[] { metadata });
+
+            // Assert
+            var endpointBuilder1 = GetRouteEndpointBuilder(builder);
+            Assert.Equal(requestDelegate, endpointBuilder1.RequestDelegate);
+            Assert.Equal("Display name!", endpointBuilder1.DisplayName);
+            Assert.Equal("/", endpointBuilder1.RoutePattern.RawText);
+            Assert.Equal(metadata, Assert.Single(endpointBuilder1.Metadata));
+        }
+    }
+}

+ 85 - 11
src/Routing/test/Microsoft.AspNetCore.Routing.Tests/DefaultLinkGeneratorProcessTemplateTest.cs

@@ -1,11 +1,13 @@
 // Copyright (c) .NET Foundation. All rights reserved.
 // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 
+using System;
 using System.Collections.Generic;
 using System.Linq;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Routing.Constraints;
 using Microsoft.AspNetCore.Routing.TestObjects;
+using Microsoft.Extensions.DependencyInjection;
 using Moq;
 using Xunit;
 
@@ -259,7 +261,12 @@ namespace Microsoft.AspNetCore.Routing
         {
             // Arrange
             var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}");
-            var linkGenerator = CreateLinkGenerator(new RouteOptions() { LowercaseUrls = true }, endpoints: new[] { endpoint, });
+            Action<IServiceCollection> configure = (s) =>
+            {
+                s.Configure<RouteOptions>(o => o.LowercaseUrls = true);
+            };
+
+            var linkGenerator = CreateLinkGenerator(configure, endpoints: new[] { endpoint, });
             var httpContext = CreateHttpContext(ambientValues: new { controller = "Home" });
 
             // Act
@@ -283,7 +290,12 @@ namespace Microsoft.AspNetCore.Routing
         {
             // Arrange
             var endpoint = EndpointFactory.CreateRouteEndpoint("Foo/{bar=BAR}/{id?}");
-            var linkGenerator = CreateLinkGenerator(new RouteOptions() { LowercaseUrls = true }, endpoints: new[] { endpoint, });
+            Action<IServiceCollection> configure = (s) =>
+            {
+                s.Configure<RouteOptions>(o => o.LowercaseUrls = true);
+            };
+
+            var linkGenerator = CreateLinkGenerator(configure, endpoints: new[] { endpoint, });
             var httpContext = CreateHttpContext();
 
             // Act
@@ -334,7 +346,12 @@ namespace Microsoft.AspNetCore.Routing
         {
             // Arrange
             var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}");
-            var linkGenerator = CreateLinkGenerator(new RouteOptions() { LowercaseUrls = true }, endpoints: new[] { endpoint, });
+            Action<IServiceCollection> configure = (s) =>
+            {
+                s.Configure<RouteOptions>(o => o.LowercaseUrls = true);
+            };
+
+            var linkGenerator = CreateLinkGenerator(configure, endpoints: new[] { endpoint, });
             var httpContext = CreateHttpContext(ambientValues: new { controller = "Home" });
 
             // Act
@@ -362,8 +379,17 @@ namespace Microsoft.AspNetCore.Routing
         {
             // Arrange
             var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}");
+            Action<IServiceCollection> configure = (s) =>
+            {
+                s.Configure<RouteOptions>(o =>
+                {
+                    o.LowercaseUrls = true;
+                    o.LowercaseQueryStrings = true;
+                });
+            };
+
             var linkGenerator = CreateLinkGenerator(
-                new RouteOptions() { LowercaseUrls = true, LowercaseQueryStrings = true }, 
+                configure, 
                 endpoints: new[] { endpoint, });
             var httpContext = CreateHttpContext(ambientValues: new { controller = "Home" });
 
@@ -387,8 +413,13 @@ namespace Microsoft.AspNetCore.Routing
         {
             // Arrange
             var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}");
+            Action<IServiceCollection> configure = (s) =>
+            {
+                s.Configure<RouteOptions>(o => o.AppendTrailingSlash = true);
+            };
+
             var linkGenerator = CreateLinkGenerator(
-                new RouteOptions() { AppendTrailingSlash = true },
+                configure,
                 endpoints: new[] { endpoint });
             var httpContext = CreateHttpContext(ambientValues: new { controller = "Home" });
 
@@ -412,8 +443,18 @@ namespace Microsoft.AspNetCore.Routing
         {
             // Arrange
             var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}");
+            Action<IServiceCollection> configure = (s) =>
+            {
+                s.Configure<RouteOptions>(o =>
+                {
+                    o.LowercaseUrls = true;
+                    o.LowercaseQueryStrings = true;
+                    o.AppendTrailingSlash = true;
+                });
+            };
+
             var linkGenerator = CreateLinkGenerator(
-                new RouteOptions() { LowercaseUrls = true, LowercaseQueryStrings = true, AppendTrailingSlash = true },
+                configure,
                 endpoints: new[] { endpoint });
             var httpContext = CreateHttpContext(ambientValues: new { controller = "Home" });
 
@@ -437,8 +478,13 @@ namespace Microsoft.AspNetCore.Routing
         {
             // Arrange
             var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}");
+            Action<IServiceCollection> configure = (s) =>
+            {
+                s.Configure<RouteOptions>(o => o.LowercaseUrls = true);
+            };
+
             var linkGenerator = CreateLinkGenerator(
-                new RouteOptions() { LowercaseUrls = true },
+                configure,
                 endpoints: new[] { endpoint });
             var httpContext = CreateHttpContext(ambientValues: new { controller = "HoMe" });
 
@@ -466,8 +512,13 @@ namespace Microsoft.AspNetCore.Routing
         {
             // Arrange
             var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}");
+            Action<IServiceCollection> configure = (s) =>
+            {
+                s.Configure<RouteOptions>(o => o.LowercaseUrls = false);
+            };
+
             var linkGenerator = CreateLinkGenerator(
-                new RouteOptions() { LowercaseUrls = false },
+                configure,
                 endpoints: new[] { endpoint });
             var httpContext = CreateHttpContext(ambientValues: new { controller = "HoMe" });
 
@@ -494,8 +545,17 @@ namespace Microsoft.AspNetCore.Routing
         {
             // Arrange
             var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}");
+            Action<IServiceCollection> configure = (s) =>
+            {
+                s.Configure<RouteOptions>(o =>
+                {
+                    o.LowercaseUrls = true;
+                    o.LowercaseQueryStrings = true;
+                });
+            };
+
             var linkGenerator = CreateLinkGenerator(
-                new RouteOptions() { LowercaseUrls = true, LowercaseQueryStrings = true },
+                configure,
                 endpoints: new[] { endpoint });
             var httpContext = CreateHttpContext(ambientValues: new { controller = "Home" });
 
@@ -523,8 +583,17 @@ namespace Microsoft.AspNetCore.Routing
         {
             // Arrange
             var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}");
+            Action<IServiceCollection> configure = (s) =>
+            {
+                s.Configure<RouteOptions>(o =>
+                {
+                    o.LowercaseUrls = false;
+                    o.LowercaseQueryStrings = false;
+                });
+            };
+
             var linkGenerator = CreateLinkGenerator(
-                new RouteOptions() { LowercaseUrls = false, LowercaseQueryStrings = false },
+                configure,
                 endpoints: new[] { endpoint });
             var httpContext = CreateHttpContext(ambientValues: new { controller = "Home" });
 
@@ -552,8 +621,13 @@ namespace Microsoft.AspNetCore.Routing
         {
             // Arrange
             var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}");
+            Action<IServiceCollection> configure = (s) =>
+            {
+                s.Configure<RouteOptions>(o => o.AppendTrailingSlash = false);
+            };
+
             var linkGenerator = CreateLinkGenerator(
-                new RouteOptions() { AppendTrailingSlash = false },
+                configure,
                 endpoints: new[] { endpoint });
             var httpContext = CreateHttpContext(ambientValues: new { controller = "Home" });
 

+ 33 - 20
src/Routing/test/Microsoft.AspNetCore.Routing.Tests/DefaultLinkGeneratorTest.cs

@@ -177,10 +177,15 @@ namespace Microsoft.AspNetCore.Routing
             var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller:slugify}/{action}/{id}", metadata: new object[] { new IntMetadata(1), });
             var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller:slugify}/{action}/{id?}", metadata: new object[] { new IntMetadata(1), });
 
-            var routeOptions = new RouteOptions();
-            routeOptions.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer);
+            Action<IServiceCollection> configureServices = s =>
+            {
+                s.Configure<RouteOptions>(o =>
+                {
+                    o.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer);
+                });
+            };
 
-            var linkGenerator = CreateLinkGenerator(routeOptions: routeOptions, configureServices: null, endpoint1, endpoint2);
+            var linkGenerator = CreateLinkGenerator(configureServices, endpoint1, endpoint2);
 
             // Act
             var path = linkGenerator.GetPathByAddress(
@@ -198,10 +203,15 @@ namespace Microsoft.AspNetCore.Routing
             var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller:slugify}/{action}/{id}", metadata: new object[] { new IntMetadata(1), });
             var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller:slugify}/{action}/{id?}", metadata: new object[] { new IntMetadata(1), });
 
-            var routeOptions = new RouteOptions();
-            routeOptions.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer);
+            Action<IServiceCollection> configureServices = s =>
+            {
+                s.Configure<RouteOptions>(o =>
+                {
+                    o.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer);
+                });
+            };
 
-            var linkGenerator = CreateLinkGenerator(routeOptions: routeOptions, configureServices: null, endpoint1, endpoint2);
+            var linkGenerator = CreateLinkGenerator(configureServices, endpoint1, endpoint2);
 
             // Act
             var path = linkGenerator.GetPathByAddress(
@@ -311,17 +321,17 @@ namespace Microsoft.AspNetCore.Routing
         public void GetLink_ParameterTransformer()
         {
             // Arrange
-            var endpoint = EndpointFactory.CreateRouteEndpoint("{controller:upper-case}/{name}");
-
-            var routeOptions = new RouteOptions();
-            routeOptions.ConstraintMap["upper-case"] = typeof(UpperCaseParameterTransform);
+            var endpoint = EndpointFactory.CreateRouteEndpoint("{controller:upper-case}/{name}", requiredValues: new { controller = "Home", name = "Test" });
 
             Action<IServiceCollection> configure = (s) =>
             {
-                s.AddSingleton(typeof(UpperCaseParameterTransform), new UpperCaseParameterTransform());
+                s.Configure<RouteOptions>(o =>
+                {
+                    o.ConstraintMap["upper-case"] = typeof(UpperCaseParameterTransform);
+                });
             };
 
-            var linkGenerator = CreateLinkGenerator(routeOptions, configure, endpoint);
+            var linkGenerator = CreateLinkGenerator(configure, endpoint);
 
             // Act
             var link = linkGenerator.GetPathByRouteValues(routeName: null, new { controller = "Home", name = "Test" });
@@ -334,17 +344,20 @@ namespace Microsoft.AspNetCore.Routing
         public void GetLink_ParameterTransformer_ForQueryString()
         {
             // Arrange
-            var endpoint = EndpointFactory.CreateRouteEndpoint("{controller:upper-case}/{name}", policies: new { c = new UpperCaseParameterTransform(), });
-
-            var routeOptions = new RouteOptions();
-            routeOptions.ConstraintMap["upper-case"] = typeof(UpperCaseParameterTransform);
+            var endpoint = EndpointFactory.CreateRouteEndpoint(
+                "{controller:upper-case}/{name}",
+                requiredValues: new { controller = "Home", name = "Test", c = "hithere", },
+                policies: new { c = new UpperCaseParameterTransform(), });
 
             Action<IServiceCollection> configure = (s) =>
             {
-                s.AddSingleton(typeof(UpperCaseParameterTransform), new UpperCaseParameterTransform());
+                s.Configure<RouteOptions>(o =>
+                {
+                    o.ConstraintMap["upper-case"] = typeof(UpperCaseParameterTransform);
+                });
             };
 
-            var linkGenerator = CreateLinkGenerator(routeOptions, configure, endpoint);
+            var linkGenerator = CreateLinkGenerator(configure, endpoint);
 
             // Act
             var link = linkGenerator.GetPathByRouteValues(routeName: null, new { controller = "Home", name = "Test", c = "hithere", });
@@ -707,9 +720,9 @@ namespace Microsoft.AspNetCore.Routing
 
         private class IntAddressScheme : IEndpointAddressScheme<int>
         {
-            private readonly CompositeEndpointDataSource _dataSource;
+            private readonly EndpointDataSource _dataSource;
 
-            public IntAddressScheme(CompositeEndpointDataSource dataSource)
+            public IntAddressScheme(EndpointDataSource dataSource)
             {
                 _dataSource = dataSource;
             }

+ 3 - 2
src/Routing/test/Microsoft.AspNetCore.Routing.Tests/EndpointFactory.cs

@@ -1,10 +1,11 @@
 // Copyright (c) .NET Foundation. All rights reserved.
 // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 
-using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Routing.Patterns;
 using System;
 using System.Collections.Generic;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Routing.Matching;
+using Microsoft.AspNetCore.Routing.Patterns;
 
 namespace Microsoft.AspNetCore.Routing
 {

+ 1 - 2
src/Routing/test/Microsoft.AspNetCore.Routing.Tests/EndpointRoutingMiddlewareTest.cs

@@ -151,10 +151,9 @@ namespace Microsoft.AspNetCore.Routing
             logger = logger ?? new Logger<EndpointRoutingMiddleware>(NullLoggerFactory.Instance);
             matcherFactory = matcherFactory ?? new TestMatcherFactory(true);
 
-            var options = Options.Create(new EndpointOptions());
             var middleware = new EndpointRoutingMiddleware(
                 matcherFactory,
-                new CompositeEndpointDataSource(Array.Empty<EndpointDataSource>()),
+                new DefaultEndpointDataSource(),
                 logger,
                 next);
 

+ 14 - 20
src/Routing/test/Microsoft.AspNetCore.Routing.Tests/LinkGeneratorTestBase.cs

@@ -9,6 +9,7 @@ using Microsoft.Extensions.Logging.Abstractions;
 using Microsoft.Extensions.ObjectPool;
 using Microsoft.Extensions.Options;
 using System;
+using System.Collections.Generic;
 
 namespace Microsoft.AspNetCore.Routing
 {
@@ -44,29 +45,22 @@ namespace Microsoft.AspNetCore.Routing
 
         private protected DefaultLinkGenerator CreateLinkGenerator(params Endpoint[] endpoints)
         {
-            return CreateLinkGenerator(routeOptions: null, endpoints);
-        }
-
-        private protected DefaultLinkGenerator CreateLinkGenerator(RouteOptions routeOptions, params Endpoint[] endpoints)
-        {
-            return CreateLinkGenerator(routeOptions, configureServices: null, endpoints);
+            return CreateLinkGenerator(configureServices: null, endpoints);
         }
 
         private protected DefaultLinkGenerator CreateLinkGenerator(
-            RouteOptions routeOptions,
             Action<IServiceCollection> configureServices,
             params Endpoint[] endpoints)
         {
-            return CreateLinkGenerator(routeOptions, configureServices, new[] { new DefaultEndpointDataSource(endpoints ?? Array.Empty<Endpoint>()) });
+            return CreateLinkGenerator(configureServices, new[] { new DefaultEndpointDataSource(endpoints ?? Array.Empty<Endpoint>()) });
         }
 
         private protected DefaultLinkGenerator CreateLinkGenerator(EndpointDataSource[] dataSources)
         {
-            return CreateLinkGenerator(routeOptions: null, configureServices: null, dataSources);
+            return CreateLinkGenerator(configureServices: null, dataSources);
         }
 
         private protected DefaultLinkGenerator CreateLinkGenerator(
-            RouteOptions routeOptions,
             Action<IServiceCollection> configureServices,
             EndpointDataSource[] dataSources)
         {
@@ -74,25 +68,25 @@ namespace Microsoft.AspNetCore.Routing
             AddAdditionalServices(services);
             configureServices?.Invoke(services);
 
-            routeOptions = routeOptions ?? new RouteOptions();
-            dataSources = dataSources ?? Array.Empty<EndpointDataSource>();
-
-            services.Configure<EndpointOptions>((o) =>
+            services.Configure<RouteOptions>(o =>
             {
-                for (var i = 0; i < dataSources.Length; i++)
+                if (dataSources != null)
                 {
-                    o.DataSources.Add(dataSources[i]);
+                    foreach (var dataSource in dataSources)
+                    {
+                        o.EndpointDataSources.Add(dataSource);
+                    }
                 }
             });
 
-            var options = Options.Create(routeOptions);
             var serviceProvider = services.BuildServiceProvider();
+            var routeOptions = serviceProvider.GetRequiredService<IOptions<RouteOptions>>();
 
             return new DefaultLinkGenerator(
-                new DefaultParameterPolicyFactory(options, serviceProvider),
-                serviceProvider.GetRequiredService<CompositeEndpointDataSource>(),
+                new DefaultParameterPolicyFactory(routeOptions, serviceProvider),
+                new CompositeEndpointDataSource(routeOptions.Value.EndpointDataSources),
                 new DefaultObjectPool<UriBuildingContext>(new UriBuilderContextPooledObjectPolicy()),
-                options,
+                routeOptions,
                 NullLogger<DefaultLinkGenerator>.Instance,
                 serviceProvider);
         }

+ 0 - 2
src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Matching/ILEmitTrieFactoryTest.cs

@@ -1,7 +1,6 @@
 // Copyright (c) .NET Foundation. All rights reserved.
 // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 
-#if IL_EMIT
 using System;
 using Xunit;
 
@@ -50,4 +49,3 @@ namespace Microsoft.AspNetCore.Routing.Matching
         }
     }
 }
-#endif

+ 0 - 2
src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Matching/ILEmitTrieJumpTableTest.cs

@@ -1,7 +1,6 @@
 // Copyright (c) .NET Foundation. All rights reserved.
 // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 
-#if IL_EMIT
 using Moq;
 using System.Threading.Tasks;
 using Xunit;
@@ -225,4 +224,3 @@ namespace Microsoft.AspNetCore.Routing.Matching
         }
     }
 }
-#endif

+ 4 - 1
src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Matching/MatcherConformanceTest.cs

@@ -21,7 +21,10 @@ namespace Microsoft.AspNetCore.Routing.Matching
             httpContext.Request.Path = path;
             httpContext.RequestServices = CreateServices();
 
-            var context = new EndpointSelectorContext();
+            var context = new EndpointSelectorContext()
+            {
+                RouteValues = new RouteValueDictionary()
+            };
             httpContext.Features.Set<IEndpointFeature>(context);
             httpContext.Features.Set<IRouteValuesFeature>(context);
 

+ 0 - 2
src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Matching/NonVectorizedILEmitTrieJumpTableTest.cs

@@ -1,7 +1,6 @@
 // Copyright (c) .NET Foundation. All rights reserved.
 // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 
-#if IL_EMIT
 namespace Microsoft.AspNetCore.Routing.Matching
 {
     public class NonVectorizedILEmitTrieJumpTableTest : ILEmitTreeJumpTableTestBase
@@ -9,4 +8,3 @@ namespace Microsoft.AspNetCore.Routing.Matching
         public override bool Vectorize => false;
     }
 }
-#endif

+ 0 - 2
src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Matching/VectorizedILEmitTrieJumpTableTest.cs

@@ -1,7 +1,6 @@
 // Copyright (c) .NET Foundation. All rights reserved.
 // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 
-#if IL_EMIT
 namespace Microsoft.AspNetCore.Routing.Matching
 {
     public class VectorizedILEmitTrieJumpTableTest : ILEmitTreeJumpTableTestBase
@@ -11,4 +10,3 @@ namespace Microsoft.AspNetCore.Routing.Matching
         public override bool Vectorize => true;
     }
 }
-#endif

+ 1 - 13
src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Microsoft.AspNetCore.Routing.Tests.csproj

@@ -1,23 +1,11 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
+    <TargetFramework>netcoreapp3.0</TargetFramework>
     <RootNamespace>Microsoft.AspNetCore.Routing</RootNamespace>
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
   </PropertyGroup>
 
-  <PropertyGroup>
-    <!-- 
-      RefEmit is supported in netcoreapp. We test on .NET Framework, but we don't support RefEmit in the product
-      on .NET Framework.
-    -->
-    <ILEmit Condition="'$(TargetFramework)'=='netcoreapp2.2'">true</ILEmit>
-    <DefineConstants Condition="'$(ILEmit)'=='true'">IL_EMIT;$(DefineConstants)</DefineConstants>
-  </PropertyGroup>
-
-  <PropertyGroup>
-  </PropertyGroup>
-
   <ItemGroup>
     <ProjectReference Include="..\..\src\Microsoft.AspNetCore.Routing\Microsoft.AspNetCore.Routing.csproj" />
   </ItemGroup>

+ 339 - 0
src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Patterns/DefaultRoutePatternTransformerTest.cs

@@ -0,0 +1,339 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using Microsoft.AspNetCore.Routing.Constraints;
+using Microsoft.Extensions.DependencyInjection;
+using System.Collections.Generic;
+using System.Linq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Routing.Patterns
+{
+    public class DefaultRoutePatternTransformerTest
+    {
+        public DefaultRoutePatternTransformerTest()
+        {
+            var services = new ServiceCollection();
+            services.AddRouting();
+            services.AddOptions();
+            Transformer = services.BuildServiceProvider().GetRequiredService<RoutePatternTransformer>();
+        }
+
+        public RoutePatternTransformer Transformer { get; }
+
+        [Fact]
+        public void SubstituteRequiredValues_CanAcceptNullForAnyKey()
+        {
+            // Arrange
+            var template = "{controller=Home}/{action=Index}/{id?}";
+            var defaults = new { };
+            var policies = new { };
+
+            var original = RoutePatternFactory.Parse(template, defaults, policies);
+
+            var requiredValues = new { a = (string)null, b = "", };
+
+            // Act
+            var actual = Transformer.SubstituteRequiredValues(original, requiredValues);
+
+            // Assert
+            Assert.Collection(
+                actual.RequiredValues.OrderBy(kvp => kvp.Key),
+                kvp => Assert.Equal(new KeyValuePair<string, object>("a", null), kvp),
+                kvp => Assert.Equal(new KeyValuePair<string, object>("b", string.Empty), kvp));
+        }
+
+        [Fact]
+        public void SubstituteRequiredValues_RejectsNullForParameter()
+        {
+            // Arrange
+            var template = "{controller=Home}/{action=Index}/{id?}";
+            var defaults = new { };
+            var policies = new { };
+
+            var original = RoutePatternFactory.Parse(template, defaults, policies);
+
+            var requiredValues = new { controller = string.Empty, };
+
+            // Act
+            var actual = Transformer.SubstituteRequiredValues(original, requiredValues);
+
+            // Assert
+            Assert.Null(actual);
+        }
+
+        [Fact]
+        public void SubstituteRequiredValues_RejectsNullForOutOfLineDefault()
+        {
+            // Arrange
+            var template = "{controller=Home}/{action=Index}/{id?}";
+            var defaults = new { area = "Admin" };
+            var policies = new { };
+
+            var original = RoutePatternFactory.Parse(template, defaults, policies);
+
+            var requiredValues = new { area = string.Empty, };
+
+            // Act
+            var actual = Transformer.SubstituteRequiredValues(original, requiredValues);
+
+            // Assert
+            Assert.Null(actual);
+        }
+
+        [Fact]
+        public void SubstituteRequiredValues_CanAcceptValueForParameter()
+        {
+            // Arrange
+            var template = "{controller}/{action}/{id?}";
+            var defaults = new { };
+            var policies = new { };
+
+            var original = RoutePatternFactory.Parse(template, defaults, policies);
+
+            var requiredValues = new { controller = "Home", action = "Index", };
+
+            // Act
+            var actual = Transformer.SubstituteRequiredValues(original, requiredValues);
+
+            // Assert
+            Assert.Collection(
+                actual.RequiredValues.OrderBy(kvp => kvp.Key),
+                kvp => Assert.Equal(new KeyValuePair<string, object>("action", "Index"), kvp),
+                kvp => Assert.Equal(new KeyValuePair<string, object>("controller", "Home"), kvp));
+        }
+
+        [Fact]
+        public void SubstituteRequiredValues_CanAcceptValueForParameter_WithSameDefault()
+        {
+            // Arrange
+            var template = "{controller=Home}/{action=Index}/{id?}";
+            var defaults = new { };
+            var policies = new { };
+
+            var original = RoutePatternFactory.Parse(template, defaults, policies);
+
+            var requiredValues = new { controller = "Home", action = "Index", };
+
+            // Act
+            var actual = Transformer.SubstituteRequiredValues(original, requiredValues);
+
+            // Assert
+            Assert.Collection(
+                actual.RequiredValues.OrderBy(kvp => kvp.Key),
+                kvp => Assert.Equal(new KeyValuePair<string, object>("action", "Index"), kvp),
+                kvp => Assert.Equal(new KeyValuePair<string, object>("controller", "Home"), kvp));
+
+            // We should not need to rewrite anything in this case.
+            Assert.Same(actual.Defaults, original.Defaults);
+            Assert.Same(actual.Parameters, original.Parameters);
+            Assert.Same(actual.PathSegments, original.PathSegments);
+        }
+
+        [Fact]
+        public void SubstituteRequiredValues_CanAcceptValueForParameter_WithDifferentDefault()
+        {
+            // Arrange
+            var template = "{controller=Blog}/{action=ReadPost}/{id?}";
+            var defaults = new { area = "Admin", };
+            var policies = new { };
+
+            var original = RoutePatternFactory.Parse(template, defaults, policies);
+
+            var requiredValues = new { area = "Admin", controller = "Home", action = "Index", };
+
+            // Act
+            var actual = Transformer.SubstituteRequiredValues(original, requiredValues);
+
+            // Assert
+            Assert.Collection(
+                actual.RequiredValues.OrderBy(kvp => kvp.Key),
+                kvp => Assert.Equal(new KeyValuePair<string, object>("action", "Index"), kvp),
+                kvp => Assert.Equal(new KeyValuePair<string, object>("area", "Admin"), kvp),
+                kvp => Assert.Equal(new KeyValuePair<string, object>("controller", "Home"), kvp));
+
+            // We should not need to rewrite anything in this case.
+            Assert.NotSame(actual.Defaults, original.Defaults);
+            Assert.NotSame(actual.Parameters, original.Parameters);
+            Assert.NotSame(actual.PathSegments, original.PathSegments);
+
+            // other defaults were wiped out
+            Assert.Equal(new KeyValuePair<string, object>("area", "Admin"), Assert.Single(actual.Defaults));
+            Assert.Null(actual.GetParameter("controller").Default);
+            Assert.False(actual.Defaults.ContainsKey("controller"));
+            Assert.Null(actual.GetParameter("action").Default);
+            Assert.False(actual.Defaults.ContainsKey("action"));
+        }
+
+        [Fact]
+        public void SubstituteRequiredValues_CanAcceptValueForParameter_WithMatchingConstraint()
+        {
+            // Arrange
+            var template = "{controller}/{action}/{id?}";
+            var defaults = new { };
+            var policies = new { controller = "Home", action = new RegexRouteConstraint("Index"), };
+
+            var original = RoutePatternFactory.Parse(template, defaults, policies);
+
+            var requiredValues = new { controller = "Home", action = "Index", };
+
+            // Act
+            var actual = Transformer.SubstituteRequiredValues(original, requiredValues);
+
+            // Assert
+            Assert.Collection(
+                actual.RequiredValues.OrderBy(kvp => kvp.Key),
+                kvp => Assert.Equal(new KeyValuePair<string, object>("action", "Index"), kvp),
+                kvp => Assert.Equal(new KeyValuePair<string, object>("controller", "Home"), kvp));
+        }
+
+        [Fact]
+        public void SubstituteRequiredValues_CanRejectValueForParameter_WithNonMatchingConstraint()
+        {
+            // Arrange
+            var template = "{controller}/{action}/{id?}";
+            var defaults = new { };
+            var policies = new { controller = "Home", action = new RegexRouteConstraint("Index"), };
+
+            var original = RoutePatternFactory.Parse(template, defaults, policies);
+
+            var requiredValues = new { controller = "Blog", action = "Index", };
+
+            // Act
+            var actual = Transformer.SubstituteRequiredValues(original, requiredValues);
+
+            // Assert
+            Assert.Null(actual);
+        }
+
+        [Fact]
+        public void SubstituteRequiredValues_CanAcceptValueForDefault_WithSameValue()
+        {
+            // Arrange
+            var template = "Home/Index/{id?}";
+            var defaults = new { controller = "Home", action = "Index", };
+            var policies = new { };
+
+            var original = RoutePatternFactory.Parse(template, defaults, policies);
+
+            var requiredValues = new { controller = "Home", action = "Index", };
+
+            // Act
+            var actual = Transformer.SubstituteRequiredValues(original, requiredValues);
+
+            // Assert
+            Assert.Collection(
+                actual.RequiredValues.OrderBy(kvp => kvp.Key),
+                kvp => Assert.Equal(new KeyValuePair<string, object>("action", "Index"), kvp),
+                kvp => Assert.Equal(new KeyValuePair<string, object>("controller", "Home"), kvp));
+        }
+
+        [Fact]
+        public void SubstituteRequiredValues_CanRejectValueForDefault_WithDifferentValue()
+        {
+            // Arrange
+            var template = "Home/Index/{id?}";
+            var defaults = new { controller = "Home", action = "Index", };
+            var policies = new { };
+
+            var original = RoutePatternFactory.Parse(template, defaults, policies);
+
+            var requiredValues = new { controller = "Blog", action = "Index", };
+
+            // Act
+            var actual = Transformer.SubstituteRequiredValues(original, requiredValues);
+
+            // Assert
+            Assert.Null(actual);
+        }
+
+        [Fact]
+        public void SubstituteRequiredValues_CanAcceptValueForDefault_WithSameValue_Null()
+        {
+            // Arrange
+            var template = "Home/Index/{id?}";
+            var defaults = new { controller = (string)null, action = "", };
+            var policies = new { };
+
+            var original = RoutePatternFactory.Parse(template, defaults, policies);
+
+            var requiredValues = new { controller = string.Empty, action = (string)null, };
+
+            // Act
+            var actual = Transformer.SubstituteRequiredValues(original, requiredValues);
+
+            // Assert
+            Assert.Collection(
+                actual.RequiredValues.OrderBy(kvp => kvp.Key),
+                kvp => Assert.Equal(new KeyValuePair<string, object>("action", null), kvp),
+                kvp => Assert.Equal(new KeyValuePair<string, object>("controller", ""), kvp));
+        }
+
+        [Fact]
+        public void SubstituteRequiredValues_CanAcceptValueForDefault_WithSameValue_WithMatchingConstraint()
+        {
+            // Arrange
+            var template = "Home/Index/{id?}";
+            var defaults = new { controller = "Home", action = "Index", };
+            var policies = new { controller = "Home", };
+
+            var original = RoutePatternFactory.Parse(template, defaults, policies);
+
+            var requiredValues = new { controller = "Home", action = "Index", };
+
+            // Act
+            var actual = Transformer.SubstituteRequiredValues(original, requiredValues);
+
+            // Assert
+            Assert.Collection(
+                actual.RequiredValues.OrderBy(kvp => kvp.Key),
+                kvp => Assert.Equal(new KeyValuePair<string, object>("action", "Index"), kvp),
+                kvp => Assert.Equal(new KeyValuePair<string, object>("controller", "Home"), kvp));
+        }
+
+        [Fact]
+        public void SubstituteRequiredValues_CanRejectValueForDefault_WithSameValue_WithNonMatchingConstraint()
+        {
+            // Arrange
+            var template = "Home/Index/{id?}";
+            var defaults = new { controller = "Home", action = "Index", };
+            var policies = new { controller = "Home", };
+
+            var original = RoutePatternFactory.Parse(template, defaults, policies);
+
+            var requiredValues = new { controller = "Home", action = "Index", };
+
+            // Act
+            var actual = Transformer.SubstituteRequiredValues(original, requiredValues);
+
+            // Assert
+            Assert.Collection(
+                actual.RequiredValues.OrderBy(kvp => kvp.Key),
+                kvp => Assert.Equal(new KeyValuePair<string, object>("action", "Index"), kvp),
+                kvp => Assert.Equal(new KeyValuePair<string, object>("controller", "Home"), kvp));
+        }
+
+        [Fact]
+        public void SubstituteRequiredValues_CanMergeExistingRequiredValues()
+        {
+            // Arrange
+            var template = "Home/Index/{id?}";
+            var defaults = new { area = "Admin", controller = "Home", action = "Index", };
+            var policies = new { };
+
+            var original = RoutePatternFactory.Parse(template, defaults, policies, new { area = "Admin", controller = "Home", });
+
+            var requiredValues = new { controller = "Home", action = "Index", };
+
+            // Act
+            var actual = Transformer.SubstituteRequiredValues(original, requiredValues);
+
+            // Assert
+            Assert.Collection(
+                actual.RequiredValues.OrderBy(kvp => kvp.Key),
+                kvp => Assert.Equal(new KeyValuePair<string, object>("action", "Index"), kvp),
+                kvp => Assert.Equal(new KeyValuePair<string, object>("area", "Admin"), kvp),
+                kvp => Assert.Equal(new KeyValuePair<string, object>("controller", "Home"), kvp));
+        }
+    }
+}

+ 83 - 0
src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Patterns/RoutePatternFactoryTest.cs

@@ -441,6 +441,89 @@ namespace Microsoft.AspNetCore.Routing.Patterns
             Assert.Null(paramPartD.Default);
         }
 
+        [Fact]
+        public void Parse_WithRequiredValues()
+        {
+            // Arrange
+            var template = "{controller=Home}/{action=Index}/{id?}";
+            var defaults = new { area = "Admin", };
+            var policies = new { };
+            var requiredValues = new { area = "Admin", controller = "Store", action = "Index", };
+
+            // Act
+            var action = RoutePatternFactory.Parse(template, defaults, policies, requiredValues);
+
+            // Assert
+            Assert.Collection(
+                action.RequiredValues.OrderBy(kvp => kvp.Key),
+                kvp => { Assert.Equal("action", kvp.Key); Assert.Equal("Index", kvp.Value); },
+                kvp => { Assert.Equal("area", kvp.Key); Assert.Equal("Admin", kvp.Value); },
+                kvp => { Assert.Equal("controller", kvp.Key); Assert.Equal("Store", kvp.Value); });
+        }
+
+        [Fact]
+        public void Parse_WithRequiredValues_AllowsNullRequiredValue()
+        {
+            // Arrange
+            var template = "{controller=Home}/{action=Index}/{id?}";
+            var defaults = new { };
+            var policies = new { };
+            var requiredValues = new { area = (string)null, controller = "Store", action = "Index", };
+
+            // Act
+            var action = RoutePatternFactory.Parse(template, defaults, policies, requiredValues);
+
+            // Assert
+            Assert.Collection(
+                action.RequiredValues.OrderBy(kvp => kvp.Key),
+                kvp => { Assert.Equal("action", kvp.Key); Assert.Equal("Index", kvp.Value); },
+                kvp => { Assert.Equal("area", kvp.Key); Assert.Null(kvp.Value); },
+                kvp => { Assert.Equal("controller", kvp.Key); Assert.Equal("Store", kvp.Value); });
+        }
+
+        [Fact]
+        public void Parse_WithRequiredValues_AllowsEmptyRequiredValue()
+        {
+            // Arrange
+            var template = "{controller=Home}/{action=Index}/{id?}";
+            var defaults = new { };
+            var policies = new { };
+            var requiredValues = new { area = "", controller = "Store", action = "Index", };
+
+            // Act
+            var action = RoutePatternFactory.Parse(template, defaults, policies, requiredValues);
+
+            // Assert
+            Assert.Collection(
+                action.RequiredValues.OrderBy(kvp => kvp.Key),
+                kvp => { Assert.Equal("action", kvp.Key); Assert.Equal("Index", kvp.Value); },
+                kvp => { Assert.Equal("area", kvp.Key); Assert.Equal("", kvp.Value); },
+                kvp => { Assert.Equal("controller", kvp.Key); Assert.Equal("Store", kvp.Value); });
+        }
+
+        [Fact]
+        public void Parse_WithRequiredValues_ThrowsForNonParameterNonDefault()
+        {
+            // Arrange
+            var template = "{controller=Home}/{action=Index}/{id?}";
+            var defaults = new { };
+            var policies = new { };
+            var requiredValues = new { area = "Admin", controller = "Store", action = "Index", };
+
+            // Act
+            var exception = Assert.Throws<InvalidOperationException>(() =>
+            {
+                var action = RoutePatternFactory.Parse(template, defaults, policies, requiredValues);
+            });
+
+            // Assert
+            Assert.Equal(
+                "No corresponding parameter or default value could be found for the required value " +
+                "'area=Admin'. A non-null required value must correspond to a route parameter or the " +
+                "route pattern must have a matching default value.", 
+                exception.Message);
+        }
+        
         [Fact]
         public void ParameterPart_ParameterNameAndDefaultAndParameterKindAndArrayOfParameterPolicies_ShouldMakeCopyOfParameterPolicies()
         {

+ 36 - 0
src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RouteEndpointModelTest.cs

@@ -0,0 +1,36 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Routing.Patterns;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Routing
+{
+    public class RouteEndpointModelTest
+    {
+        [Fact]
+        public void Build_AllValuesSet_EndpointCreated()
+        {
+            const int defaultOrder = 0;
+            var metadata = new object();
+            RequestDelegate requestDelegate = (d) => null;
+
+            var builder = new RouteEndpointModel(requestDelegate, RoutePatternFactory.Parse("/"), defaultOrder)
+            {
+                DisplayName = "Display name!",
+                Metadata = { metadata }
+            };
+
+            var endpoint = Assert.IsType<RouteEndpoint>(builder.Build());
+            Assert.Equal("Display name!", endpoint.DisplayName);
+            Assert.Equal(defaultOrder, endpoint.Order);
+            Assert.Equal(requestDelegate, endpoint.RequestDelegate);
+            Assert.Equal("/", endpoint.RoutePattern.RawText);
+            Assert.Equal(metadata, Assert.Single(endpoint.Metadata));
+        }
+    }
+}

+ 75 - 30
src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RouteValuesAddressSchemeTest.cs

@@ -18,7 +18,7 @@ namespace Microsoft.AspNetCore.Routing
         public void GetOutboundMatches_GetsNamedMatchesFor_EndpointsHaving_IRouteNameMetadata()
         {
             // Arrange
-            var endpoint1 = CreateEndpoint("/a");
+            var endpoint1 = CreateEndpoint("/a", routeName: "other");
             var endpoint2 = CreateEndpoint("/a", routeName: "named");
 
             // Act
@@ -38,7 +38,7 @@ namespace Microsoft.AspNetCore.Routing
         public void GetOutboundMatches_GroupsMultipleEndpoints_WithSameName()
         {
             // Arrange
-            var endpoint1 = CreateEndpoint("/a");
+            var endpoint1 = CreateEndpoint("/a", routeName: "other");
             var endpoint2 = CreateEndpoint("/a", routeName: "named");
             var endpoint3 = CreateEndpoint("/b", routeName: "named");
 
@@ -59,7 +59,7 @@ namespace Microsoft.AspNetCore.Routing
         public void GetOutboundMatches_GroupsMultipleEndpoints_WithSameName_IgnoringCase()
         {
             // Arrange
-            var endpoint1 = CreateEndpoint("/a");
+            var endpoint1 = CreateEndpoint("/a", routeName: "other");
             var endpoint2 = CreateEndpoint("/a", routeName: "named");
             var endpoint3 = CreateEndpoint("/b", routeName: "NaMed");
 
@@ -80,7 +80,7 @@ namespace Microsoft.AspNetCore.Routing
         public void EndpointDataSource_ChangeCallback_Refreshes_OutboundMatches()
         {
             // Arrange 1
-            var endpoint1 = CreateEndpoint("/a");
+            var endpoint1 = CreateEndpoint("/a", metadataRequiredValues: new { });
             var dynamicDataSource = new DynamicEndpointDataSource(new[] { endpoint1 });
 
             // Act 1
@@ -93,21 +93,21 @@ namespace Microsoft.AspNetCore.Routing
             Assert.Same(endpoint1, actual);
 
             // Arrange 2
-            var endpoint2 = CreateEndpoint("/b");
+            var endpoint2 = CreateEndpoint("/b", metadataRequiredValues: new { });
 
             // Act 2
             // Trigger change
             dynamicDataSource.AddEndpoint(endpoint2);
 
             // Arrange 2
-            var endpoint3 = CreateEndpoint("/c");
+            var endpoint3 = CreateEndpoint("/c", metadataRequiredValues: new { });
 
             // Act 2
             // Trigger change
             dynamicDataSource.AddEndpoint(endpoint3);
 
             // Arrange 3
-            var endpoint4 = CreateEndpoint("/d");
+            var endpoint4 = CreateEndpoint("/d", metadataRequiredValues: new { });
 
             // Act 3
             // Trigger change
@@ -146,13 +146,11 @@ namespace Microsoft.AspNetCore.Routing
             var endpoint1 = CreateEndpoint(
                 "api/orders/{id}/{name?}/{urgent=true}/{zipCode}",
                 defaults: new { zipCode = 3510 },
-                requiredValues: new { id = 7 },
-                routeName: "OrdersApi");
+                metadataRequiredValues: new { id = 7 });
             var endpoint2 = CreateEndpoint(
                 "api/orders/{id}/{name?}/{urgent=true}/{zipCode}",
                 defaults: new { id = 12 },
-                requiredValues: new { zipCode = 3510 },
-                routeName: "OrdersApi");
+                metadataRequiredValues: new { zipCode = 3510 });
             var addressScheme = CreateAddressScheme(endpoint1, endpoint2);
 
             // Act
@@ -174,25 +172,23 @@ namespace Microsoft.AspNetCore.Routing
             var endpoint1 = CreateEndpoint(
                 "api/orders/{id}/{name?}/{urgent=true}/{zipCode}",
                 defaults: new { zipCode = 3510 },
-                requiredValues: new { id = 7 },
-                routeName: "OrdersApi");
+                metadataRequiredValues: new { id = 7 });
             var endpoint2 = CreateEndpoint(
                 "api/orders/{id}/{name?}/{urgent=true}/{zipCode}",
-                defaults: new { id = 12 },
-                routeName: "OrdersApi");
+                defaults: new { id = 12 });
             var addressScheme = CreateAddressScheme(endpoint1, endpoint2);
 
             // Act
             var foundEndpoints = addressScheme.FindEndpoints(
                 new RouteValuesAddress
                 {
-                    ExplicitValues = new RouteValueDictionary(new { id = 13 }),
+                    ExplicitValues = new RouteValueDictionary(new { id = 7 }),
                     AmbientValues = new RouteValueDictionary(new { zipCode = 3500 }),
                 });
 
             // Assert
             var actual = Assert.Single(foundEndpoints);
-            Assert.Same(endpoint2, actual);
+            Assert.Same(endpoint1, actual);
         }
 
         [Fact]
@@ -202,28 +198,53 @@ namespace Microsoft.AspNetCore.Routing
             var endpoint1 = CreateEndpoint(
                 "api/orders/{id}/{name?}/{urgent=true}/{zipCode}",
                 defaults: new { zipCode = 3510 },
-                requiredValues: new { id = 7 },
-                routeName: "OrdersApi");
+                metadataRequiredValues: new { id = 7 });
             var endpoint2 = CreateEndpoint(
                 "api/orders/{id}/{name?}/{urgent}/{zipCode}",
                 defaults: new { id = 12 },
-                routeName: "OrdersApi");
+                metadataRequiredValues: new { id = 12 });
             var endpoint3 = CreateEndpoint(
                 "api/orders/{id}/{name?}/{urgent=true}/{zipCode}",
                 defaults: new { id = 12 },
-                routeName: "OrdersApi");
+                metadataRequiredValues: new { id = 12 });
             var addressScheme = CreateAddressScheme(endpoint1, endpoint2, endpoint3);
 
             // Act
             var foundEndpoints = addressScheme.FindEndpoints(
                 new RouteValuesAddress
                 {
-                    ExplicitValues = new RouteValueDictionary(new { id = 7 }),
+                    ExplicitValues = new RouteValueDictionary(new { id = 12 }),
                     AmbientValues = new RouteValueDictionary(new { zipCode = 3500 }),
                 });
 
             // Assert
-            Assert.Contains(endpoint1, foundEndpoints);
+            Assert.Collection(foundEndpoints,
+                e => Assert.Equal(endpoint3, e),
+                e => Assert.Equal(endpoint2, e));
+        }
+
+        [Fact]
+        public void FindEndpoints_LookedUpByCriteria_ExcludeEndpointWithoutRouteValuesAddressMetadata()
+        {
+            // Arrange
+            var endpoint1 = CreateEndpoint(
+                "api/orders/{id}/{name?}/{urgent=true}/{zipCode}",
+                defaults: new { zipCode = 3510 },
+                metadataRequiredValues: new { id = 7 });
+            var endpoint2 = CreateEndpoint("test");
+
+            var addressScheme = CreateAddressScheme(endpoint1, endpoint2);
+
+            // Act
+            var foundEndpoints = addressScheme.FindEndpoints(
+                new RouteValuesAddress
+                {
+                    ExplicitValues = new RouteValueDictionary(new { id = 7 }),
+                    AmbientValues = new RouteValueDictionary(new { zipCode = 3500 }),
+                }).ToList();
+
+            // Assert
+            Assert.DoesNotContain(endpoint2, foundEndpoints);
             Assert.Contains(endpoint1, foundEndpoints);
         }
 
@@ -234,7 +255,7 @@ namespace Microsoft.AspNetCore.Routing
             var expected = CreateEndpoint(
                 "api/orders/{id}",
                 defaults: new { controller = "Orders", action = "GetById" },
-                requiredValues: new { controller = "Orders", action = "GetById" },
+                metadataRequiredValues: new { controller = "Orders", action = "GetById" },
                 routeName: "OrdersApi");
             var addressScheme = CreateAddressScheme(expected);
 
@@ -252,6 +273,29 @@ namespace Microsoft.AspNetCore.Routing
             Assert.Same(expected, actual);
         }
 
+        [Fact]
+        public void FindEndpoints_ReturnsEndpoint_UsingRoutePatternRequiredValues()
+        {
+            // Arrange
+            var expected = CreateEndpoint(
+                "api/orders/{id}",
+                defaults: new { controller = "Orders", action = "GetById" },
+                routePatternRequiredValues: new { controller = "Orders", action = "GetById" });
+            var addressScheme = CreateAddressScheme(expected);
+
+            // Act
+            var foundEndpoints = addressScheme.FindEndpoints(
+                new RouteValuesAddress
+                {
+                    ExplicitValues = new RouteValueDictionary(new { id = 10 }),
+                    AmbientValues = new RouteValueDictionary(new { controller = "Orders", action = "GetById" }),
+                });
+
+            // Assert
+            var actual = Assert.Single(foundEndpoints);
+            Assert.Same(expected, actual);
+        }
+
         [Fact]
         public void FindEndpoints_AlwaysReturnsEndpointsByRouteName_IgnoringMissingRequiredParameterValues()
         {
@@ -263,7 +307,7 @@ namespace Microsoft.AspNetCore.Routing
             var expected = CreateEndpoint(
                 "api/orders/{id}",
                 defaults: new { controller = "Orders", action = "GetById" },
-                requiredValues: new { controller = "Orders", action = "GetById" },
+                metadataRequiredValues: new { controller = "Orders", action = "GetById" },
                 routeName: "OrdersApi");
             var addressScheme = CreateAddressScheme(expected);
 
@@ -302,7 +346,7 @@ namespace Microsoft.AspNetCore.Routing
             // Arrange
             var endpoint = EndpointFactory.CreateRouteEndpoint(
                 "/a",
-                metadata: new object[] { new SuppressLinkGenerationMetadata(), new EncourageLinkGenerationMetadata(), });
+                metadata: new object[] { new SuppressLinkGenerationMetadata(), new EncourageLinkGenerationMetadata(), new RouteValuesAddressMetadata(string.Empty), });
 
             // Act
             var addressScheme = CreateAddressScheme(endpoint);
@@ -324,7 +368,8 @@ namespace Microsoft.AspNetCore.Routing
         private RouteEndpoint CreateEndpoint(
             string template,
             object defaults = null,
-            object requiredValues = null,
+            object metadataRequiredValues = null,
+            object routePatternRequiredValues = null,
             int order = 0,
             string routeName = null,
             EndpointMetadataCollection metadataCollection = null)
@@ -332,16 +377,16 @@ namespace Microsoft.AspNetCore.Routing
             if (metadataCollection == null)
             {
                 var metadata = new List<object>();
-                if (!string.IsNullOrEmpty(routeName) || requiredValues != null)
+                if (!string.IsNullOrEmpty(routeName) || metadataRequiredValues != null)
                 {
-                    metadata.Add(new RouteValuesAddressMetadata(routeName, new RouteValueDictionary(requiredValues)));
+                    metadata.Add(new RouteValuesAddressMetadata(routeName, new RouteValueDictionary(metadataRequiredValues)));
                 }
                 metadataCollection = new EndpointMetadataCollection(metadata);
             }
 
             return new RouteEndpoint(
                 TestConstants.EmptyRequestDelegate,
-                RoutePatternFactory.Parse(template, defaults, parameterPolicies: null),
+                RoutePatternFactory.Parse(template, defaults, parameterPolicies: null, requiredValues: routePatternRequiredValues),
                 order,
                 metadataCollection,
                 null);

+ 27 - 0
src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Template/RoutePatternPrecedenceTests.cs

@@ -0,0 +1,27 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using Microsoft.AspNetCore.Routing.Patterns;
+
+namespace Microsoft.AspNetCore.Routing.Template
+{
+    public class RoutePatternPrecedenceTests : RoutePrecedenceTestsBase
+    {
+        protected override decimal ComputeMatched(string template)
+        {
+            return ComputeRoutePattern(template, RoutePrecedence.ComputeInbound);
+        }
+
+        protected override decimal ComputeGenerated(string template)
+        {
+            return ComputeRoutePattern(template, RoutePrecedence.ComputeOutbound);
+        }
+
+        private static decimal ComputeRoutePattern(string template, Func<RoutePattern, decimal> func)
+        {
+            var parsed = RoutePatternFactory.Parse(template);
+            return func(parsed);
+        }
+    }
+}

+ 24 - 14
src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Template/RoutePrecedenceTests.cs → src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Template/RoutePrecedenceTestsBase.cs

@@ -2,13 +2,11 @@
 // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 
 using System;
-using Microsoft.Extensions.Options;
-using Moq;
 using Xunit;
 
 namespace Microsoft.AspNetCore.Routing.Template
 {
-    public class RoutePrecedenceTests
+    public abstract class RoutePrecedenceTestsBase
     {
         [Theory]
         [InlineData("Employees/{id}", "Employees/{employeeId}")]
@@ -100,22 +98,34 @@ namespace Microsoft.AspNetCore.Routing.Template
             Assert.True(xPrecedence > yPrecedence);
         }
 
-        private static decimal ComputeMatched(string template)
+        [Fact]
+        public void ComputeGenerated_TooManySegments_ThrowHumaneError()
         {
-            return Compute(template, RoutePrecedence.ComputeInbound);
-        }
-        private static decimal ComputeGenerated(string template)
-        {
-            return Compute(template, RoutePrecedence.ComputeOutbound);
+            var ex = Assert.Throws<InvalidOperationException>(() =>
+            {
+                // Arrange & Act
+                ComputeGenerated("{a}/{b}/{c}/{d}/{e}/{f}/{g}/{h}/{i}/{j}/{k}/{l}/{m}/{n}/{o}/{p}/{q}/{r}/{s}/{t}/{u}/{v}/{w}/{x}/{y}/{z}/{a2}/{b2}/{b3}");
+            });
+
+            // Assert
+            Assert.Equal("Route exceeds the maximum number of allowed segments of 28 and is unable to be processed.", ex.Message);
         }
 
-        private static decimal Compute(string template, Func<RouteTemplate, decimal> func)
+        [Fact]
+        public void ComputeMatched_TooManySegments_ThrowHumaneError()
         {
-            var options = new Mock<IOptions<RouteOptions>>();
-            options.SetupGet(o => o.Value).Returns(new RouteOptions());
+            var ex = Assert.Throws<InvalidOperationException>(() =>
+            {
+                // Arrange & Act
+                ComputeMatched("{a}/{b}/{c}/{d}/{e}/{f}/{g}/{h}/{i}/{j}/{k}/{l}/{m}/{n}/{o}/{p}/{q}/{r}/{s}/{t}/{u}/{v}/{w}/{x}/{y}/{z}/{a2}/{b2}/{b3}");
+            });
 
-            var parsed = TemplateParser.Parse(template);
-            return func(parsed);
+            // Assert
+            Assert.Equal("Route exceeds the maximum number of allowed segments of 28 and is unable to be processed.", ex.Message);
         }
+
+        protected abstract decimal ComputeMatched(string template);
+
+        protected abstract decimal ComputeGenerated(string template);
     }
 }

+ 26 - 0
src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Template/RouteTemplatePrecedenceTests.cs

@@ -0,0 +1,26 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+
+namespace Microsoft.AspNetCore.Routing.Template
+{
+    public class RouteTemplatePrecedenceTests : RoutePrecedenceTestsBase
+    {
+        protected override decimal ComputeMatched(string template)
+        {
+            return ComputeRouteTemplate(template, RoutePrecedence.ComputeInbound);
+        }
+
+        protected override decimal ComputeGenerated(string template)
+        {
+            return ComputeRouteTemplate(template, RoutePrecedence.ComputeOutbound);
+        }
+
+        private static decimal ComputeRouteTemplate(string template, Func<RouteTemplate, decimal> func)
+        {
+            var parsed = TemplateParser.Parse(template);
+            return func(parsed);
+        }
+    }
+}

+ 0 - 7
src/Routing/test/WebSites/Directory.Build.props

@@ -1,11 +1,4 @@
 <Project>
   <!-- Skip the parent folder to prevent getting test package references. -->
   <Import Project="..\..\Directory.Build.props" />
-
-  <PropertyGroup>
-    <DeveloperBuildTestWebsiteTfms>netcoreapp2.2</DeveloperBuildTestWebsiteTfms>
-    <StandardTestWebsiteTfms>$(DeveloperBuildTestWebsiteTfms)</StandardTestWebsiteTfms>
-    <StandardTestWebsiteTfms Condition=" '$(DeveloperBuild)' != 'true' ">netcoreapp2.2</StandardTestWebsiteTfms>
-    <StandardTestWebsiteTfms Condition=" '$(DeveloperBuild)' != 'true' AND '$(OS)' == 'Windows_NT' ">$(StandardTestWebsiteTfms);net461</StandardTestWebsiteTfms>
-  </PropertyGroup>
 </Project>

+ 33 - 0
src/Routing/test/WebSites/RoutingWebSite/HelloExtension/EndpointRouteBuilderExtensions.cs

@@ -0,0 +1,33 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.AspNetCore.Routing.Matching;
+using Microsoft.AspNetCore.Routing.Patterns;
+
+namespace Microsoft.AspNetCore.Builder
+{
+    public static class EndpointRouteBuilderExtensions
+    {
+        public static IEndpointConventionBuilder MapHello(this IEndpointRouteBuilder builder, string template, string greeter)
+        {
+            if (builder == null)
+            {
+                throw new ArgumentNullException(nameof(builder));
+            }
+
+            var pipeline = builder.CreateApplicationBuilder()
+               .UseHello(greeter)
+               .Build();
+
+            return builder.Map(
+                template,
+                "Hello " + greeter,
+                pipeline);
+        }
+    }
+}

+ 25 - 0
src/Routing/test/WebSites/RoutingWebSite/HelloExtension/HelloAppBuilderExtensions.cs

@@ -0,0 +1,25 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using Microsoft.Extensions.Options;
+using RoutingWebSite.HelloExtension;
+
+namespace Microsoft.AspNetCore.Builder
+{
+    public static class HelloAppBuilderExtensions
+    {
+        public static IApplicationBuilder UseHello(this IApplicationBuilder app, string greeter)
+        {
+            if (app == null)
+            {
+                throw new ArgumentNullException(nameof(app));
+            }
+
+            return app.UseMiddleware<HelloMiddleware>(Options.Create(new HelloOptions
+            {
+                Greeter = greeter
+            }));
+        }
+    }
+}

+ 45 - 0
src/Routing/test/WebSites/RoutingWebSite/HelloExtension/HelloMiddleware.cs

@@ -0,0 +1,45 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Options;
+
+namespace RoutingWebSite.HelloExtension
+{
+    public class HelloMiddleware
+    {
+        private readonly RequestDelegate _next;
+        private readonly HelloOptions _helloOptions;
+        private readonly byte[] _helloPayload;
+
+        public HelloMiddleware(RequestDelegate next, IOptions<HelloOptions> helloOptions)
+        {
+            _next = next;
+            _helloOptions = helloOptions.Value;
+
+            var payload = new List<byte>();
+            payload.AddRange(Encoding.UTF8.GetBytes("Hello"));
+            if (!string.IsNullOrEmpty(_helloOptions.Greeter))
+            {
+                payload.Add((byte)' ');
+                payload.AddRange(Encoding.UTF8.GetBytes(_helloOptions.Greeter));
+            }
+            _helloPayload = payload.ToArray();
+        }
+
+        public Task InvokeAsync(HttpContext context)
+        {
+            var response = context.Response;
+            var payloadLength = _helloPayload.Length;
+            response.StatusCode = 200;
+            response.ContentType = "text/plain";
+            response.ContentLength = payloadLength;
+            return response.Body.WriteAsync(_helloPayload, 0, payloadLength);
+        }
+    }
+}

+ 10 - 0
src/Routing/test/WebSites/RoutingWebSite/HelloExtension/HelloOptions.cs

@@ -0,0 +1,10 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+namespace RoutingWebSite.HelloExtension
+{
+    public class HelloOptions
+    {
+        public string Greeter { get; set; }
+    }
+}

+ 1 - 2
src/Routing/test/WebSites/RoutingWebSite/RoutingWebSite.csproj

@@ -1,8 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk.Web">
 
   <PropertyGroup>
-    <TargetFrameworks>netcoreapp2.2</TargetFrameworks>
-    <TargetFrameworks Condition=" '$(OS)' == 'Windows_NT' ">$(TargetFrameworks);net461</TargetFrameworks>
+    <TargetFramework>netcoreapp3.0</TargetFramework>
   </PropertyGroup>
 
   <ItemGroup>

+ 94 - 105
src/Routing/test/WebSites/RoutingWebSite/UseEndpointRoutingStartup.cs

@@ -3,10 +3,12 @@
 
 using System;
 using System.IO;
+using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
 using Microsoft.AspNetCore.Builder;
 using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Features;
 using Microsoft.AspNetCore.Internal;
 using Microsoft.AspNetCore.Routing;
 using Microsoft.AspNetCore.Routing.Internal;
@@ -19,7 +21,7 @@ namespace RoutingWebSite
     public class UseEndpointRoutingStartup
     {
         private static readonly byte[] _homePayload = Encoding.UTF8.GetBytes("Endpoint Routing sample endpoints:" + Environment.NewLine + "/plaintext");
-        private static readonly byte[] _helloWorldPayload = Encoding.UTF8.GetBytes("Hello, World!");
+        private static readonly byte[] _plainTextPayload = Encoding.UTF8.GetBytes("Plain text!");
 
         public void ConfigureServices(IServiceCollection services)
         {
@@ -29,114 +31,91 @@ namespace RoutingWebSite
             {
                 options.ConstraintMap.Add("endsWith", typeof(EndsWithStringRouteConstraint));
             });
-
-            var endpointDataSource = new DefaultEndpointDataSource(new[]
-                {
-                    new RouteEndpoint((httpContext) =>
-                        {
-                            var response = httpContext.Response;
-                            var payloadLength = _homePayload.Length;
-                            response.StatusCode = 200;
-                            response.ContentType = "text/plain";
-                            response.ContentLength = payloadLength;
-                            return response.Body.WriteAsync(_homePayload, 0, payloadLength);
-                        },
-                        RoutePatternFactory.Parse("/"),
-                        0,
-                        EndpointMetadataCollection.Empty,
-                        "Home"),
-                    new RouteEndpoint((httpContext) =>
-                        {
-                            var response = httpContext.Response;
-                            var payloadLength = _helloWorldPayload.Length;
-                            response.StatusCode = 200;
-                            response.ContentType = "text/plain";
-                            response.ContentLength = payloadLength;
-                            return response.Body.WriteAsync(_helloWorldPayload, 0, payloadLength);
-                        },
-                         RoutePatternFactory.Parse("/plaintext"),
-                        0,
-                        EndpointMetadataCollection.Empty,
-                        "Plaintext"),
-                    new RouteEndpoint((httpContext) =>
-                        {
-                            var response = httpContext.Response;
-                            response.StatusCode = 200;
-                            response.ContentType = "text/plain";
-                            return response.WriteAsync("WithConstraints");
-                        },
-                        RoutePatternFactory.Parse("/withconstraints/{id:endsWith(_001)}"),
-                        0,
-                        EndpointMetadataCollection.Empty,
-                        "withconstraints"),
-                    new RouteEndpoint((httpContext) =>
-                        {
-                            var response = httpContext.Response;
-                            response.StatusCode = 200;
-                            response.ContentType = "text/plain";
-                            return response.WriteAsync("withoptionalconstraints");
-                        },
-                        RoutePatternFactory.Parse("/withoptionalconstraints/{id:endsWith(_001)?}"),
-                        0,
-                        EndpointMetadataCollection.Empty,
-                        "withoptionalconstraints"),
-                    new RouteEndpoint((httpContext) =>
-                        {
-                            using (var writer = new StreamWriter(httpContext.Response.Body, Encoding.UTF8, 1024, leaveOpen: true))
-                            {
-                                var graphWriter = httpContext.RequestServices.GetRequiredService<DfaGraphWriter>();
-                                var dataSource = httpContext.RequestServices.GetRequiredService<CompositeEndpointDataSource>();
-                                graphWriter.Write(dataSource, writer);
-                            }
-
-                            return Task.CompletedTask;
-                        },
-                        RoutePatternFactory.Parse("/graph"),
-                        0,
-                        new EndpointMetadataCollection(new HttpMethodMetadata(new[]{ "GET", })),
-                        "DFA Graph"),
-                    new RouteEndpoint((httpContext) =>
-                        {
-                            var linkGenerator = httpContext.RequestServices.GetRequiredService<LinkGenerator>();
-
-                            var response = httpContext.Response;
-                            response.StatusCode = 200;
-                            response.ContentType = "text/plain";
-                            return response.WriteAsync(
-                                "Link: " + linkGenerator.GetPathByRouteValues(httpContext, "WithSingleAsteriskCatchAll", new { }));
-                        },
-                        RoutePatternFactory.Parse("/WithSingleAsteriskCatchAll/{*path}"),
-                        0,
-                        new EndpointMetadataCollection(
-                            new RouteValuesAddressMetadata(
-                                routeName: "WithSingleAsteriskCatchAll",
-                                requiredValues: new RouteValueDictionary())),
-                        "WithSingleAsteriskCatchAll"),
-                    new RouteEndpoint((httpContext) =>
-                        {
-                            var linkGenerator = httpContext.RequestServices.GetRequiredService<LinkGenerator>();
-
-                            var response = httpContext.Response;
-                            response.StatusCode = 200;
-                            response.ContentType = "text/plain";
-                            return response.WriteAsync(
-                                "Link: " + linkGenerator.GetPathByRouteValues(httpContext, "WithDoubleAsteriskCatchAll", new { }));
-                        },
-                        RoutePatternFactory.Parse("/WithDoubleAsteriskCatchAll/{**path}"),
-                        0,
-                        new EndpointMetadataCollection(
-                            new RouteValuesAddressMetadata(
-                                routeName: "WithDoubleAsteriskCatchAll",
-                                requiredValues: new RouteValueDictionary())),
-                        "WithDoubleAsteriskCatchAll"),
-                });
-
-            services.TryAddEnumerable(ServiceDescriptor.Singleton<EndpointDataSource>(endpointDataSource));
         }
 
         public void Configure(IApplicationBuilder app)
         {
-            app.UseEndpointRouting();
+            app.UseEndpointRouting(routes =>
+            {
+                routes.MapHello("/helloworld", "World");
+
+                routes.MapGet(
+                    "/",
+                    (httpContext) =>
+                    {
+                        var dataSource = httpContext.RequestServices.GetRequiredService<EndpointDataSource>();
+
+                        var sb = new StringBuilder();
+                        sb.AppendLine("Endpoints:");
+                        foreach (var endpoint in dataSource.Endpoints.OfType<RouteEndpoint>().OrderBy(e => e.RoutePattern.RawText, StringComparer.OrdinalIgnoreCase))
+                        {
+                            sb.AppendLine($"- {endpoint.RoutePattern.RawText}");
+                        }
+
+                        var response = httpContext.Response;
+                        response.StatusCode = 200;
+                        response.ContentType = "text/plain";
+                        return response.WriteAsync(sb.ToString());
+                    });
+                routes.MapGet(
+                    "/plaintext",
+                    (httpContext) =>
+                    {
+                        var response = httpContext.Response;
+                        var payloadLength = _plainTextPayload.Length;
+                        response.StatusCode = 200;
+                        response.ContentType = "text/plain";
+                        response.ContentLength = payloadLength;
+                        return response.Body.WriteAsync(_plainTextPayload, 0, payloadLength);
+                    });
+                routes.MapGet(
+                    "/withconstraints/{id:endsWith(_001)}",
+                    (httpContext) =>
+                    {
+                        var response = httpContext.Response;
+                        response.StatusCode = 200;
+                        response.ContentType = "text/plain";
+                        return response.WriteAsync("WithConstraints");
+                    });
+                routes.MapGet(
+                    "/withoptionalconstraints/{id:endsWith(_001)?}",
+                    (httpContext) =>
+                    {
+                        var response = httpContext.Response;
+                        response.StatusCode = 200;
+                        response.ContentType = "text/plain";
+                        return response.WriteAsync("withoptionalconstraints");
+                    });
+                routes.MapGet(
+                    "/WithSingleAsteriskCatchAll/{*path}",
+                    (httpContext) =>
+                    {
+                        var linkGenerator = httpContext.RequestServices.GetRequiredService<LinkGenerator>();
+
+                        var response = httpContext.Response;
+                        response.StatusCode = 200;
+                        response.ContentType = "text/plain";
+                        return response.WriteAsync(
+                            "Link: " + linkGenerator.GetPathByRouteValues(httpContext, "WithSingleAsteriskCatchAll", new { }));
+                    },
+                    new RouteValuesAddressMetadata(routeName: "WithSingleAsteriskCatchAll", requiredValues: new RouteValueDictionary()));
+                routes.MapGet(
+                    "/WithDoubleAsteriskCatchAll/{**path}",
+                    (httpContext) =>
+                    {
+                        var linkGenerator = httpContext.RequestServices.GetRequiredService<LinkGenerator>();
+
+                        var response = httpContext.Response;
+                        response.StatusCode = 200;
+                        response.ContentType = "text/plain";
+                        return response.WriteAsync(
+                            "Link: " + linkGenerator.GetPathByRouteValues(httpContext, "WithDoubleAsteriskCatchAll", new { }));
+                    },
+                    new RouteValuesAddressMetadata(routeName: "WithDoubleAsteriskCatchAll", requiredValues: new RouteValueDictionary()));
+            });
+
+            app.Map("/Branch1", branch => SetupBranch(branch, "Branch1"));
+            app.Map("/Branch2", branch => SetupBranch(branch, "Branch2"));
 
             app.UseStaticFiles();
 
@@ -144,5 +123,15 @@ namespace RoutingWebSite
 
             app.UseEndpoint();
         }
+
+        private void SetupBranch(IApplicationBuilder app, string name)
+        {
+            app.UseEndpointRouting(routes =>
+            {
+                routes.MapGet("api/get/{id}", (context) => context.Response.WriteAsync($"{name} - API Get {context.GetRouteData().Values["id"]}"));
+            });
+
+            app.UseEndpoint();
+        }
     }
 }

+ 11 - 0
src/Routing/test/WebSites/RoutingWebSite/UseRouterStartup.cs

@@ -38,6 +38,17 @@ namespace RoutingWebSite
                         defaults: new { lastName = "Doe" },
                         constraints: new { lastName = new RegexRouteConstraint(new Regex("[a-zA-Z]{3}", RegexOptions.CultureInvariant, RegexMatchTimeout)) });
             });
+
+            app.Map("/Branch1", branch => SetupBranch(branch, "Branch1"));
+            app.Map("/Branch2", branch => SetupBranch(branch, "Branch2"));
+        }
+
+        private void SetupBranch(IApplicationBuilder app, string name)
+        {
+            app.UseRouter(routes =>
+            {
+                routes.MapGet("api/get/{id}", (request, response, routeData) => response.WriteAsync($"{name} - API Get {routeData.Values["id"]}"));
+            });
         }
     }
 }

+ 1 - 1
src/Routing/tools/Swaggatherer/Swaggatherer.csproj

@@ -2,7 +2,7 @@
 
   <PropertyGroup>
     <OutputType>Exe</OutputType>
-    <TargetFramework>netcoreapp2.0</TargetFramework>
+    <TargetFramework>netcoreapp3.0</TargetFramework>
   </PropertyGroup>
 
   <ItemGroup>

+ 3 - 9
src/Routing/version.props

@@ -1,12 +1,6 @@
-<Project>
+<Project>
   <PropertyGroup>
-    <VersionPrefix>2.2.0</VersionPrefix>
-    <VersionSuffix>rtm</VersionSuffix>
-    <PackageVersion Condition="'$(IsFinalBuild)' == 'true' AND '$(VersionSuffix)' == 'rtm' ">$(VersionPrefix)</PackageVersion>
-    <PackageVersion Condition="'$(IsFinalBuild)' == 'true' AND '$(VersionSuffix)' != 'rtm' ">$(VersionPrefix)-$(VersionSuffix)-final</PackageVersion>
-    <BuildNumber Condition="'$(BuildNumber)' == ''">t000</BuildNumber>
-    <FeatureBranchVersionPrefix Condition="'$(FeatureBranchVersionPrefix)' == ''">a-</FeatureBranchVersionPrefix>
-    <VersionSuffix Condition="'$(VersionSuffix)' != '' And '$(FeatureBranchVersionSuffix)' != ''">$(FeatureBranchVersionPrefix)$(VersionSuffix)-$([System.Text.RegularExpressions.Regex]::Replace('$(FeatureBranchVersionSuffix)', '[^\w-]', '-'))</VersionSuffix>
-    <VersionSuffix Condition="'$(VersionSuffix)' != '' And '$(BuildNumber)' != ''">$(VersionSuffix)-$(BuildNumber)</VersionSuffix>
+    <VersionPrefix>3.0.0</VersionPrefix>
+    <VersionSuffix Condition=" '$(VersionSuffix)' == '' ">dev</VersionSuffix>
   </PropertyGroup>
 </Project>