Stephen Halter 5 лет назад
Родитель
Сommit
3f0bec1608
43 измененных файлов с 1965 добавлено и 153 удалено
  1. 18 0
      AspNetCore.sln
  2. 20 0
      src/Http/Http.Abstractions/src/IResult.cs
  3. 16 0
      src/Http/Http.Abstractions/src/Metadata/IFromBodyMetadata.cs
  4. 16 0
      src/Http/Http.Abstractions/src/Metadata/IFromFormMetadata.cs
  5. 16 0
      src/Http/Http.Abstractions/src/Metadata/IFromHeaderMetadata.cs
  6. 16 0
      src/Http/Http.Abstractions/src/Metadata/IFromQueryMetadata.cs
  7. 16 0
      src/Http/Http.Abstractions/src/Metadata/IFromRouteMetadata.cs
  8. 12 0
      src/Http/Http.Abstractions/src/Metadata/IFromServiceMetadata.cs
  9. 13 0
      src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt
  10. 1 1
      src/Http/Http/src/QueryCollection.cs
  11. 29 28
      src/Http/HttpAbstractions.slnf
  12. 16 0
      src/Http/Routing/samples/MapActionSample/MapActionSample.csproj
  13. 26 0
      src/Http/Routing/samples/MapActionSample/Program.cs
  14. 13 0
      src/Http/Routing/samples/MapActionSample/Properties/launchSettings.json
  15. 49 0
      src/Http/Routing/samples/MapActionSample/Startup.cs
  16. 9 0
      src/Http/Routing/samples/MapActionSample/Todo.cs
  17. 9 0
      src/Http/Routing/samples/MapActionSample/appsettings.Development.json
  18. 10 0
      src/Http/Routing/samples/MapActionSample/appsettings.json
  19. 33 0
      src/Http/Routing/src/Builder/MapActionEndpointConventionBuilder.cs
  20. 80 0
      src/Http/Routing/src/Builder/MapActionEndpointRouteBuilderExtensions.cs
  21. 19 0
      src/Http/Routing/src/IRouteOrderMetadata.cs
  22. 16 0
      src/Http/Routing/src/IRoutePatternMetadata.cs
  23. 463 0
      src/Http/Routing/src/Internal/MapActionExpressionTreeBuilder.cs
  24. 1 0
      src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj
  25. 8 0
      src/Http/Routing/src/PublicAPI.Unshipped.txt
  26. 71 0
      src/Http/Routing/test/FunctionalTests/MapActionTest.cs
  27. 1 0
      src/Http/Routing/test/FunctionalTests/Microsoft.AspNetCore.Routing.FunctionalTests.csproj
  28. 71 0
      src/Http/Routing/test/UnitTests/Builder/MapActionEndpointRouteBuilderExtensionsTest.cs
  29. 675 0
      src/Http/Routing/test/UnitTests/Internal/MapActionExpressionTreeBuilderTest.cs
  30. 31 0
      src/Http/Routing/test/UnitTests/TestObjects/CustomRouteMetadataAttribute.cs
  31. 10 4
      src/Mvc/Mvc.Core/src/AcceptVerbsAttribute.cs
  32. 6 1
      src/Mvc/Mvc.Core/src/FromBodyAttribute.cs
  33. 2 1
      src/Mvc/Mvc.Core/src/FromFormAttribute.cs
  34. 2 1
      src/Mvc/Mvc.Core/src/FromHeaderAttribute.cs
  35. 2 1
      src/Mvc/Mvc.Core/src/FromQueryAttribute.cs
  36. 6 2
      src/Mvc/Mvc.Core/src/FromRouteAttribute.cs
  37. 2 1
      src/Mvc/Mvc.Core/src/FromServicesAttribute.cs
  38. 12 1
      src/Mvc/Mvc.Core/src/JsonResult.cs
  39. 11 3
      src/Mvc/Mvc.Core/src/Routing/HttpMethodAttribute.cs
  40. 12 1
      src/Mvc/Mvc.Core/src/Routing/IRouteTemplateProvider.cs
  41. 21 3
      src/Mvc/Mvc.Core/src/StatusCodeResult.cs
  42. 4 4
      src/Mvc/Mvc.Core/test/ApplicationModels/ControllerActionDescriptorProviderTests.cs
  43. 101 101
      src/Mvc/Mvc.slnf

+ 18 - 0
AspNetCore.sln

@@ -1572,6 +1572,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "BrowserTesting", "BrowserTe
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.BrowserTesting", "src\Shared\BrowserTesting\src\Microsoft.AspNetCore.BrowserTesting.csproj", "{B739074E-6652-4F5B-B37E-775DC2245FEC}"
 EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{722E5A66-D84A-4689-AA87-7197FF5D7070}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MapActionSample", "src\Http\Routing\samples\MapActionSample\MapActionSample.csproj", "{8F510BAA-FA6B-4648-8F98-28DF5C69DBB2}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -7451,6 +7455,18 @@ Global
 		{B739074E-6652-4F5B-B37E-775DC2245FEC}.Release|x64.Build.0 = Release|Any CPU
 		{B739074E-6652-4F5B-B37E-775DC2245FEC}.Release|x86.ActiveCfg = Release|Any CPU
 		{B739074E-6652-4F5B-B37E-775DC2245FEC}.Release|x86.Build.0 = Release|Any CPU
+		{8F510BAA-FA6B-4648-8F98-28DF5C69DBB2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{8F510BAA-FA6B-4648-8F98-28DF5C69DBB2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{8F510BAA-FA6B-4648-8F98-28DF5C69DBB2}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{8F510BAA-FA6B-4648-8F98-28DF5C69DBB2}.Debug|x64.Build.0 = Debug|Any CPU
+		{8F510BAA-FA6B-4648-8F98-28DF5C69DBB2}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{8F510BAA-FA6B-4648-8F98-28DF5C69DBB2}.Debug|x86.Build.0 = Debug|Any CPU
+		{8F510BAA-FA6B-4648-8F98-28DF5C69DBB2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{8F510BAA-FA6B-4648-8F98-28DF5C69DBB2}.Release|Any CPU.Build.0 = Release|Any CPU
+		{8F510BAA-FA6B-4648-8F98-28DF5C69DBB2}.Release|x64.ActiveCfg = Release|Any CPU
+		{8F510BAA-FA6B-4648-8F98-28DF5C69DBB2}.Release|x64.Build.0 = Release|Any CPU
+		{8F510BAA-FA6B-4648-8F98-28DF5C69DBB2}.Release|x86.ActiveCfg = Release|Any CPU
+		{8F510BAA-FA6B-4648-8F98-28DF5C69DBB2}.Release|x86.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
@@ -8227,6 +8243,8 @@ Global
 		{22EA0993-8DFC-40C2-8481-8E85E21EFB56} = {41BB7BA4-AC08-4E9A-83EA-6D587A5B951C}
 		{8F33439F-5532-45D6-8A44-20EF9104AA9D} = {5F0044F2-4C66-46A8-BD79-075F001AA034}
 		{B739074E-6652-4F5B-B37E-775DC2245FEC} = {8F33439F-5532-45D6-8A44-20EF9104AA9D}
+		{722E5A66-D84A-4689-AA87-7197FF5D7070} = {54C42F57-5447-4C21-9812-4AF665567566}
+		{8F510BAA-FA6B-4648-8F98-28DF5C69DBB2} = {722E5A66-D84A-4689-AA87-7197FF5D7070}
 	EndGlobalSection
 	GlobalSection(ExtensibilityGlobals) = postSolution
 		SolutionGuid = {3E8720B3-DBDD-498C-B383-2CC32A054E8F}

+ 20 - 0
src/Http/Http.Abstractions/src/IResult.cs

@@ -0,0 +1,20 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Threading.Tasks;
+
+namespace Microsoft.AspNetCore.Http
+{
+    /// <summary>
+    /// Defines a contract that represents the result of an HTTP endpoint.
+    /// </summary>
+    public interface IResult
+    {
+        /// <summary>
+        /// Write an HTTP response reflecting the result.
+        /// </summary>
+        /// <param name="httpContext">The <see cref="HttpContext"/> for the current request.</param>
+        /// <returns>A task that represents the asynchronous execute operation.</returns>
+        Task ExecuteAsync(HttpContext httpContext);
+    }
+}

+ 16 - 0
src/Http/Http.Abstractions/src/Metadata/IFromBodyMetadata.cs

@@ -0,0 +1,16 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+namespace Microsoft.AspNetCore.Http.Metadata
+{
+    /// <summary>
+    /// Interface marking attributes that specify a parameter should be bound using the request body.
+    /// </summary>
+    public interface IFromBodyMetadata
+    {
+        /// <summary>
+        /// Gets whether empty input should be rejected or treated as valid.
+        /// </summary>
+        bool AllowEmpty => false;
+    }
+}

+ 16 - 0
src/Http/Http.Abstractions/src/Metadata/IFromFormMetadata.cs

@@ -0,0 +1,16 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+namespace Microsoft.AspNetCore.Http.Metadata
+{
+    /// <summary>
+    /// Interface marking attributes that specify a parameter should be bound using form-data in the request body.
+    /// </summary>
+    public interface IFromFormMetadata
+    {
+        /// <summary>
+        /// The form field name.
+        /// </summary>
+        string? Name { get; }
+    }
+}

+ 16 - 0
src/Http/Http.Abstractions/src/Metadata/IFromHeaderMetadata.cs

@@ -0,0 +1,16 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+namespace Microsoft.AspNetCore.Http.Metadata
+{
+    /// <summary>
+    /// Interface marking attributes that specify a parameter should be bound using the request headers.
+    /// </summary>
+    public interface IFromHeaderMetadata
+    {
+        /// <summary>
+        /// The request header name.
+        /// </summary>
+        string? Name { get; }
+    }
+}

+ 16 - 0
src/Http/Http.Abstractions/src/Metadata/IFromQueryMetadata.cs

@@ -0,0 +1,16 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+namespace Microsoft.AspNetCore.Http.Metadata
+{
+    /// <summary>
+    /// Interface marking attributes that specify a parameter should be bound using the request query string.
+    /// </summary>
+    public interface IFromQueryMetadata
+    {
+        /// <summary>
+        /// The name of the query string field.
+        /// </summary>
+        string? Name { get; }
+    }
+}

+ 16 - 0
src/Http/Http.Abstractions/src/Metadata/IFromRouteMetadata.cs

@@ -0,0 +1,16 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+namespace Microsoft.AspNetCore.Http.Metadata
+{
+    /// <summary>
+    /// Interface marking attributes that specify a parameter should be bound using route-data from the current request.
+    /// </summary>
+    public interface IFromRouteMetadata
+    {
+        /// <summary>
+        /// The <see cref="HttpRequest.RouteValues"/> name.
+        /// </summary>
+        string? Name { get; }
+    }
+}

+ 12 - 0
src/Http/Http.Abstractions/src/Metadata/IFromServiceMetadata.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.
+
+namespace Microsoft.AspNetCore.Http.Metadata
+{
+    /// <summary>
+    /// Interface marking attributes that specify a parameter should be bound using request services.
+    /// </summary>
+    public interface IFromServiceMetadata
+    {
+    }
+}

+ 13 - 0
src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt

@@ -4,6 +4,19 @@
 *REMOVED*Microsoft.AspNetCore.Routing.RouteValueDictionary.TryAdd(string! key, object! value) -> bool
 *REMOVED*static Microsoft.AspNetCore.Builder.UseMiddlewareExtensions.UseMiddleware(this Microsoft.AspNetCore.Builder.IApplicationBuilder! app, System.Type! middleware, params object![]! args) -> Microsoft.AspNetCore.Builder.IApplicationBuilder!
 *REMOVED*static Microsoft.AspNetCore.Builder.UseMiddlewareExtensions.UseMiddleware<TMiddleware>(this Microsoft.AspNetCore.Builder.IApplicationBuilder! app, params object![]! args) -> Microsoft.AspNetCore.Builder.IApplicationBuilder!
+Microsoft.AspNetCore.Http.IResult
+Microsoft.AspNetCore.Http.IResult.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task!
+Microsoft.AspNetCore.Http.Metadata.IFromBodyMetadata
+Microsoft.AspNetCore.Http.Metadata.IFromBodyMetadata.AllowEmpty.get -> bool
+Microsoft.AspNetCore.Http.Metadata.IFromFormMetadata
+Microsoft.AspNetCore.Http.Metadata.IFromFormMetadata.Name.get -> string?
+Microsoft.AspNetCore.Http.Metadata.IFromHeaderMetadata
+Microsoft.AspNetCore.Http.Metadata.IFromHeaderMetadata.Name.get -> string?
+Microsoft.AspNetCore.Http.Metadata.IFromQueryMetadata
+Microsoft.AspNetCore.Http.Metadata.IFromQueryMetadata.Name.get -> string?
+Microsoft.AspNetCore.Http.Metadata.IFromRouteMetadata
+Microsoft.AspNetCore.Http.Metadata.IFromRouteMetadata.Name.get -> string?
+Microsoft.AspNetCore.Http.Metadata.IFromServiceMetadata
 Microsoft.AspNetCore.Http.Endpoint.Endpoint(Microsoft.AspNetCore.Http.RequestDelegate? requestDelegate, Microsoft.AspNetCore.Http.EndpointMetadataCollection? metadata, string? displayName) -> void
 Microsoft.AspNetCore.Http.Endpoint.RequestDelegate.get -> Microsoft.AspNetCore.Http.RequestDelegate?
 Microsoft.AspNetCore.Routing.RouteValueDictionary.TryAdd(string! key, object? value) -> bool

+ 1 - 1
src/Http/Http/src/QueryCollection.cs

@@ -24,7 +24,7 @@ namespace Microsoft.AspNetCore.Http
         private static readonly IEnumerator<KeyValuePair<string, StringValues>> EmptyIEnumeratorType = EmptyEnumerator;
         private static readonly IEnumerator EmptyIEnumerator = EmptyEnumerator;
 
-        private Dictionary<string, StringValues>? Store { get; set; }
+        private Dictionary<string, StringValues>? Store { get; }
 
         /// <summary>
         /// Initializes a new instance of <see cref="QueryCollection"/>.

+ 29 - 28
src/Http/HttpAbstractions.slnf

@@ -1,53 +1,54 @@
-{
+{
   "solution": {
     "path": "..\\..\\AspNetCore.sln",
-    "projects" : [
+    "projects": [
+      "src\\Hosting\\Abstractions\\src\\Microsoft.AspNetCore.Hosting.Abstractions.csproj",
+      "src\\Hosting\\Hosting\\src\\Microsoft.AspNetCore.Hosting.csproj",
+      "src\\Hosting\\Server.Abstractions\\src\\Microsoft.AspNetCore.Hosting.Server.Abstractions.csproj",
+      "src\\Hosting\\TestHost\\src\\Microsoft.AspNetCore.TestHost.csproj",
       "src\\Http\\Authentication.Abstractions\\src\\Microsoft.AspNetCore.Authentication.Abstractions.csproj",
       "src\\Http\\Authentication.Core\\src\\Microsoft.AspNetCore.Authentication.Core.csproj",
       "src\\Http\\Authentication.Core\\test\\Microsoft.AspNetCore.Authentication.Core.Test.csproj",
       "src\\Http\\Headers\\src\\Microsoft.Net.Http.Headers.csproj",
       "src\\Http\\Headers\\test\\Microsoft.Net.Http.Headers.Tests.csproj",
-      "src\\Http\\Http\\src\\Microsoft.AspNetCore.Http.csproj",
-      "src\\Http\\Http\\test\\Microsoft.AspNetCore.Http.Tests.csproj",
       "src\\Http\\Http.Abstractions\\src\\Microsoft.AspNetCore.Http.Abstractions.csproj",
       "src\\Http\\Http.Abstractions\\test\\Microsoft.AspNetCore.Http.Abstractions.Tests.csproj",
       "src\\Http\\Http.Extensions\\src\\Microsoft.AspNetCore.Http.Extensions.csproj",
       "src\\Http\\Http.Extensions\\test\\Microsoft.AspNetCore.Http.Extensions.Tests.csproj",
       "src\\Http\\Http.Features\\src\\Microsoft.AspNetCore.Http.Features.csproj",
       "src\\Http\\Http.Features\\test\\Microsoft.AspNetCore.Http.Features.Tests.csproj",
+      "src\\Http\\Http\\perf\\Microsoft.AspNetCore.Http.Performance.csproj",
+      "src\\Http\\Http\\src\\Microsoft.AspNetCore.Http.csproj",
+      "src\\Http\\Http\\test\\Microsoft.AspNetCore.Http.Tests.csproj",
+      "src\\Http\\Metadata\\src\\Microsoft.AspNetCore.Metadata.csproj",
       "src\\Http\\Owin\\src\\Microsoft.AspNetCore.Owin.csproj",
       "src\\Http\\Owin\\test\\Microsoft.AspNetCore.Owin.Tests.csproj",
-      "src\\Http\\samples\\SampleApp\\HttpAbstractions.SampleApp.csproj",
-      "src\\Http\\WebUtilities\\src\\Microsoft.AspNetCore.WebUtilities.csproj",
-      "src\\Http\\WebUtilities\\test\\Microsoft.AspNetCore.WebUtilities.Tests.csproj",
-      "src\\Http\\Http\\perf\\Microsoft.AspNetCore.Http.Performance.csproj",
+      "src\\Http\\Routing.Abstractions\\src\\Microsoft.AspNetCore.Routing.Abstractions.csproj",
+      "src\\Http\\Routing.Abstractions\\test\\Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests.csproj",
       "src\\Http\\Routing\\perf\\Microsoft.AspNetCore.Routing.Performance.csproj",
+      "src\\Http\\Routing\\samples\\MapActionSample\\MapActionSample.csproj",
       "src\\Http\\Routing\\src\\Microsoft.AspNetCore.Routing.csproj",
       "src\\Http\\Routing\\test\\FunctionalTests\\Microsoft.AspNetCore.Routing.FunctionalTests.csproj",
       "src\\Http\\Routing\\test\\UnitTests\\Microsoft.AspNetCore.Routing.Tests.csproj",
-      "src\\Http\\Routing.Abstractions\\src\\Microsoft.AspNetCore.Routing.Abstractions.csproj",
-      "src\\Http\\Routing.Abstractions\\test\\Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests.csproj",
-      "src\\Hosting\\TestHost\\src\\Microsoft.AspNetCore.TestHost.csproj",
-      "src\\Http\\Routing\\test\\testassets\\RoutingWebSite\\RoutingWebSite.csproj",
-      "src\\Http\\Routing\\test\\testassets\\RoutingSandbox\\RoutingSandbox.csproj",
       "src\\Http\\Routing\\test\\testassets\\Benchmarks\\Benchmarks.csproj",
-      "src\\Servers\\Kestrel\\Kestrel\\src\\Microsoft.AspNetCore.Server.Kestrel.csproj",
-      "src\\Middleware\\StaticFiles\\src\\Microsoft.AspNetCore.StaticFiles.csproj",
-      "src\\Servers\\Kestrel\\Core\\src\\Microsoft.AspNetCore.Server.Kestrel.Core.csproj",
-      "src\\Hosting\\Hosting\\src\\Microsoft.AspNetCore.Hosting.csproj",
-      "src\\Servers\\Connections.Abstractions\\src\\Microsoft.AspNetCore.Connections.Abstractions.csproj",
-      "src\\Hosting\\Abstractions\\src\\Microsoft.AspNetCore.Hosting.Abstractions.csproj",
-      "src\\Hosting\\Server.Abstractions\\src\\Microsoft.AspNetCore.Hosting.Server.Abstractions.csproj",
-      "src\\Servers\\Kestrel\\Transport.Sockets\\src\\Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.csproj",
-      "src\\Middleware\\HttpOverrides\\src\\Microsoft.AspNetCore.HttpOverrides.csproj",
-      "src\\Servers\\IIS\\IISIntegration\\src\\Microsoft.AspNetCore.Server.IISIntegration.csproj",
+      "src\\Http\\Routing\\test\\testassets\\RoutingSandbox\\RoutingSandbox.csproj",
+      "src\\Http\\Routing\\test\\testassets\\RoutingWebSite\\RoutingWebSite.csproj",
       "src\\Http\\WebUtilities\\perf\\Microsoft.AspNetCore.WebUtilities.Performance\\Microsoft.AspNetCore.WebUtilities.Performance.csproj",
-      "src\\Security\\Authorization\\Policy\\src\\Microsoft.AspNetCore.Authorization.Policy.csproj",
+      "src\\Http\\WebUtilities\\src\\Microsoft.AspNetCore.WebUtilities.csproj",
+      "src\\Http\\WebUtilities\\test\\Microsoft.AspNetCore.WebUtilities.Tests.csproj",
+      "src\\Http\\samples\\SampleApp\\HttpAbstractions.SampleApp.csproj",
       "src\\Middleware\\CORS\\src\\Microsoft.AspNetCore.Cors.csproj",
-      "src\\Http\\Metadata\\src\\Microsoft.AspNetCore.Metadata.csproj",
+      "src\\Middleware\\HttpOverrides\\src\\Microsoft.AspNetCore.HttpOverrides.csproj",
+      "src\\Middleware\\StaticFiles\\src\\Microsoft.AspNetCore.StaticFiles.csproj",
       "src\\ObjectPool\\src\\Microsoft.Extensions.ObjectPool.csproj",
-      "src\\WebEncoders\\src\\Microsoft.Extensions.WebEncoders.csproj",
-      "src\\Testing\\src\\Microsoft.AspNetCore.Testing.csproj"
+      "src\\Security\\Authorization\\Policy\\src\\Microsoft.AspNetCore.Authorization.Policy.csproj",
+      "src\\Servers\\Connections.Abstractions\\src\\Microsoft.AspNetCore.Connections.Abstractions.csproj",
+      "src\\Servers\\IIS\\IISIntegration\\src\\Microsoft.AspNetCore.Server.IISIntegration.csproj",
+      "src\\Servers\\Kestrel\\Core\\src\\Microsoft.AspNetCore.Server.Kestrel.Core.csproj",
+      "src\\Servers\\Kestrel\\Kestrel\\src\\Microsoft.AspNetCore.Server.Kestrel.csproj",
+      "src\\Servers\\Kestrel\\Transport.Sockets\\src\\Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.csproj",
+      "src\\Testing\\src\\Microsoft.AspNetCore.Testing.csproj",
+      "src\\WebEncoders\\src\\Microsoft.Extensions.WebEncoders.csproj"
     ]
   }
-}
+}

+ 16 - 0
src/Http/Routing/samples/MapActionSample/MapActionSample.csproj

@@ -0,0 +1,16 @@
+<Project Sdk="Microsoft.NET.Sdk.Web">
+
+  <PropertyGroup>
+    <TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <Reference Include="Microsoft.AspNetCore" />
+    <Reference Include="Microsoft.AspNetCore.Diagnostics" />
+    <Reference Include="Microsoft.AspNetCore.Hosting" />
+    <!-- Mvc.Core is referenced only for its attributes -->
+    <Reference Include="Microsoft.AspNetCore.Mvc.Core" />
+    <Reference Include="Microsoft.AspNetCore.Server.Kestrel" />
+  </ItemGroup>
+
+</Project>

+ 26 - 0
src/Http/Routing/samples/MapActionSample/Program.cs

@@ -0,0 +1,26 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+
+namespace HttpApiSampleApp
+{
+    public class Program
+    {
+        public static void Main(string[] args)
+        {
+            CreateHostBuilder(args).Build().Run();
+        }
+
+        public static IHostBuilder CreateHostBuilder(string[] args) =>
+            Host.CreateDefaultBuilder(args)
+                .ConfigureWebHostDefaults(webBuilder =>
+                {
+                    webBuilder.UseStartup<Startup>();
+                });
+    }
+}

+ 13 - 0
src/Http/Routing/samples/MapActionSample/Properties/launchSettings.json

@@ -0,0 +1,13 @@
+{
+  "profiles": {
+    "HttpApiSampleApp": {
+      "commandName": "Project",
+      "dotnetRunMessages": "true",
+      "launchBrowser": true,
+      "applicationUrl": "https://localhost:5001;http://localhost:5000",
+      "environmentVariables": {
+        "ASPNETCORE_ENVIRONMENT": "Development"
+      }
+    }
+  }
+}

+ 49 - 0
src/Http/Routing/samples/MapActionSample/Startup.cs

@@ -0,0 +1,49 @@
+using System;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+
+namespace HttpApiSampleApp
+{
+    public class Startup
+    {
+        // This method gets called by the runtime. Use this method to add services to the container.
+        // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
+        public void ConfigureServices(IServiceCollection services)
+        {
+        }
+
+        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
+        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
+        {
+            if (env.IsDevelopment())
+            {
+                app.UseDeveloperExceptionPage();
+            }
+
+            app.UseRouting();
+
+            app.UseEndpoints(endpoints =>
+            {
+                [HttpPost("/EchoTodo")]
+                JsonResult EchoTodo([FromBody] Todo todo) => new(todo);
+
+                endpoints.MapAction((Func<Todo, JsonResult>)EchoTodo);
+
+                endpoints.MapPost("/EchoTodoProto", async httpContext =>
+                {
+                    var todo = await httpContext.Request.ReadFromJsonAsync<Todo>();
+                    await httpContext.Response.WriteAsJsonAsync(todo);
+                });
+
+                endpoints.MapGet("/", async context =>
+                {
+                    await context.Response.WriteAsync("Hello World!");
+                });
+            });
+        }
+    }
+}

+ 9 - 0
src/Http/Routing/samples/MapActionSample/Todo.cs

@@ -0,0 +1,9 @@
+namespace HttpApiSampleApp
+{
+    public class Todo
+    {
+        public int Id { get; set; }
+        public string Name { get; set; }
+        public bool IsComplete { get; set; }
+    }
+}

+ 9 - 0
src/Http/Routing/samples/MapActionSample/appsettings.Development.json

@@ -0,0 +1,9 @@
+{
+  "Logging": {
+    "LogLevel": {
+      "Default": "Information",
+      "Microsoft": "Warning",
+      "Microsoft.Hosting.Lifetime": "Information"
+    }
+  }
+}

+ 10 - 0
src/Http/Routing/samples/MapActionSample/appsettings.json

@@ -0,0 +1,10 @@
+{
+  "Logging": {
+    "LogLevel": {
+      "Default": "Information",
+      "Microsoft": "Warning",
+      "Microsoft.Hosting.Lifetime": "Information"
+    }
+  },
+  "AllowedHosts": "*"
+}

+ 33 - 0
src/Http/Routing/src/Builder/MapActionEndpointConventionBuilder.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;
+
+namespace Microsoft.AspNetCore.Builder
+{
+    /// <summary>
+    /// Builds conventions that will be used for customization of MapAction <see cref="EndpointBuilder"/> instances.
+    /// </summary>
+    public sealed class MapActionEndpointConventionBuilder : IEndpointConventionBuilder
+    {
+        private readonly List<IEndpointConventionBuilder> _endpointConventionBuilders;
+
+        internal MapActionEndpointConventionBuilder(List<IEndpointConventionBuilder> endpointConventionBuilders)
+        {
+            _endpointConventionBuilders = endpointConventionBuilders;
+        }
+
+        /// <summary>
+        /// Adds the specified convention to the builder. Conventions are used to customize <see cref="EndpointBuilder"/> instances.
+        /// </summary>
+        /// <param name="convention">The convention to add to the builder.</param>
+        public void Add(Action<EndpointBuilder> convention)
+        {
+            foreach (var endpointConventionBuilder in _endpointConventionBuilders)
+            {
+                endpointConventionBuilder.Add(convention);
+            }
+        }
+    }
+}

+ 80 - 0
src/Http/Routing/src/Builder/MapActionEndpointRouteBuilderExtensions.cs

@@ -0,0 +1,80 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.AspNetCore.Routing.Internal;
+
+namespace Microsoft.AspNetCore.Builder
+{
+    /// <summary>
+    /// Provides extension methods for <see cref="IEndpointRouteBuilder"/> to define HTTP API endpoints.
+    /// </summary>
+    public static class MapActionEndpointRouteBuilderExtensions
+    {
+        /// <summary>
+        /// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches the pattern specified via attributes.
+        /// </summary>
+        /// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
+        /// <param name="action">The delegate executed when the endpoint is matched.</param>
+        /// <returns>An <see cref="IEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns>
+        public static MapActionEndpointConventionBuilder MapAction(
+            this IEndpointRouteBuilder endpoints,
+            Delegate action)
+        {
+            if (endpoints is null)
+            {
+                throw new ArgumentNullException(nameof(endpoints));
+            }
+
+            if (action is null)
+            {
+                throw new ArgumentNullException(nameof(action));
+            }
+
+            var requestDelegate = MapActionExpressionTreeBuilder.BuildRequestDelegate(action);
+
+            var routeAttributes = action.Method.GetCustomAttributes().OfType<IRoutePatternMetadata>();
+            var conventionBuilders = new List<IEndpointConventionBuilder>();
+
+            const int defaultOrder = 0;
+
+            foreach (var routeAttribute in routeAttributes)
+            {
+                if (routeAttribute.RoutePattern is not string pattern)
+                {
+                    continue;
+                }
+
+                var routeName = (routeAttribute as IRouteNameMetadata)?.RouteName;
+                var routeOrder = (routeAttribute as IRouteOrderMetadata)?.RouteOrder;
+
+                var conventionBuilder = endpoints.Map(pattern, requestDelegate);
+
+                conventionBuilder.Add(endpointBuilder =>
+                {
+                    foreach (var attribute in action.Method.GetCustomAttributes())
+                    {
+                        endpointBuilder.Metadata.Add(attribute);
+                    }
+
+                    endpointBuilder.DisplayName = routeName ?? pattern;
+
+                    ((RouteEndpointBuilder)endpointBuilder).Order = routeOrder ?? defaultOrder;
+                });
+
+                conventionBuilders.Add(conventionBuilder);
+            }
+
+            if (conventionBuilders.Count == 0)
+            {
+                throw new InvalidOperationException("Action must have a pattern. Is it missing a Route attribute?");
+            }
+
+            return new MapActionEndpointConventionBuilder(conventionBuilders);
+        }
+    }
+}

+ 19 - 0
src/Http/Routing/src/IRouteOrderMetadata.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.
+
+namespace Microsoft.AspNetCore.Routing
+{
+    /// <summary>
+    /// Interface for attributes which can supply a route order for attribute routing.
+    /// </summary>
+    public interface IRouteOrderMetadata
+    {
+        /// <summary>
+        /// Gets the route order. The order determines the order of route execution. Routes with a lower
+        /// order value are tried first. When a route doesn't specify a value, it gets a default value of 0.
+        /// A null value for the Order property means that the user didn't specify an explicit order for the
+        /// route.
+        /// </summary>
+        int? RouteOrder { get; }
+    }
+}

+ 16 - 0
src/Http/Routing/src/IRoutePatternMetadata.cs

@@ -0,0 +1,16 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+namespace Microsoft.AspNetCore.Routing
+{
+    /// <summary>
+    /// Interface for attributes which can supply a route pattern for attribute routing.
+    /// </summary>
+    public interface IRoutePatternMetadata
+    {
+        /// <summary>
+        /// The route pattern. May be <see langword="null"/>.
+        /// </summary>
+        string? RoutePattern { get; }
+    }
+}

+ 463 - 0
src/Http/Routing/src/Internal/MapActionExpressionTreeBuilder.cs

@@ -0,0 +1,463 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Reflection;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Metadata;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Internal;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.Routing.Internal
+{
+    internal static class MapActionExpressionTreeBuilder
+    {
+        private static readonly MethodInfo ChangeTypeMethodInfo = GetMethodInfo<Func<object, Type, object>>((value, type) => Convert.ChangeType(value, type, CultureInfo.InvariantCulture));
+        private static readonly MethodInfo ExecuteTaskOfTMethodInfo = typeof(MapActionExpressionTreeBuilder).GetMethod(nameof(ExecuteTask), BindingFlags.NonPublic | BindingFlags.Static)!;
+        private static readonly MethodInfo ExecuteValueTaskOfTMethodInfo = typeof(MapActionExpressionTreeBuilder).GetMethod(nameof(ExecuteValueTask), BindingFlags.NonPublic | BindingFlags.Static)!;
+        private static readonly MethodInfo ExecuteTaskResultOfTMethodInfo = typeof(MapActionExpressionTreeBuilder).GetMethod(nameof(ExecuteTaskResult), BindingFlags.NonPublic | BindingFlags.Static)!;
+        private static readonly MethodInfo ExecuteValueResultTaskOfTMethodInfo = typeof(MapActionExpressionTreeBuilder).GetMethod(nameof(ExecuteValueTaskResult), BindingFlags.NonPublic | BindingFlags.Static)!;
+        private static readonly MethodInfo GetRequiredServiceMethodInfo = typeof(ServiceProviderServiceExtensions).GetMethod(nameof(ServiceProviderServiceExtensions.GetRequiredService), BindingFlags.Public | BindingFlags.Static, new Type[] { typeof(IServiceProvider) })!;
+        private static readonly MethodInfo ResultWriteResponseAsync = typeof(IResult).GetMethod(nameof(IResult.ExecuteAsync), BindingFlags.Public | BindingFlags.Instance)!;
+        private static readonly MethodInfo StringResultWriteResponseAsync = GetMethodInfo<Func<HttpResponse, string, Task>>((response, text) => HttpResponseWritingExtensions.WriteAsync(response, text, default));
+        private static readonly MethodInfo JsonResultWriteResponseAsync = GetMethodInfo<Func<HttpResponse, object, Task>>((response, value) => HttpResponseJsonExtensions.WriteAsJsonAsync(response, value, default));
+        private static readonly MemberInfo CompletedTaskMemberInfo = GetMemberInfo<Func<Task>>(() => Task.CompletedTask);
+
+        private static readonly ParameterExpression TargetArg = Expression.Parameter(typeof(object), "target");
+        private static readonly ParameterExpression HttpContextParameter = Expression.Parameter(typeof(HttpContext), "httpContext");
+        private static readonly ParameterExpression DeserializedBodyArg = Expression.Parameter(typeof(object), "bodyValue");
+
+        private static readonly MemberExpression RequestServicesExpr = Expression.Property(HttpContextParameter, nameof(HttpContext.RequestServices));
+        private static readonly MemberExpression HttpRequestExpr = Expression.Property(HttpContextParameter, nameof(HttpContext.Request));
+        private static readonly MemberExpression HttpResponseExpr = Expression.Property(HttpContextParameter, nameof(HttpContext.Response));
+
+        public static RequestDelegate BuildRequestDelegate(Delegate action)
+        {
+            // Non void return type
+
+            // Task Invoke(HttpContext httpContext)
+            // {
+            //     // Action parameters are bound from the request, services, etc... based on attribute and type information.
+            //     return ExecuteTask(action(...), httpContext);
+            // }
+
+            // void return type
+
+            // Task Invoke(HttpContext httpContext)
+            // {
+            //     action(...);
+            //     return default;
+            // }
+
+            var method = action.Method;
+
+            var consumeBodyDirectly = false;
+            var consumeBodyAsForm = false;
+            Type? bodyType = null;
+            var allowEmptyBody = false;
+
+            // This argument represents the deserialized body returned from IHttpRequestReader
+            // when the method has a FromBody attribute declared
+
+            var args = new List<Expression>();
+
+            foreach (var parameter in method.GetParameters())
+            {
+                Expression paramterExpression = Expression.Default(parameter.ParameterType);
+
+                if (parameter.GetCustomAttributes().OfType<IFromRouteMetadata>().FirstOrDefault() is { } routeAttribute)
+                {
+                    var routeValuesProperty = Expression.Property(HttpRequestExpr, nameof(HttpRequest.RouteValues));
+                    paramterExpression = BindParamenter(routeValuesProperty, parameter, routeAttribute.Name);
+                }
+                else if (parameter.GetCustomAttributes().OfType<IFromQueryMetadata>().FirstOrDefault() is { } queryAttribute)
+                {
+                    var queryProperty = Expression.Property(HttpRequestExpr, nameof(HttpRequest.Query));
+                    paramterExpression = BindParamenter(queryProperty, parameter, queryAttribute.Name);
+                }
+                else if (parameter.GetCustomAttributes().OfType<IFromHeaderMetadata>().FirstOrDefault() is { } headerAttribute)
+                {
+                    var headersProperty = Expression.Property(HttpRequestExpr, nameof(HttpRequest.Headers));
+                    paramterExpression = BindParamenter(headersProperty, parameter, headerAttribute.Name);
+                }
+                else if (parameter.GetCustomAttributes().OfType<IFromBodyMetadata>().FirstOrDefault() is { } bodyAttribute)
+                {
+                    if (consumeBodyDirectly)
+                    {
+                        throw new InvalidOperationException("Action cannot have more than one FromBody attribute.");
+                    }
+
+                    if (consumeBodyAsForm)
+                    {
+                        ThrowCannotReadBodyDirectlyAndAsForm();
+                    }
+
+                    consumeBodyDirectly = true;
+                    allowEmptyBody = bodyAttribute.AllowEmpty;
+                    bodyType = parameter.ParameterType;
+                    paramterExpression = Expression.Convert(DeserializedBodyArg, bodyType);
+                }
+                else if (parameter.GetCustomAttributes().OfType<IFromFormMetadata>().FirstOrDefault() is { } formAttribute)
+                {
+                    if (consumeBodyDirectly)
+                    {
+                        ThrowCannotReadBodyDirectlyAndAsForm();
+                    }
+
+                    consumeBodyAsForm = true;
+
+                    var formProperty = Expression.Property(HttpRequestExpr, nameof(HttpRequest.Form));
+                    paramterExpression = BindParamenter(formProperty, parameter, parameter.Name);
+                }
+                else if (parameter.CustomAttributes.Any(a => typeof(IFromServiceMetadata).IsAssignableFrom(a.AttributeType)))
+                {
+                    paramterExpression = Expression.Call(GetRequiredServiceMethodInfo.MakeGenericMethod(parameter.ParameterType), RequestServicesExpr);
+                }
+                else
+                {
+                    if (parameter.ParameterType == typeof(IFormCollection))
+                    {
+                        if (consumeBodyDirectly)
+                        {
+                            ThrowCannotReadBodyDirectlyAndAsForm();
+                        }
+
+                        consumeBodyAsForm = true;
+
+                        paramterExpression = Expression.Property(HttpRequestExpr, nameof(HttpRequest.Form));
+                    }
+                    else if (parameter.ParameterType == typeof(HttpContext))
+                    {
+                        paramterExpression = HttpContextParameter;
+                    }
+                }
+
+                args.Add(paramterExpression);
+            }
+
+            Expression? body = null;
+
+            MethodCallExpression methodCall;
+
+            if (action.Target is null)
+            {
+                methodCall = Expression.Call(method, args);
+            }
+            else
+            {
+                var castedTarget = Expression.Convert(TargetArg, action.Target.GetType());
+                methodCall = Expression.Call(castedTarget, method, args);
+            }
+
+            // Exact request delegate match
+            if (method.ReturnType == typeof(void))
+            {
+                var bodyExpressions = new List<Expression>
+                {
+                    methodCall,
+                    Expression.Property(null, (PropertyInfo)CompletedTaskMemberInfo)
+                };
+
+                body = Expression.Block(bodyExpressions);
+            }
+            else if (AwaitableInfo.IsTypeAwaitable(method.ReturnType, out var info))
+            {
+                if (method.ReturnType == typeof(Task))
+                {
+                    body = methodCall;
+                }
+                else if (method.ReturnType.IsGenericType &&
+                         method.ReturnType.GetGenericTypeDefinition() == typeof(Task<>))
+                {
+                    var typeArg = method.ReturnType.GetGenericArguments()[0];
+
+                    if (typeof(IResult).IsAssignableFrom(typeArg))
+                    {
+                        body = Expression.Call(
+                                           ExecuteTaskResultOfTMethodInfo.MakeGenericMethod(typeArg),
+                                           methodCall,
+                                           TargetArg,
+                                           HttpContextParameter);
+                    }
+                    else
+                    {
+                        // ExecuteTask<T>(action(..), httpContext);
+                        body = Expression.Call(
+                                           ExecuteTaskOfTMethodInfo.MakeGenericMethod(typeArg),
+                                           methodCall,
+                                           TargetArg,
+                                           HttpContextParameter);
+                    }
+                }
+                else if (method.ReturnType.IsGenericType &&
+                         method.ReturnType.GetGenericTypeDefinition() == typeof(ValueTask<>))
+                {
+                    var typeArg = method.ReturnType.GetGenericArguments()[0];
+
+                    if (typeof(IResult).IsAssignableFrom(typeArg))
+                    {
+                        body = Expression.Call(
+                                           ExecuteValueResultTaskOfTMethodInfo.MakeGenericMethod(typeArg),
+                                           methodCall,
+                                           TargetArg,
+                                           HttpContextParameter);
+                    }
+                    else
+                    {
+                        // ExecuteTask<T>(action(..), httpContext);
+                        body = Expression.Call(
+                                       ExecuteValueTaskOfTMethodInfo.MakeGenericMethod(typeArg),
+                                       methodCall,
+                                       TargetArg,
+                                       HttpContextParameter);
+                    }
+                }
+                else
+                {
+                    // TODO: Handle custom awaitables
+                    throw new NotSupportedException($"Unsupported return type: {method.ReturnType}");
+                }
+            }
+            else if (typeof(IResult).IsAssignableFrom(method.ReturnType))
+            {
+                body = Expression.Call(methodCall, ResultWriteResponseAsync, HttpContextParameter);
+            }
+            else if (method.ReturnType == typeof(string))
+            {
+                body = Expression.Call(StringResultWriteResponseAsync, HttpResponseExpr, methodCall, Expression.Constant(CancellationToken.None));
+            }
+            else
+            {
+                body = Expression.Call(JsonResultWriteResponseAsync, HttpResponseExpr, methodCall, Expression.Constant(CancellationToken.None));
+            }
+
+            Func<object?, HttpContext, Task>? requestDelegate = null;
+
+            if (consumeBodyDirectly)
+            {
+                // We need to generate the code for reading from the body before calling into the delegate
+                var lambda = Expression.Lambda<Func<object?, HttpContext, object?, Task>>(body, TargetArg, HttpContextParameter, DeserializedBodyArg);
+                var invoker = lambda.Compile();
+                object? defaultBodyValue = null;
+
+                if (allowEmptyBody && bodyType!.IsValueType)
+                {
+                    defaultBodyValue = Activator.CreateInstance(bodyType);
+                }
+
+                requestDelegate = async (target, httpContext) =>
+                {
+                    object? bodyValue;
+
+                    if (allowEmptyBody && httpContext.Request.ContentLength == 0)
+                    {
+                        bodyValue = defaultBodyValue;
+                    }
+                    else
+                    {
+                        try
+                        {
+                            bodyValue = await httpContext.Request.ReadFromJsonAsync(bodyType!);
+                        }
+                        catch (IOException ex)
+                        {
+                            Log.RequestBodyIOException(GetLogger(httpContext), ex);
+                            httpContext.Abort();
+                            return;
+                        }
+                        catch (InvalidDataException ex)
+                        {
+                            Log.RequestBodyInvalidDataException(GetLogger(httpContext), ex);
+                            httpContext.Response.StatusCode = 400;
+                            return;
+                        }
+                    }
+
+                    await invoker(target, httpContext, bodyValue);
+                };
+            }
+            else if (consumeBodyAsForm)
+            {
+                var lambda = Expression.Lambda<Func<object?, HttpContext, Task>>(body, TargetArg, HttpContextParameter);
+                var invoker = lambda.Compile();
+
+                requestDelegate = async (target, httpContext) =>
+                {
+                    // Generating async code would just be insane so if the method needs the form populate it here
+                    // so the within the method it's cached
+                    try
+                    {
+                        await httpContext.Request.ReadFormAsync();
+                    }
+                    catch (IOException ex)
+                    {
+                        Log.RequestBodyIOException(GetLogger(httpContext), ex);
+                        httpContext.Abort();
+                        return;
+                    }
+                    catch (InvalidDataException ex)
+                    {
+                        Log.RequestBodyInvalidDataException(GetLogger(httpContext), ex);
+                        httpContext.Response.StatusCode = 400;
+                        return;
+                    }
+
+                    await invoker(target, httpContext);
+                };
+            }
+            else
+            {
+                var lambda = Expression.Lambda<Func<object?, HttpContext, Task>>(body, TargetArg, HttpContextParameter);
+                var invoker = lambda.Compile();
+
+                requestDelegate = invoker;
+            }
+
+            return httpContext =>
+            {
+                return requestDelegate(action.Target, httpContext);
+            };
+        }
+
+        private static ILogger GetLogger(HttpContext httpContext)
+        {
+            var loggerFactory = httpContext.RequestServices.GetRequiredService<ILoggerFactory>();
+            return loggerFactory.CreateLogger("Microsoft.AspNetCore.Routing.MapAction");
+        }
+
+        private static Expression BindParamenter(Expression sourceExpression, ParameterInfo parameter, string? name)
+        {
+            var key = name ?? parameter.Name;
+            var type = Nullable.GetUnderlyingType(parameter.ParameterType) ?? parameter.ParameterType;
+            var valueArg = Expression.Convert(
+                                Expression.MakeIndex(sourceExpression,
+                                                     sourceExpression.Type.GetProperty("Item"),
+                                                     new[] { Expression.Constant(key) }),
+                                typeof(string));
+
+            MethodInfo parseMethod = (from m in type.GetMethods(BindingFlags.Public | BindingFlags.Static)
+                                      let parameters = m.GetParameters()
+                                      where m.Name == "Parse" && parameters.Length == 1 && parameters[0].ParameterType == typeof(string)
+                                      select m).FirstOrDefault()!;
+
+            Expression? expr = null;
+
+            if (parseMethod != null)
+            {
+                expr = Expression.Call(parseMethod, valueArg);
+            }
+            else if (parameter.ParameterType != valueArg.Type)
+            {
+                // Convert.ChangeType()
+                expr = Expression.Call(ChangeTypeMethodInfo, valueArg, Expression.Constant(type));
+            }
+            else
+            {
+                expr = valueArg;
+            }
+
+            if (expr.Type != parameter.ParameterType)
+            {
+                expr = Expression.Convert(expr, parameter.ParameterType);
+            }
+
+            // property[key] == null ? default : (ParameterType){Type}.Parse(property[key]);
+            expr = Expression.Condition(
+                Expression.Equal(valueArg, Expression.Constant(null)),
+                Expression.Default(parameter.ParameterType),
+                expr);
+
+            return expr;
+        }
+
+        private static MethodInfo GetMethodInfo<T>(Expression<T> expr)
+        {
+            var mc = (MethodCallExpression)expr.Body;
+            return mc.Method;
+        }
+
+        private static MemberInfo GetMemberInfo<T>(Expression<T> expr)
+        {
+            var mc = (MemberExpression)expr.Body;
+            return mc.Member;
+        }
+
+        private static async ValueTask ExecuteTask<T>(Task<T> task, HttpContext httpContext)
+        {
+            await httpContext.Response.WriteAsJsonAsync(await task);
+        }
+
+        private static Task ExecuteValueTask<T>(ValueTask<T> task, HttpContext httpContext)
+        {
+            static async Task ExecuteAwaited(ValueTask<T> task, HttpContext httpContext)
+            {
+                await httpContext.Response.WriteAsJsonAsync(await task);
+            }
+
+            if (task.IsCompletedSuccessfully)
+            {
+                return httpContext.Response.WriteAsJsonAsync(task.GetAwaiter().GetResult());
+            }
+
+            return ExecuteAwaited(task, httpContext);
+        }
+
+        private static Task ExecuteValueTaskResult<T>(ValueTask<T> task, HttpContext httpContext) where T : IResult
+        {
+            static async Task ExecuteAwaited(ValueTask<T> task, HttpContext httpContext)
+            {
+                await (await task).ExecuteAsync(httpContext);
+            }
+
+            if (task.IsCompletedSuccessfully)
+            {
+                return task.GetAwaiter().GetResult().ExecuteAsync(httpContext);
+            }
+
+            return ExecuteAwaited(task, httpContext);
+        }
+
+        private static async ValueTask ExecuteTaskResult<T>(Task<T> task, HttpContext httpContext) where T : IResult
+        {
+            await (await task).ExecuteAsync(httpContext);
+        }
+
+        [StackTraceHidden]
+        private static void ThrowCannotReadBodyDirectlyAndAsForm()
+        {
+            throw new InvalidOperationException("Action cannot mix FromBody and FromForm on the same method.");
+        }
+
+        private static class Log
+        {
+            private static readonly Action<ILogger, Exception> _requestBodyIOException = LoggerMessage.Define(
+                LogLevel.Debug,
+                new EventId(1, "RequestBodyIOException"),
+                "Reading the request body failed with an IOException.");
+
+            private static readonly Action<ILogger, Exception> _requestBodyInvalidDataException = LoggerMessage.Define(
+                LogLevel.Debug,
+                new EventId(2, "RequestBodyInvalidDataException"),
+                "Reading the request body failed with an InvalidDataException.");
+
+            public static void RequestBodyIOException(ILogger logger, IOException exception)
+            {
+                _requestBodyIOException(logger, exception);
+            }
+
+            public static void RequestBodyInvalidDataException(ILogger logger, InvalidDataException exception)
+            {
+                _requestBodyInvalidDataException(logger, exception);
+            }
+        }
+    }
+}

+ 1 - 0
src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj

@@ -24,6 +24,7 @@ Microsoft.AspNetCore.Routing.RouteCollection</Description>
 
   <ItemGroup>
     <Compile Include="$(SharedSourceRoot)PropertyHelper\*.cs" />
+    <Compile Include="$(SharedSourceRoot)ObjectMethodExecutor\**\*.cs" />
   </ItemGroup>
 
   <ItemGroup>

+ 8 - 0
src/Http/Routing/src/PublicAPI.Unshipped.txt

@@ -5,9 +5,17 @@
 *REMOVED*Microsoft.AspNetCore.Routing.IRouteNameMetadata.RouteName.get -> string!
 *REMOVED*Microsoft.AspNetCore.Routing.RouteNameMetadata.RouteName.get -> string!
 *REMOVED*Microsoft.AspNetCore.Routing.RouteNameMetadata.RouteNameMetadata(string! routeName) -> void
+Microsoft.AspNetCore.Builder.MapActionEndpointConventionBuilder
+Microsoft.AspNetCore.Builder.MapActionEndpointConventionBuilder.Add(System.Action<Microsoft.AspNetCore.Builder.EndpointBuilder!>! convention) -> void
+Microsoft.AspNetCore.Builder.MapActionEndpointRouteBuilderExtensions
 Microsoft.AspNetCore.Routing.DataTokensMetadata.DataTokens.get -> System.Collections.Generic.IReadOnlyDictionary<string!, object?>!
 Microsoft.AspNetCore.Routing.DataTokensMetadata.DataTokensMetadata(System.Collections.Generic.IReadOnlyDictionary<string!, object?>! dataTokens) -> void
 Microsoft.AspNetCore.Routing.IDataTokensMetadata.DataTokens.get -> System.Collections.Generic.IReadOnlyDictionary<string!, object?>!
 Microsoft.AspNetCore.Routing.IRouteNameMetadata.RouteName.get -> string?
+Microsoft.AspNetCore.Routing.IRouteOrderMetadata
+Microsoft.AspNetCore.Routing.IRouteOrderMetadata.RouteOrder.get -> int?
+Microsoft.AspNetCore.Routing.IRoutePatternMetadata
+Microsoft.AspNetCore.Routing.IRoutePatternMetadata.RoutePattern.get -> string?
 Microsoft.AspNetCore.Routing.RouteNameMetadata.RouteName.get -> string?
 Microsoft.AspNetCore.Routing.RouteNameMetadata.RouteNameMetadata(string? routeName) -> void
+static Microsoft.AspNetCore.Builder.MapActionEndpointRouteBuilderExtensions.MapAction(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! endpoints, System.Delegate! action) -> Microsoft.AspNetCore.Builder.MapActionEndpointConventionBuilder!

+ 71 - 0
src/Http/Routing/test/FunctionalTests/MapActionTest.cs

@@ -0,0 +1,71 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+#nullable enable
+
+using System;
+using System.Net.Http.Json;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Routing.FunctionalTests
+{
+    public class MapActionTest
+    {
+        [Fact]
+        public async Task MapAction_FromBodyWorksWithJsonPayload()
+        {
+            [HttpPost("/EchoTodo/{id}")]
+            Todo EchoTodo([FromRoute] int id, [FromBody] Todo todo) => todo with { Id = id };
+
+            using var host = new HostBuilder()
+                .ConfigureWebHost(webHostBuilder =>
+                {
+                    webHostBuilder
+                        .Configure(app =>
+                        {
+                            app.UseRouting();
+                            app.UseEndpoints(b => b.MapAction((Func<int, Todo, Todo>)EchoTodo));
+                        })
+                        .UseTestServer();
+                })
+                .ConfigureServices(services =>
+                {
+                    services.AddRouting();
+                })
+                .Build();
+
+            using var server = host.GetTestServer();
+            await host.StartAsync();
+            var client = server.CreateClient();
+
+            var todo = new Todo
+            {
+                Name = "Write tests!"
+            };
+
+            var response = await client.PostAsJsonAsync("/EchoTodo/42", todo);
+            response.EnsureSuccessStatusCode();
+
+            var echoedTodo = await response.Content.ReadFromJsonAsync<Todo>();
+
+            Assert.NotNull(echoedTodo);
+            Assert.Equal(todo.Name, echoedTodo?.Name);
+            Assert.Equal(42, echoedTodo?.Id);
+        }
+
+        private record Todo
+        {
+            public int Id { get; set; }
+            public string Name { get; set; } = "Todo";
+            public bool IsComplete { get; set; }
+        }
+    }
+}

+ 1 - 0
src/Http/Routing/test/FunctionalTests/Microsoft.AspNetCore.Routing.FunctionalTests.csproj

@@ -12,6 +12,7 @@
   <ItemGroup>
     <Reference Include="Microsoft.AspNetCore.Authorization.Policy" />
     <Reference Include="Microsoft.AspNetCore.Cors" />
+    <Reference Include="Microsoft.AspNetCore.Mvc.Core" />
     <Reference Include="Microsoft.AspNetCore.Routing" />
     <Reference Include="Microsoft.AspNetCore.TestHost" />
   </ItemGroup>

+ 71 - 0
src/Http/Routing/test/UnitTests/Builder/MapActionEndpointRouteBuilderExtensionsTest.cs

@@ -0,0 +1,71 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+#nullable enable
+
+using System;
+using System.Linq;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.AspNetCore.Routing.TestObjects;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Builder
+{
+    public class MapActionEndpointDataSourceBuilderExtensionsTest
+    {
+        private ModelEndpointDataSource GetBuilderEndpointDataSource(IEndpointRouteBuilder endpointRouteBuilder)
+        {
+            return Assert.IsType<ModelEndpointDataSource>(Assert.Single(endpointRouteBuilder.DataSources));
+        }
+
+        private RouteEndpointBuilder GetRouteEndpointBuilder(IEndpointRouteBuilder endpointRouteBuilder)
+        {
+            return Assert.IsType<RouteEndpointBuilder>(Assert.Single(GetBuilderEndpointDataSource(endpointRouteBuilder).EndpointBuilders));
+        }
+
+        [Fact]
+        public void MapAction_BuildsEndpointFromAttributes()
+        {
+            const string customPattern = "/CustomTemplate";
+            const string customMethod = "CUSTOM_METHOD";
+
+            [CustomRouteMetadata(Pattern = customPattern, Methods = new[] { customMethod })]
+            void TestAction() { };
+
+            var builder = new DefaultEndpointRouteBuilder(Mock.Of<IApplicationBuilder>());
+            _ = builder.MapAction((Action)TestAction);
+
+            var routeEndpointBuilder = GetRouteEndpointBuilder(builder);
+            Assert.Equal(customPattern, routeEndpointBuilder.RoutePattern.RawText);
+
+            var dataSource = GetBuilderEndpointDataSource(builder);
+            var endpoint = Assert.Single(dataSource.Endpoints);
+
+            var httpMethodMetadata = Assert.Single(endpoint.Metadata.OfType<IHttpMethodMetadata>());
+            var method = Assert.Single(httpMethodMetadata.HttpMethods);
+            Assert.Equal(customMethod, method);
+        }
+
+        [Fact]
+        public void MapAction_BuildsEndpointWithRouteNameAndOrder()
+        {
+            const string customName = "Custom Name";
+            const int customOrder = 1337;
+
+            [CustomRouteMetadata(Name = customName, Order = customOrder)]
+            void TestAction() { };
+
+            var builder = new DefaultEndpointRouteBuilder(Mock.Of<IApplicationBuilder>());
+            _ = builder.MapAction((Action)TestAction);
+
+            var dataSource = GetBuilderEndpointDataSource(builder);
+            // Trigger Endpoint build by calling getter.
+            var endpoint = Assert.Single(dataSource.Endpoints);
+
+            var routeEndpointBuilder = GetRouteEndpointBuilder(builder);
+            Assert.Equal(customName, routeEndpointBuilder.DisplayName);
+            Assert.Equal(customOrder, routeEndpointBuilder.Order);
+        }
+    }
+}

+ 675 - 0
src/Http/Routing/test/UnitTests/Internal/MapActionExpressionTreeBuilderTest.cs

@@ -0,0 +1,675 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+#nullable enable
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Text;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.AspNetCore.Http.Metadata;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Testing;
+using Microsoft.Extensions.Primitives;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Routing.Internal
+{
+    public class MapActionExpressionTreeBuilderTest
+    {
+        [Fact]
+        public async Task RequestDelegateInvokesAction()
+        {
+            var invoked = false;
+
+            void TestAction()
+            {
+                invoked = true;
+            }
+
+            var requestDelegate = MapActionExpressionTreeBuilder.BuildRequestDelegate((Action)TestAction);
+
+            await requestDelegate(null!);
+
+            Assert.True(invoked);
+        }
+
+        [Fact]
+        public async Task RequestDelegatePopulatesFromRouteParameterBasedOnParameterName()
+        {
+            const string paramName = "value";
+            const int originalRouteParam = 42;
+
+            int? deserializedRouteParam = null;
+
+            void TestAction([FromRoute] int value)
+            {
+                deserializedRouteParam = value;
+            }
+
+            var httpContext = new DefaultHttpContext();
+            httpContext.Request.RouteValues[paramName] = originalRouteParam.ToString(NumberFormatInfo.InvariantInfo);
+
+            var requestDelegate = MapActionExpressionTreeBuilder.BuildRequestDelegate((Action<int>)TestAction);
+
+            await requestDelegate(httpContext);
+
+            Assert.Equal(originalRouteParam, deserializedRouteParam);
+        }
+
+        [Fact]
+        public async Task RequestDelegatePopulatesFromRouteParameterBasedOnAttributeNameProperty()
+        {
+            const string specifiedName = "value";
+            const int originalRouteParam = 42;
+
+            int? deserializedRouteParam = null;
+
+            void TestAction([FromRoute(Name = specifiedName)] int foo)
+            {
+                deserializedRouteParam = foo;
+            }
+
+            var httpContext = new DefaultHttpContext();
+            httpContext.Request.RouteValues[specifiedName] = originalRouteParam.ToString(NumberFormatInfo.InvariantInfo);
+
+            var requestDelegate = MapActionExpressionTreeBuilder.BuildRequestDelegate((Action<int>)TestAction);
+
+            await requestDelegate(httpContext);
+
+            Assert.Equal(originalRouteParam, deserializedRouteParam);
+        }
+
+        [Fact]
+        public async Task UsesDefaultValueIfNoMatchingRouteValue()
+        {
+            const string unmatchedName = "value";
+            const int unmatchedRouteParam = 42;
+
+            int? deserializedRouteParam = null;
+
+            void TestAction([FromRoute] int foo)
+            {
+                deserializedRouteParam = foo;
+            }
+
+            var httpContext = new DefaultHttpContext();
+            httpContext.Request.RouteValues[unmatchedName] = unmatchedRouteParam.ToString(NumberFormatInfo.InvariantInfo);
+
+            var requestDelegate = MapActionExpressionTreeBuilder.BuildRequestDelegate((Action<int>)TestAction);
+
+            await requestDelegate(httpContext);
+
+            Assert.Equal(0, deserializedRouteParam);
+        }
+
+        [Fact]
+        public async Task RequestDelegatePopulatesFromQueryParameterBasedOnParameterName()
+        {
+            const string paramName = "value";
+            const int originalQueryParam = 42;
+
+            int? deserializedRouteParam = null;
+
+            void TestAction([FromQuery] int value)
+            {
+                deserializedRouteParam = value;
+            }
+
+            var query = new QueryCollection(new Dictionary<string, StringValues>()
+            {
+                [paramName] = originalQueryParam.ToString(NumberFormatInfo.InvariantInfo)
+            });
+
+            var httpContext = new DefaultHttpContext();
+            httpContext.Request.Query = query;
+
+            var requestDelegate = MapActionExpressionTreeBuilder.BuildRequestDelegate((Action<int>)TestAction);
+
+            await requestDelegate(httpContext);
+
+            Assert.Equal(originalQueryParam, deserializedRouteParam);
+        }
+
+        [Fact]
+        public async Task RequestDelegatePopulatesFromHeaderParameterBasedOnParameterName()
+        {
+            const string customHeaderName = "X-Custom-Header";
+            const int originalHeaderParam = 42;
+
+            int? deserializedRouteParam = null;
+
+            void TestAction([FromHeader(Name = customHeaderName)] int value)
+            {
+                deserializedRouteParam = value;
+            }
+
+            var httpContext = new DefaultHttpContext();
+            httpContext.Request.Headers[customHeaderName] = originalHeaderParam.ToString(NumberFormatInfo.InvariantInfo);
+
+            var requestDelegate = MapActionExpressionTreeBuilder.BuildRequestDelegate((Action<int>)TestAction);
+
+            await requestDelegate(httpContext);
+
+            Assert.Equal(originalHeaderParam, deserializedRouteParam);
+        }
+
+        [Fact]
+        public async Task RequestDelegatePopulatesFromBodyParameter()
+        {
+            Todo originalTodo = new()
+            {
+                Name = "Write more tests!"
+            };
+
+            Todo? deserializedRequestBody = null;
+
+            void TestAction([FromBody] Todo todo)
+            {
+                deserializedRequestBody = todo;
+            }
+
+            var httpContext = new DefaultHttpContext();
+            httpContext.Request.Headers["Content-Type"] = "application/json";
+
+            var requestBodyBytes = JsonSerializer.SerializeToUtf8Bytes(originalTodo);
+            httpContext.Request.Body = new MemoryStream(requestBodyBytes);
+
+            var requestDelegate = MapActionExpressionTreeBuilder.BuildRequestDelegate((Action<Todo>)TestAction);
+
+            await requestDelegate(httpContext);
+
+            Assert.NotNull(deserializedRequestBody);
+            Assert.Equal(originalTodo.Name, deserializedRequestBody!.Name);
+        }
+
+        [Fact]
+        public async Task RequestDelegateRejectsEmptyBodyGivenDefaultFromBodyParameter()
+        {
+            void TestAction([FromBody] Todo todo)
+            {
+            }
+
+            var httpContext = new DefaultHttpContext();
+            httpContext.Request.Headers["Content-Type"] = "application/json";
+            httpContext.Request.Headers["Content-Length"] = "0";
+
+            var requestDelegate = MapActionExpressionTreeBuilder.BuildRequestDelegate((Action<Todo>)TestAction);
+
+            await Assert.ThrowsAsync<JsonException>(() => requestDelegate(httpContext));
+        }
+
+        [Fact]
+        public async Task RequestDelegateAllowsEmptyBodyGivenCorrectyConfiguredFromBodyParameter()
+        {
+            var todoToBecomeNull = new Todo();
+
+            void TestAction([FromBody(AllowEmpty = true)] Todo todo)
+            {
+                todoToBecomeNull = todo;
+            }
+
+            var httpContext = new DefaultHttpContext();
+            httpContext.Request.Headers["Content-Type"] = "application/json";
+            httpContext.Request.Headers["Content-Length"] = "0";
+
+            var requestDelegate = MapActionExpressionTreeBuilder.BuildRequestDelegate((Action<Todo>)TestAction);
+
+            await requestDelegate(httpContext);
+
+            Assert.Null(todoToBecomeNull);
+        }
+
+        [Fact]
+        public async Task RequestDelegateAllowsEmptyBodyStructGivenCorrectyConfiguredFromBodyParameter()
+        {
+            var structToBeZeroed = new BodyStruct
+            {
+                Id = 42
+            };
+
+            void TestAction([FromBody(AllowEmpty = true)] BodyStruct bodyStruct)
+            {
+                structToBeZeroed = bodyStruct;
+            }
+
+            var httpContext = new DefaultHttpContext();
+            httpContext.Request.Headers["Content-Type"] = "application/json";
+            httpContext.Request.Headers["Content-Length"] = "0";
+
+            var requestDelegate = MapActionExpressionTreeBuilder.BuildRequestDelegate((Action<BodyStruct>)TestAction);
+
+            await requestDelegate(httpContext);
+
+            Assert.Equal(default, structToBeZeroed);
+        }
+
+        [Fact]
+        public async Task RequestDelegateLogsFromBodyIOExceptionsAsDebugAndAborts()
+        {
+            var invoked = false;
+
+            var sink = new TestSink(context => context.LoggerName == "Microsoft.AspNetCore.Routing.MapAction");
+            var testLoggerFactory = new TestLoggerFactory(sink, enabled: true);
+
+            void TestAction([FromBody] Todo todo)
+            {
+                invoked = true;
+            }
+
+            var ioException = new IOException();
+            var serviceCollection = new ServiceCollection();
+            serviceCollection.AddSingleton<ILoggerFactory>(testLoggerFactory);
+
+            var httpContext = new DefaultHttpContext();
+            httpContext.Request.Headers["Content-Type"] = "application/json";
+            httpContext.Request.Body = new IOExceptionThrowingRequestBodyStream(ioException);
+            httpContext.Features.Set<IHttpRequestLifetimeFeature>(new TestHttpRequestLifetimeFeature());
+            httpContext.RequestServices = serviceCollection.BuildServiceProvider();
+
+            var requestDelegate = MapActionExpressionTreeBuilder.BuildRequestDelegate((Action<Todo>)TestAction);
+
+            await requestDelegate(httpContext);
+
+            Assert.False(invoked);
+            Assert.True(httpContext.RequestAborted.IsCancellationRequested);
+
+            var logMessage = Assert.Single(sink.Writes);
+            Assert.Equal(new EventId(1, "RequestBodyIOException"), logMessage.EventId);
+            Assert.Equal(LogLevel.Debug, logMessage.LogLevel);
+            Assert.Same(ioException, logMessage.Exception);
+        }
+
+        [Fact]
+        public async Task RequestDelegateLogsFromBodyInvalidDataExceptionsAsDebugAndSets400Response()
+        {
+            var invoked = false;
+
+            var sink = new TestSink(context => context.LoggerName == "Microsoft.AspNetCore.Routing.MapAction");
+            var testLoggerFactory = new TestLoggerFactory(sink, enabled: true);
+
+            void TestAction([FromBody] Todo todo)
+            {
+                invoked = true;
+            }
+
+            var invalidDataException = new InvalidDataException();
+            var serviceCollection = new ServiceCollection();
+            serviceCollection.AddSingleton<ILoggerFactory>(testLoggerFactory);
+
+            var httpContext = new DefaultHttpContext();
+            httpContext.Request.Headers["Content-Type"] = "application/json";
+            httpContext.Request.Body = new IOExceptionThrowingRequestBodyStream(invalidDataException);
+            httpContext.Features.Set<IHttpRequestLifetimeFeature>(new TestHttpRequestLifetimeFeature());
+            httpContext.RequestServices = serviceCollection.BuildServiceProvider();
+
+            var requestDelegate = MapActionExpressionTreeBuilder.BuildRequestDelegate((Action<Todo>)TestAction);
+
+            await requestDelegate(httpContext);
+
+            Assert.False(invoked);
+            Assert.False(httpContext.RequestAborted.IsCancellationRequested);
+            Assert.Equal(400, httpContext.Response.StatusCode);
+
+            var logMessage = Assert.Single(sink.Writes);
+            Assert.Equal(new EventId(2, "RequestBodyInvalidDataException"), logMessage.EventId);
+            Assert.Equal(LogLevel.Debug, logMessage.LogLevel);
+            Assert.Same(invalidDataException, logMessage.Exception);
+        }
+
+        [Fact]
+        public async Task RequestDelegatePopulatesFromFormParameterBasedOnParameterName()
+        {
+            const string paramName = "value";
+            const int originalQueryParam = 42;
+
+            int? deserializedRouteParam = null;
+
+            void TestAction([FromForm] int value)
+            {
+                deserializedRouteParam = value;
+            }
+
+            var form = new FormCollection(new Dictionary<string, StringValues>()
+            {
+                [paramName] = originalQueryParam.ToString(NumberFormatInfo.InvariantInfo)
+            });
+
+            var httpContext = new DefaultHttpContext();
+            httpContext.Request.Form = form;
+
+            var requestDelegate = MapActionExpressionTreeBuilder.BuildRequestDelegate((Action<int>)TestAction);
+
+            await requestDelegate(httpContext);
+
+            Assert.Equal(originalQueryParam, deserializedRouteParam);
+        }
+
+        [Fact]
+        public async Task RequestDelegateLogsFromFormIOExceptionsAsDebugAndAborts()
+        {
+            var invoked = false;
+
+            var sink = new TestSink(context => context.LoggerName == "Microsoft.AspNetCore.Routing.MapAction");
+            var testLoggerFactory = new TestLoggerFactory(sink, enabled: true);
+
+            void TestAction([FromForm] int value)
+            {
+                invoked = true;
+            }
+
+            var ioException = new IOException();
+            var serviceCollection = new ServiceCollection();
+            serviceCollection.AddSingleton<ILoggerFactory>(testLoggerFactory);
+
+            var httpContext = new DefaultHttpContext();
+            httpContext.Request.Headers["Content-Type"] = "application/x-www-form-urlencoded";
+            httpContext.Request.Body = new IOExceptionThrowingRequestBodyStream(ioException);
+            httpContext.Features.Set<IHttpRequestLifetimeFeature>(new TestHttpRequestLifetimeFeature());
+            httpContext.RequestServices = serviceCollection.BuildServiceProvider();
+
+            var requestDelegate = MapActionExpressionTreeBuilder.BuildRequestDelegate((Action<int>)TestAction);
+
+            await requestDelegate(httpContext);
+
+            Assert.False(invoked);
+            Assert.True(httpContext.RequestAborted.IsCancellationRequested);
+
+            var logMessage = Assert.Single(sink.Writes);
+            Assert.Equal(new EventId(1, "RequestBodyIOException"), logMessage.EventId);
+            Assert.Equal(LogLevel.Debug, logMessage.LogLevel);
+            Assert.Same(ioException, logMessage.Exception);
+        }
+
+        [Fact]
+        public async Task RequestDelegateLogsFromFormInvalidDataExceptionsAsDebugAndSets400Response()
+        {
+            var invoked = false;
+
+            var sink = new TestSink(context => context.LoggerName == "Microsoft.AspNetCore.Routing.MapAction");
+            var testLoggerFactory = new TestLoggerFactory(sink, enabled: true);
+
+            void TestAction([FromForm] int value)
+            {
+                invoked = true;
+            }
+
+            var invalidDataException = new InvalidDataException();
+            var serviceCollection = new ServiceCollection();
+            serviceCollection.AddSingleton<ILoggerFactory>(testLoggerFactory);
+
+            var httpContext = new DefaultHttpContext();
+            httpContext.Request.Headers["Content-Type"] = "application/x-www-form-urlencoded";
+            httpContext.Request.Body = new IOExceptionThrowingRequestBodyStream(invalidDataException);
+            httpContext.Features.Set<IHttpRequestLifetimeFeature>(new TestHttpRequestLifetimeFeature());
+            httpContext.RequestServices = serviceCollection.BuildServiceProvider();
+
+            var requestDelegate = MapActionExpressionTreeBuilder.BuildRequestDelegate((Action<int>)TestAction);
+
+            await requestDelegate(httpContext);
+
+            Assert.False(invoked);
+            Assert.False(httpContext.RequestAborted.IsCancellationRequested);
+            Assert.Equal(400, httpContext.Response.StatusCode);
+
+            var logMessage = Assert.Single(sink.Writes);
+            Assert.Equal(new EventId(2, "RequestBodyInvalidDataException"), logMessage.EventId);
+            Assert.Equal(LogLevel.Debug, logMessage.LogLevel);
+            Assert.Same(invalidDataException, logMessage.Exception);
+        }
+
+        [Fact]
+        public void BuildRequestDelegateThrowsInvalidOperationExceptionGivenBothFromBodyAndFromFormOnDifferentParameters()
+        {
+            void TestAction([FromBody] int value1, [FromForm] int value2) { }
+            void TestActionWithFlippedParams([FromForm] int value1, [FromBody] int value2) { }
+
+            Assert.Throws<InvalidOperationException>(() => MapActionExpressionTreeBuilder.BuildRequestDelegate((Action<int, int>)TestAction));
+            Assert.Throws<InvalidOperationException>(() => MapActionExpressionTreeBuilder.BuildRequestDelegate((Action<int, int>)TestActionWithFlippedParams));
+        }
+
+        [Fact]
+        public void BuildRequestDelegateThrowsInvalidOperationExceptionGivenFromBodyOnMultipleParameters()
+        {
+            void TestAction([FromBody] int value1, [FromBody] int value2) { }
+
+            Assert.Throws<InvalidOperationException>(() => MapActionExpressionTreeBuilder.BuildRequestDelegate((Action<int, int>)TestAction));
+        }
+
+        [Fact]
+        public async Task RequestDelegatePopulatesFromServiceParameterBasedOnParameterType()
+        {
+            var myOriginalService = new MyService();
+            MyService? injectedService = null;
+
+            void TestAction([FromService] MyService myService)
+            {
+                injectedService = myService;
+            }
+
+            var serviceCollection = new ServiceCollection();
+            serviceCollection.AddSingleton(myOriginalService);
+
+            var httpContext = new DefaultHttpContext();
+            httpContext.RequestServices = serviceCollection.BuildServiceProvider();
+
+            var requestDelegate = MapActionExpressionTreeBuilder.BuildRequestDelegate((Action<MyService>)TestAction);
+
+            await requestDelegate(httpContext);
+
+            Assert.Same(myOriginalService, injectedService);
+        }
+
+        [Fact]
+        public async Task RequestDelegatePopulatesHttpContextParameterWithoutAttribute()
+        {
+            HttpContext? httpContextArgument = null;
+
+            void TestAction(HttpContext httpContext)
+            {
+                httpContextArgument = httpContext;
+            }
+
+            var httpContext = new DefaultHttpContext();
+
+            var requestDelegate = MapActionExpressionTreeBuilder.BuildRequestDelegate((Action<HttpContext>)TestAction);
+
+            await requestDelegate(httpContext);
+
+            Assert.Same(httpContext, httpContextArgument);
+        }
+
+        [Fact]
+        public async Task RequestDelegatePopulatesIFormCollectionParameterWithoutAttribute()
+        {
+            IFormCollection? formCollectionArgument = null;
+
+            void TestAction(IFormCollection httpContext)
+            {
+                formCollectionArgument = httpContext;
+            }
+
+            var httpContext = new DefaultHttpContext();
+            httpContext.Request.Headers["Content-Type"] = "application/x-www-form-urlencoded";
+
+            var requestDelegate = MapActionExpressionTreeBuilder.BuildRequestDelegate((Action<IFormCollection>)TestAction);
+
+            await requestDelegate(httpContext);
+
+            Assert.Same(httpContext.Request.Form, formCollectionArgument);
+        }
+
+        [Fact]
+        public async Task RequestDelegateWritesComplexReturnValueAsJsonResponseBody()
+        {
+            Todo originalTodo = new()
+            {
+                Name = "Write even more tests!"
+            };
+
+            Todo TestAction() => originalTodo;
+
+            var httpContext = new DefaultHttpContext();
+            var responseBodyStream = new MemoryStream();
+            httpContext.Response.Body = responseBodyStream;
+
+            var requestDelegate = MapActionExpressionTreeBuilder.BuildRequestDelegate((Func<Todo>)TestAction);
+
+            await requestDelegate(httpContext);
+
+            var deserializedResponseBody = JsonSerializer.Deserialize<Todo>(responseBodyStream.ToArray(), new JsonSerializerOptions
+            {
+                // TODO: the output is "{\"id\":0,\"name\":\"Write even more tests!\",\"isComplete\":false}"
+                // Verify that the camelCased property names are consistent with MVC and if so whether we should keep the behavior.
+                PropertyNameCaseInsensitive = true
+            });
+
+            Assert.NotNull(deserializedResponseBody);
+            Assert.Equal(originalTodo.Name, deserializedResponseBody!.Name);
+        }
+
+        [Fact]
+        public async Task RequestDelegateUsesCustomIResult()
+        {
+            var resultString = "Still not enough tests!";
+
+            CustomResult TestAction() => new(resultString!);
+
+            var httpContext = new DefaultHttpContext();
+            var responseBodyStream = new MemoryStream();
+            httpContext.Response.Body = responseBodyStream;
+
+            var requestDelegate = MapActionExpressionTreeBuilder.BuildRequestDelegate((Func<CustomResult>)TestAction);
+
+            await requestDelegate(httpContext);
+
+            var decodedResponseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray());
+
+            Assert.Equal(resultString, decodedResponseBody);
+        }
+
+        private class Todo
+        {
+            public int Id { get; set; }
+            public string? Name { get; set; } = "Todo";
+            public bool IsComplete { get; set; }
+        }
+
+        private struct BodyStruct
+        {
+            public int Id { get; set; }
+        }
+
+        private class FromRouteAttribute : Attribute, IFromRouteMetadata
+        {
+            public string? Name { get; set; }
+        }
+
+        private class FromQueryAttribute : Attribute, IFromQueryMetadata
+        {
+            public string? Name { get; set; }
+        }
+
+        private class FromHeaderAttribute : Attribute, IFromHeaderMetadata
+        {
+            public string? Name { get; set; }
+        }
+
+        private class FromBodyAttribute : Attribute, IFromBodyMetadata
+        {
+            public bool AllowEmpty { get; set; }
+        }
+
+        private class FromFormAttribute : Attribute, IFromFormMetadata
+        {
+            public string? Name { get; set; }
+        }
+
+        private class FromServiceAttribute : Attribute, IFromServiceMetadata
+        {
+        }
+
+        private class MyService
+        {
+        }
+
+        private class CustomResult : IResult
+        {
+            private readonly string _resultString;
+
+            public CustomResult(string resultString)
+            {
+                _resultString = resultString;
+            }
+
+            public Task ExecuteAsync(HttpContext httpContext)
+            {
+                return httpContext.Response.WriteAsync(_resultString);
+            }
+        }
+
+        private class IOExceptionThrowingRequestBodyStream : Stream
+        {
+            private readonly Exception _exceptionToThrow;
+
+            public IOExceptionThrowingRequestBodyStream(Exception exceptionToThrow)
+            {
+                _exceptionToThrow = exceptionToThrow;
+            }
+
+            public override bool CanRead => true;
+
+            public override bool CanSeek => false;
+
+            public override bool CanWrite => false;
+
+            public override long Length => throw new NotImplementedException();
+
+            public override long Position { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
+
+            public override void Flush()
+            {
+                throw new NotImplementedException();
+            }
+
+            public override int Read(byte[] buffer, int offset, int count)
+            {
+                throw _exceptionToThrow;
+            }
+
+            public override long Seek(long offset, SeekOrigin origin)
+            {
+                throw new NotImplementedException();
+            }
+
+            public override void SetLength(long value)
+            {
+                throw new NotImplementedException();
+            }
+
+            public override void Write(byte[] buffer, int offset, int count)
+            {
+                throw new NotImplementedException();
+            }
+        }
+
+        private class TestHttpRequestLifetimeFeature : IHttpRequestLifetimeFeature
+        {
+            private readonly CancellationTokenSource _requestAbortedCts = new CancellationTokenSource();
+
+            public CancellationToken RequestAborted { get => _requestAbortedCts.Token; set => throw new NotImplementedException(); }
+
+            public void Abort()
+            {
+                _requestAbortedCts.Cancel();
+            }
+        }
+    }
+}

+ 31 - 0
src/Http/Routing/test/UnitTests/TestObjects/CustomRouteMetadataAttribute.cs

@@ -0,0 +1,31 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+#nullable enable
+
+using System;
+using System.Collections.Generic;
+
+namespace Microsoft.AspNetCore.Routing.TestObjects
+{
+    internal class CustomRouteMetadataAttribute : Attribute, IRoutePatternMetadata, IHttpMethodMetadata, IRouteNameMetadata, IRouteOrderMetadata
+    {
+        public string Pattern { get; set; } = "/";
+
+        public string? Name { get; set; }
+
+        public int Order { get; set; } = 0;
+
+        public string[] Methods { get; set; } = new[] { "GET" };
+
+        string? IRoutePatternMetadata.RoutePattern => Pattern;
+
+        string? IRouteNameMetadata.RouteName => Name;
+
+        int? IRouteOrderMetadata.RouteOrder => Order;
+
+        IReadOnlyList<string> IHttpMethodMetadata.HttpMethods => Methods;
+
+        bool IHttpMethodMetadata.AcceptCorsPreflight => false;
+    }
+}

+ 10 - 4
src/Mvc/Mvc.Core/src/AcceptVerbsAttribute.cs

@@ -5,6 +5,7 @@ using System;
 using System.Collections.Generic;
 using System.Linq;
 using Microsoft.AspNetCore.Mvc.Routing;
+using Microsoft.AspNetCore.Routing;
 
 namespace Microsoft.AspNetCore.Mvc
 {
@@ -12,8 +13,10 @@ namespace Microsoft.AspNetCore.Mvc
     /// Specifies what HTTP methods an action supports.
     /// </summary>
     [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
-    public sealed class AcceptVerbsAttribute : Attribute, IActionHttpMethodProvider, IRouteTemplateProvider
+    public sealed class AcceptVerbsAttribute : Attribute, IHttpMethodMetadata, IActionHttpMethodProvider, IRouteTemplateProvider
     {
+        private readonly List<string> _httpMethods;
+
         private int? _order;
 
         /// <summary>
@@ -35,13 +38,16 @@ namespace Microsoft.AspNetCore.Mvc
         /// <param name="methods">The HTTP methods the action supports.</param>
         public AcceptVerbsAttribute(params string[] methods)
         {
-            HttpMethods = methods.Select(method => method.ToUpperInvariant());
+            _httpMethods = methods.Select(method => method.ToUpperInvariant()).ToList();
         }
 
         /// <summary>
         /// Gets the HTTP methods the action supports.
         /// </summary>
-        public IEnumerable<string> HttpMethods { get; }
+        public IEnumerable<string> HttpMethods => _httpMethods;
+
+        IReadOnlyList<string> IHttpMethodMetadata.HttpMethods => _httpMethods;
+        bool IHttpMethodMetadata.AcceptCorsPreflight => false;
 
         /// <summary>
         /// The route template. May be null.
@@ -69,4 +75,4 @@ namespace Microsoft.AspNetCore.Mvc
         /// <inheritdoc />
         public string Name { get; set; }
     }
-}
+}

+ 6 - 1
src/Mvc/Mvc.Core/src/FromBodyAttribute.cs

@@ -3,6 +3,7 @@
 
 using System;
 using Microsoft.AspNetCore.Mvc.ModelBinding;
+using Microsoft.AspNetCore.Http.Metadata;
 
 namespace Microsoft.AspNetCore.Mvc
 {
@@ -10,7 +11,7 @@ namespace Microsoft.AspNetCore.Mvc
     /// Specifies that a parameter or property should be bound using the request body.
     /// </summary>
     [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
-    public class FromBodyAttribute : Attribute, IBindingSourceMetadata, IConfigureEmptyBodyBehavior
+    public class FromBodyAttribute : Attribute, IBindingSourceMetadata, IConfigureEmptyBodyBehavior, IFromBodyMetadata
     {
         /// <inheritdoc />
         public BindingSource BindingSource => BindingSource.Body;
@@ -24,5 +25,9 @@ namespace Microsoft.AspNetCore.Mvc
         /// Specifying <see cref="EmptyBodyBehavior.Allow"/> or <see cref="EmptyBodyBehavior.Disallow" /> will override the framework defaults.
         /// </remarks>
         public EmptyBodyBehavior EmptyBodyBehavior { get; set; }
+
+        // Since the default behavior is to reject empty bodies if MvcOptions.AllowEmptyInputInBodyModelBinding is not configured,
+        // we'll consider EmptyBodyBehavior.Default the same as EmptyBodyBehavior.Disallow.
+        bool IFromBodyMetadata.AllowEmpty => EmptyBodyBehavior == EmptyBodyBehavior.Allow;
     }
 }

+ 2 - 1
src/Mvc/Mvc.Core/src/FromFormAttribute.cs

@@ -2,6 +2,7 @@
 // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 
 using System;
+using Microsoft.AspNetCore.Http.Metadata;
 using Microsoft.AspNetCore.Mvc.ModelBinding;
 
 namespace Microsoft.AspNetCore.Mvc
@@ -10,7 +11,7 @@ namespace Microsoft.AspNetCore.Mvc
     /// Specifies that a parameter or property should be bound using form-data in the request body.
     /// </summary>
     [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
-    public class FromFormAttribute : Attribute, IBindingSourceMetadata, IModelNameProvider
+    public class FromFormAttribute : Attribute, IBindingSourceMetadata, IModelNameProvider, IFromFormMetadata
     {
         /// <inheritdoc />
         public BindingSource BindingSource => BindingSource.Form;

+ 2 - 1
src/Mvc/Mvc.Core/src/FromHeaderAttribute.cs

@@ -2,6 +2,7 @@
 // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 
 using System;
+using Microsoft.AspNetCore.Http.Metadata;
 using Microsoft.AspNetCore.Mvc.ModelBinding;
 
 namespace Microsoft.AspNetCore.Mvc
@@ -10,7 +11,7 @@ namespace Microsoft.AspNetCore.Mvc
     /// Specifies that a parameter or property should be bound using the request headers.
     /// </summary>
     [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
-    public class FromHeaderAttribute : Attribute, IBindingSourceMetadata, IModelNameProvider
+    public class FromHeaderAttribute : Attribute, IBindingSourceMetadata, IModelNameProvider, IFromHeaderMetadata
     {
         /// <inheritdoc />
         public BindingSource BindingSource => BindingSource.Header;

+ 2 - 1
src/Mvc/Mvc.Core/src/FromQueryAttribute.cs

@@ -2,6 +2,7 @@
 // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 
 using System;
+using Microsoft.AspNetCore.Http.Metadata;
 using Microsoft.AspNetCore.Mvc.ModelBinding;
 
 namespace Microsoft.AspNetCore.Mvc
@@ -10,7 +11,7 @@ namespace Microsoft.AspNetCore.Mvc
     /// Specifies that a parameter or property should be bound using the request query string.
     /// </summary>
     [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
-    public class FromQueryAttribute : Attribute, IBindingSourceMetadata, IModelNameProvider
+    public class FromQueryAttribute : Attribute, IBindingSourceMetadata, IModelNameProvider, IFromQueryMetadata
     {
         /// <inheritdoc />
         public BindingSource BindingSource => BindingSource.Query;

+ 6 - 2
src/Mvc/Mvc.Core/src/FromRouteAttribute.cs

@@ -2,6 +2,8 @@
 // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 
 using System;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Metadata;
 using Microsoft.AspNetCore.Mvc.ModelBinding;
 
 namespace Microsoft.AspNetCore.Mvc
@@ -10,12 +12,14 @@ namespace Microsoft.AspNetCore.Mvc
     /// Specifies that a parameter or property should be bound using route-data from the current request.
     /// </summary>
     [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
-    public class FromRouteAttribute : Attribute, IBindingSourceMetadata, IModelNameProvider
+    public class FromRouteAttribute : Attribute, IBindingSourceMetadata, IModelNameProvider, IFromRouteMetadata
     {
         /// <inheritdoc />
         public BindingSource BindingSource => BindingSource.Path;
 
-        /// <inheritdoc />
+        /// <summary>
+        /// The <see cref="HttpRequest.RouteValues"/> name.
+        /// </summary>
         public string Name { get; set; }
     }
 }

+ 2 - 1
src/Mvc/Mvc.Core/src/FromServicesAttribute.cs

@@ -2,6 +2,7 @@
 // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 
 using System;
+using Microsoft.AspNetCore.Http.Metadata;
 using Microsoft.AspNetCore.Mvc.ModelBinding;
 
 namespace Microsoft.AspNetCore.Mvc
@@ -23,7 +24,7 @@ namespace Microsoft.AspNetCore.Mvc
     /// </code>
     /// </example>
     [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
-    public class FromServicesAttribute : Attribute, IBindingSourceMetadata
+    public class FromServicesAttribute : Attribute, IBindingSourceMetadata, IFromServiceMetadata
     {
         /// <inheritdoc />
         public BindingSource BindingSource => BindingSource.Services;

+ 12 - 1
src/Mvc/Mvc.Core/src/JsonResult.cs

@@ -4,6 +4,7 @@
 using System;
 using System.Text.Json;
 using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc.Infrastructure;
 using Microsoft.Extensions.DependencyInjection;
 
@@ -12,7 +13,7 @@ namespace Microsoft.AspNetCore.Mvc
     /// <summary>
     /// An action result which formats the given object as JSON.
     /// </summary>
-    public class JsonResult : ActionResult, IStatusCodeActionResult
+    public class JsonResult : ActionResult, IResult, IStatusCodeActionResult
     {
         /// <summary>
         /// Creates a new <see cref="JsonResult"/> with the given <paramref name="value"/>.
@@ -80,5 +81,15 @@ namespace Microsoft.AspNetCore.Mvc
             var executor = services.GetRequiredService<IActionResultExecutor<JsonResult>>();
             return executor.ExecuteAsync(context, this);
         }
+
+        /// <summary>
+        /// Write the result as JSON to the HTTP response.
+        /// </summary>
+        /// <param name="httpContext">The <see cref="HttpContext"/> for the current request.</param>
+        /// <returns>A task that represents the asynchronous execute operation.</returns>
+        Task IResult.ExecuteAsync(HttpContext httpContext)
+        {
+            return httpContext.Response.WriteAsJsonAsync(Value);
+        }
     }
 }

+ 11 - 3
src/Mvc/Mvc.Core/src/Routing/HttpMethodAttribute.cs

@@ -6,6 +6,8 @@
 using System;
 using System.Collections.Generic;
 using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using Microsoft.AspNetCore.Routing;
 
 namespace Microsoft.AspNetCore.Mvc.Routing
 {
@@ -13,8 +15,10 @@ namespace Microsoft.AspNetCore.Mvc.Routing
     /// Identifies an action that supports a given set of HTTP methods.
     /// </summary>
     [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
-    public abstract class HttpMethodAttribute : Attribute, IActionHttpMethodProvider, IRouteTemplateProvider
+    public abstract class HttpMethodAttribute : Attribute, IHttpMethodMetadata, IActionHttpMethodProvider, IRouteTemplateProvider
     {
+        private readonly List<string> _httpMethods;
+
         private int? _order;
 
         /// <summary>
@@ -40,12 +44,15 @@ namespace Microsoft.AspNetCore.Mvc.Routing
                 throw new ArgumentNullException(nameof(httpMethods));
             }
 
-            HttpMethods = httpMethods;
+            _httpMethods = httpMethods.ToList();
             Template = template;
         }
 
         /// <inheritdoc />
-        public IEnumerable<string> HttpMethods { get; }
+        public IEnumerable<string> HttpMethods => _httpMethods;
+
+        IReadOnlyList<string> IHttpMethodMetadata.HttpMethods => _httpMethods;
+        bool IHttpMethodMetadata.AcceptCorsPreflight => false;
 
         /// <inheritdoc />
         public string? Template { get; }
@@ -68,5 +75,6 @@ namespace Microsoft.AspNetCore.Mvc.Routing
         /// <inheritdoc />
         [DisallowNull]
         public string? Name { get; set; }
+
     }
 }

+ 12 - 1
src/Mvc/Mvc.Core/src/Routing/IRouteTemplateProvider.cs

@@ -3,12 +3,14 @@
 
 #nullable enable
 
+using Microsoft.AspNetCore.Routing;
+
 namespace Microsoft.AspNetCore.Mvc.Routing
 {
     /// <summary>
     /// Interface for attributes which can supply a route template for attribute routing.
     /// </summary>
-    public interface IRouteTemplateProvider
+    public interface IRouteTemplateProvider : IRoutePatternMetadata, IRouteOrderMetadata, IRouteNameMetadata
     {
         /// <summary>
         /// The route template. May be <see langword="null"/>.
@@ -28,5 +30,14 @@ namespace Microsoft.AspNetCore.Mvc.Routing
         ///  of relying on selection of a route based on the given set of route values.
         /// </summary>
         string? Name { get; }
+
+        /// <inheritdoc />
+        string? IRoutePatternMetadata.RoutePattern => Template;
+
+        /// <inheritdoc />
+        int? IRouteOrderMetadata.RouteOrder => Order;
+
+        /// <inheritdoc />
+        string? IRouteNameMetadata.RouteName => Name;
     }
 }

+ 21 - 3
src/Mvc/Mvc.Core/src/StatusCodeResult.cs

@@ -2,6 +2,8 @@
 // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 
 using System;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc.Infrastructure;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
@@ -12,7 +14,7 @@ namespace Microsoft.AspNetCore.Mvc
     /// Represents an <see cref="ActionResult"/> that when executed will
     /// produce an HTTP response with the given response status code.
     /// </summary>
-    public class StatusCodeResult : ActionResult, IClientErrorActionResult
+    public class StatusCodeResult : ActionResult, IResult, IClientErrorActionResult
     {
         /// <summary>
         /// Initializes a new instance of the <see cref="StatusCodeResult"/> class
@@ -39,12 +41,28 @@ namespace Microsoft.AspNetCore.Mvc
                 throw new ArgumentNullException(nameof(context));
             }
 
-            var factory = context.HttpContext.RequestServices.GetRequiredService<ILoggerFactory>();
+            Execute(context.HttpContext);
+        }
+
+        /// <summary>
+        /// Sets the status code on the HTTP response.
+        /// </summary>
+        /// <param name="httpContext">The <see cref="HttpContext"/> for the current request.</param>
+        /// <returns>A task that represents the asynchronous execute operation.</returns>
+        Task IResult.ExecuteAsync(HttpContext httpContext)
+        {
+            Execute(httpContext);
+            return Task.CompletedTask;
+        }
+
+        private void Execute(HttpContext httpContext)
+        {
+            var factory = httpContext.RequestServices.GetRequiredService<ILoggerFactory>();
             var logger = factory.CreateLogger<StatusCodeResult>();
 
             logger.HttpStatusCodeResultExecuting(StatusCode);
 
-            context.HttpContext.Response.StatusCode = StatusCode;
+            httpContext.Response.StatusCode = StatusCode;
         }
     }
 }

+ 4 - 4
src/Mvc/Mvc.Core/test/ApplicationModels/ControllerActionDescriptorProviderTests.cs

@@ -304,7 +304,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
         }
 
         [Fact]
-        public void GetDescriptors_ActionWithMultipleHttpMethods_SingleHttpMethodMetadata()
+        public void GetDescriptors_ActionWithMultipleHttpMethods_LastHttpMethodMetadata()
         {
             // Arrange & Act
             var descriptors = GetDescriptors(
@@ -329,9 +329,9 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
                     var httpMethodAttribute = Assert.Single(descriptor.EndpointMetadata.OfType<HttpMethodAttribute>());
                     Assert.Equal(httpMethod, httpMethodAttribute.HttpMethods.Single(), ignoreCase: true);
 
-                    var httpMethodMetadata = Assert.Single(descriptor.EndpointMetadata.OfType<IHttpMethodMetadata>());
-                    Assert.Equal(httpMethod, httpMethodMetadata.HttpMethods.Single(), ignoreCase: true);
-                    Assert.False(httpMethodMetadata.AcceptCorsPreflight);
+                    var lastHttpMethodMetadata = descriptor.EndpointMetadata.OfType<IHttpMethodMetadata>().Last();
+                    Assert.Equal(httpMethod, lastHttpMethodMetadata.HttpMethods.Single(), ignoreCase: true);
+                    Assert.False(lastHttpMethodMetadata.AcceptCorsPreflight);
                 };
             }
         }

+ 101 - 101
src/Mvc/Mvc.slnf

@@ -1,87 +1,57 @@
-{
+{
   "solution": {
     "path": "..\\..\\AspNetCore.sln",
-    "projects" : [
-      "src\\Mvc\\test\\WebSites\\BasicWebSite\\BasicWebSite.csproj",
-      "src\\Mvc\\test\\WebSites\\RoutingWebSite\\Mvc.RoutingWebSite.csproj",
-      "src\\Mvc\\test\\WebSites\\RazorWebSite\\RazorWebSite.csproj",
-      "src\\Mvc\\test\\WebSites\\FormatterWebSite\\FormatterWebSite.csproj",
-      "src\\Mvc\\test\\WebSites\\ApiExplorerWebSite\\ApiExplorerWebSite.csproj",
-      "src\\Mvc\\test\\WebSites\\VersioningWebSite\\VersioningWebSite.csproj",
-      "src\\Mvc\\test\\WebSites\\TagHelpersWebSite\\TagHelpersWebSite.csproj",
-      "src\\Mvc\\test\\WebSites\\RoutingWebSite\\Mvc.RoutingWebSite.csproj",
-      "src\\Mvc\\test\\WebSites\\FilesWebSite\\FilesWebSite.csproj",
-      "src\\Mvc\\test\\WebSites\\ApplicationModelWebSite\\ApplicationModelWebSite.csproj",
-      "src\\Mvc\\test\\WebSites\\HtmlGenerationWebSite\\HtmlGenerationWebSite.csproj",
-      "src\\Mvc\\test\\WebSites\\ErrorPageMiddlewareWebSite\\ErrorPageMiddlewareWebSite.csproj",
-      "src\\Mvc\\test\\WebSites\\XmlFormattersWebSite\\XmlFormattersWebSite.csproj",
-      "src\\Mvc\\test\\WebSites\\ControllersFromServicesWebSite\\ControllersFromServicesWebSite.csproj",
-      "src\\Mvc\\test\\WebSites\\ControllersFromServicesClassLibrary\\ControllersFromServicesClassLibrary.csproj",
-      "src\\Mvc\\test\\WebSites\\CorsWebSite\\CorsWebSite.csproj",
-      "src\\Mvc\\samples\\MvcSandbox\\MvcSandbox.csproj",
-      "src\\Mvc\\test\\WebSites\\SimpleWebSite\\SimpleWebSite.csproj",
-      "src\\Mvc\\test\\WebSites\\SecurityWebSite\\SecurityWebSite.csproj",
-      "src\\Mvc\\test\\WebSites\\RazorPagesWebSite\\RazorPagesWebSite.csproj",
-      "src\\Mvc\\benchmarks\\Microsoft.AspNetCore.Mvc.Performance\\Microsoft.AspNetCore.Mvc.Performance.csproj",
-      "src\\Mvc\\test\\WebSites\\RazorBuildWebSite\\RazorBuildWebSite.csproj",
-      "src\\Mvc\\test\\WebSites\\RazorBuildWebSite.Views\\RazorBuildWebSite.Views.csproj",
-      "src\\Mvc\\Mvc.Analyzers\\src\\Microsoft.AspNetCore.Mvc.Analyzers.csproj",
-      "src\\Mvc\\Mvc.Analyzers\\test\\Mvc.Analyzers.Test.csproj",
-      "src\\Mvc\\test\\WebSites\\RazorPagesClassLibrary\\RazorPagesClassLibrary.csproj",
-      "src\\Mvc\\shared\\Mvc.Views.TestCommon\\Microsoft.AspNetCore.Mvc.Views.TestCommon.csproj",
-      "src\\Mvc\\Mvc.Api.Analyzers\\test\\Mvc.Api.Analyzers.Test.csproj",
-      "src\\Mvc\\Mvc.Api.Analyzers\\src\\Microsoft.AspNetCore.Mvc.Api.Analyzers.csproj",
-      "src\\Html.Abstractions\\src\\Microsoft.AspNetCore.Html.Abstractions.csproj",
-      "src\\Http\\Http\\src\\Microsoft.AspNetCore.Http.csproj",
-      "src\\Middleware\\CORS\\src\\Microsoft.AspNetCore.Cors.csproj",
+    "projects": [
+      "src\\Antiforgery\\src\\Microsoft.AspNetCore.Antiforgery.csproj",
+      "src\\Components\\Authorization\\src\\Microsoft.AspNetCore.Components.Authorization.csproj",
+      "src\\Components\\Components\\src\\Microsoft.AspNetCore.Components.csproj",
+      "src\\Components\\Forms\\src\\Microsoft.AspNetCore.Components.Forms.csproj",
+      "src\\Components\\Server\\src\\Microsoft.AspNetCore.Components.Server.csproj",
+      "src\\Components\\Web\\src\\Microsoft.AspNetCore.Components.Web.csproj",
+      "src\\DataProtection\\Abstractions\\src\\Microsoft.AspNetCore.DataProtection.Abstractions.csproj",
+      "src\\DataProtection\\Cryptography.Internal\\src\\Microsoft.AspNetCore.Cryptography.Internal.csproj",
+      "src\\DataProtection\\DataProtection\\src\\Microsoft.AspNetCore.DataProtection.csproj",
+      "src\\DataProtection\\Extensions\\src\\Microsoft.AspNetCore.DataProtection.Extensions.csproj",
+      "src\\Features\\JsonPatch\\src\\Microsoft.AspNetCore.JsonPatch.csproj",
+      "src\\FileProviders\\Embedded\\src\\Microsoft.Extensions.FileProviders.Embedded.csproj",
+      "src\\Hosting\\Abstractions\\src\\Microsoft.AspNetCore.Hosting.Abstractions.csproj",
       "src\\Hosting\\Hosting\\src\\Microsoft.AspNetCore.Hosting.csproj",
-      "src\\Hosting\\TestHost\\src\\Microsoft.AspNetCore.TestHost.csproj",
-      "src\\Middleware\\Diagnostics\\src\\Microsoft.AspNetCore.Diagnostics.csproj",
-      "src\\Hosting\\Server.IntegrationTesting\\src\\Microsoft.AspNetCore.Server.IntegrationTesting.csproj",
-      "src\\Http\\WebUtilities\\src\\Microsoft.AspNetCore.WebUtilities.csproj",
-      "src\\Middleware\\ResponseCaching\\src\\Microsoft.AspNetCore.ResponseCaching.csproj",
-      "src\\Middleware\\StaticFiles\\src\\Microsoft.AspNetCore.StaticFiles.csproj",
-      "src\\Middleware\\Session\\src\\Microsoft.AspNetCore.Session.csproj",
-      "src\\Middleware\\Localization.Routing\\src\\Microsoft.AspNetCore.Localization.Routing.csproj",
-      "src\\Razor\\Razor.Runtime\\src\\Microsoft.AspNetCore.Razor.Runtime.csproj",
-      "src\\Security\\Authentication\\Cookies\\src\\Microsoft.AspNetCore.Authentication.Cookies.csproj",
-      "src\\Servers\\IIS\\IISIntegration\\src\\Microsoft.AspNetCore.Server.IISIntegration.csproj",
-      "src\\Security\\Authentication\\Core\\src\\Microsoft.AspNetCore.Authentication.csproj",
-      "src\\Servers\\Kestrel\\Kestrel\\src\\Microsoft.AspNetCore.Server.Kestrel.csproj",
-      "src\\Security\\CookiePolicy\\src\\Microsoft.AspNetCore.CookiePolicy.csproj",
-      "src\\Security\\Authorization\\Policy\\src\\Microsoft.AspNetCore.Authorization.Policy.csproj",
-      "src\\Http\\Routing\\src\\Microsoft.AspNetCore.Routing.csproj",
-      "src\\Http\\Routing.Abstractions\\src\\Microsoft.AspNetCore.Routing.Abstractions.csproj",
-      "src\\Security\\Authentication\\JwtBearer\\src\\Microsoft.AspNetCore.Authentication.JwtBearer.csproj",
       "src\\Hosting\\Server.Abstractions\\src\\Microsoft.AspNetCore.Hosting.Server.Abstractions.csproj",
-      "src\\Hosting\\Abstractions\\src\\Microsoft.AspNetCore.Hosting.Abstractions.csproj",
-      "src\\Mvc\\benchmarks\\Microsoft.AspNetCore.Mvc.Performance.Views\\Microsoft.AspNetCore.Mvc.Performance.Views.csproj",
-      "src\\Features\\JsonPatch\\src\\Microsoft.AspNetCore.JsonPatch.csproj",
-      "src\\Middleware\\Localization\\src\\Microsoft.AspNetCore.Localization.csproj",
-      "src\\Antiforgery\\src\\Microsoft.AspNetCore.Antiforgery.csproj",
-      "src\\Middleware\\Diagnostics.Abstractions\\src\\Microsoft.AspNetCore.Diagnostics.Abstractions.csproj",
-      "src\\Http\\Http.Extensions\\src\\Microsoft.AspNetCore.Http.Extensions.csproj",
-      "src\\Middleware\\ResponseCaching.Abstractions\\src\\Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj",
+      "src\\Hosting\\Server.IntegrationTesting\\src\\Microsoft.AspNetCore.Server.IntegrationTesting.csproj",
+      "src\\Hosting\\TestHost\\src\\Microsoft.AspNetCore.TestHost.csproj",
+      "src\\Html.Abstractions\\src\\Microsoft.AspNetCore.Html.Abstractions.csproj",
+      "src\\Http\\Authentication.Abstractions\\src\\Microsoft.AspNetCore.Authentication.Abstractions.csproj",
       "src\\Http\\Authentication.Core\\src\\Microsoft.AspNetCore.Authentication.Core.csproj",
       "src\\Http\\Headers\\src\\Microsoft.Net.Http.Headers.csproj",
-      "src\\Servers\\Kestrel\\Transport.Sockets\\src\\Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.csproj",
-      "src\\Servers\\Kestrel\\Core\\src\\Microsoft.AspNetCore.Server.Kestrel.Core.csproj",
       "src\\Http\\Http.Abstractions\\src\\Microsoft.AspNetCore.Http.Abstractions.csproj",
-      "src\\Security\\Authorization\\Core\\src\\Microsoft.AspNetCore.Authorization.csproj",
-      "src\\Http\\Authentication.Abstractions\\src\\Microsoft.AspNetCore.Authentication.Abstractions.csproj",
+      "src\\Http\\Http.Extensions\\src\\Microsoft.AspNetCore.Http.Extensions.csproj",
       "src\\Http\\Http.Features\\src\\Microsoft.AspNetCore.Http.Features.csproj",
-      "src\\Razor\\Razor\\src\\Microsoft.AspNetCore.Razor.csproj",
-      "src\\DataProtection\\DataProtection\\src\\Microsoft.AspNetCore.DataProtection.csproj",
-      "src\\Servers\\Connections.Abstractions\\src\\Microsoft.AspNetCore.Connections.Abstractions.csproj",
+      "src\\Http\\Http\\src\\Microsoft.AspNetCore.Http.csproj",
+      "src\\Http\\Metadata\\src\\Microsoft.AspNetCore.Metadata.csproj",
+      "src\\Http\\Routing.Abstractions\\src\\Microsoft.AspNetCore.Routing.Abstractions.csproj",
+      "src\\Http\\Routing\\samples\\MapActionSample\\MapActionSample.csproj",
+      "src\\Http\\Routing\\src\\Microsoft.AspNetCore.Routing.csproj",
+      "src\\Http\\WebUtilities\\src\\Microsoft.AspNetCore.WebUtilities.csproj",
+      "src\\JSInterop\\Microsoft.JSInterop\\src\\Microsoft.JSInterop.csproj",
+      "src\\Localization\\Abstractions\\src\\Microsoft.Extensions.Localization.Abstractions.csproj",
+      "src\\Localization\\Localization\\src\\Microsoft.Extensions.Localization.csproj",
+      "src\\Middleware\\CORS\\src\\Microsoft.AspNetCore.Cors.csproj",
+      "src\\Middleware\\Diagnostics.Abstractions\\src\\Microsoft.AspNetCore.Diagnostics.Abstractions.csproj",
+      "src\\Middleware\\Diagnostics\\src\\Microsoft.AspNetCore.Diagnostics.csproj",
       "src\\Middleware\\HttpOverrides\\src\\Microsoft.AspNetCore.HttpOverrides.csproj",
-      "src\\DataProtection\\Cryptography.Internal\\src\\Microsoft.AspNetCore.Cryptography.Internal.csproj",
-      "src\\DataProtection\\Abstractions\\src\\Microsoft.AspNetCore.DataProtection.Abstractions.csproj",
-      "src\\Mvc\\test\\WebSites\\GenericHostWebSite\\GenericHostWebSite.csproj",
-      "src\\Components\\Components\\src\\Microsoft.AspNetCore.Components.csproj",
-      "src\\Mvc\\Mvc\\src\\Microsoft.AspNetCore.Mvc.csproj",
-      "src\\Mvc\\Mvc\\test\\Microsoft.AspNetCore.Mvc.Test.csproj",
+      "src\\Middleware\\Localization.Routing\\src\\Microsoft.AspNetCore.Localization.Routing.csproj",
+      "src\\Middleware\\Localization\\src\\Microsoft.AspNetCore.Localization.csproj",
+      "src\\Middleware\\ResponseCaching.Abstractions\\src\\Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj",
+      "src\\Middleware\\ResponseCaching\\src\\Microsoft.AspNetCore.ResponseCaching.csproj",
+      "src\\Middleware\\Session\\src\\Microsoft.AspNetCore.Session.csproj",
+      "src\\Middleware\\StaticFiles\\src\\Microsoft.AspNetCore.StaticFiles.csproj",
+      "src\\Middleware\\WebSockets\\src\\Microsoft.AspNetCore.WebSockets.csproj",
       "src\\Mvc\\Mvc.Abstractions\\src\\Microsoft.AspNetCore.Mvc.Abstractions.csproj",
       "src\\Mvc\\Mvc.Abstractions\\test\\Microsoft.AspNetCore.Mvc.Abstractions.Test.csproj",
+      "src\\Mvc\\Mvc.Analyzers\\src\\Microsoft.AspNetCore.Mvc.Analyzers.csproj",
+      "src\\Mvc\\Mvc.Analyzers\\test\\Mvc.Analyzers.Test.csproj",
+      "src\\Mvc\\Mvc.Api.Analyzers\\src\\Microsoft.AspNetCore.Mvc.Api.Analyzers.csproj",
+      "src\\Mvc\\Mvc.Api.Analyzers\\test\\Mvc.Api.Analyzers.Test.csproj",
       "src\\Mvc\\Mvc.ApiExplorer\\src\\Microsoft.AspNetCore.Mvc.ApiExplorer.csproj",
       "src\\Mvc\\Mvc.ApiExplorer\\test\\Microsoft.AspNetCore.Mvc.ApiExplorer.Test.csproj",
       "src\\Mvc\\Mvc.Core\\src\\Microsoft.AspNetCore.Mvc.Core.csproj",
@@ -95,44 +65,74 @@
       "src\\Mvc\\Mvc.Formatters.Xml\\test\\Microsoft.AspNetCore.Mvc.Formatters.Xml.Test.csproj",
       "src\\Mvc\\Mvc.Localization\\src\\Microsoft.AspNetCore.Mvc.Localization.csproj",
       "src\\Mvc\\Mvc.Localization\\test\\Microsoft.AspNetCore.Mvc.Localization.Test.csproj",
-      "src\\Mvc\\Mvc.Razor\\src\\Microsoft.AspNetCore.Mvc.Razor.csproj",
-      "src\\Mvc\\Mvc.Razor\\test\\Microsoft.AspNetCore.Mvc.Razor.Test.csproj",
+      "src\\Mvc\\Mvc.NewtonsoftJson\\src\\Microsoft.AspNetCore.Mvc.NewtonsoftJson.csproj",
+      "src\\Mvc\\Mvc.NewtonsoftJson\\test\\Microsoft.AspNetCore.Mvc.NewtonsoftJson.Test.csproj",
+      "src\\Mvc\\Mvc.Razor.RuntimeCompilation\\src\\Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.csproj",
+      "src\\Mvc\\Mvc.Razor.RuntimeCompilation\\test\\Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test.csproj",
       "src\\Mvc\\Mvc.RazorPages\\src\\Microsoft.AspNetCore.Mvc.RazorPages.csproj",
       "src\\Mvc\\Mvc.RazorPages\\test\\Microsoft.AspNetCore.Mvc.RazorPages.Test.csproj",
+      "src\\Mvc\\Mvc.Razor\\src\\Microsoft.AspNetCore.Mvc.Razor.csproj",
+      "src\\Mvc\\Mvc.Razor\\test\\Microsoft.AspNetCore.Mvc.Razor.Test.csproj",
       "src\\Mvc\\Mvc.TagHelpers\\src\\Microsoft.AspNetCore.Mvc.TagHelpers.csproj",
       "src\\Mvc\\Mvc.TagHelpers\\test\\Microsoft.AspNetCore.Mvc.TagHelpers.Test.csproj",
+      "src\\Mvc\\Mvc.Testing.Tasks\\src\\Microsoft.AspNetCore.Mvc.Testing.Tasks.csproj",
+      "src\\Mvc\\Mvc.Testing\\src\\Microsoft.AspNetCore.Mvc.Testing.csproj",
       "src\\Mvc\\Mvc.ViewFeatures\\src\\Microsoft.AspNetCore.Mvc.ViewFeatures.csproj",
       "src\\Mvc\\Mvc.ViewFeatures\\test\\Microsoft.AspNetCore.Mvc.ViewFeatures.Test.csproj",
+      "src\\Mvc\\Mvc\\src\\Microsoft.AspNetCore.Mvc.csproj",
+      "src\\Mvc\\Mvc\\test\\Microsoft.AspNetCore.Mvc.Test.csproj",
+      "src\\Mvc\\benchmarks\\Microsoft.AspNetCore.Mvc.Performance.Views\\Microsoft.AspNetCore.Mvc.Performance.Views.csproj",
+      "src\\Mvc\\benchmarks\\Microsoft.AspNetCore.Mvc.Performance\\Microsoft.AspNetCore.Mvc.Performance.csproj",
+      "src\\Mvc\\samples\\MvcSandbox\\MvcSandbox.csproj",
+      "src\\Mvc\\shared\\Mvc.Core.TestCommon\\Microsoft.AspNetCore.Mvc.Core.TestCommon.csproj",
+      "src\\Mvc\\shared\\Mvc.TestDiagnosticListener\\Microsoft.AspNetCore.Mvc.TestDiagnosticListener.csproj",
+      "src\\Mvc\\shared\\Mvc.Views.TestCommon\\Microsoft.AspNetCore.Mvc.Views.TestCommon.csproj",
       "src\\Mvc\\test\\Mvc.FunctionalTests\\Microsoft.AspNetCore.Mvc.FunctionalTests.csproj",
       "src\\Mvc\\test\\Mvc.IntegrationTests\\Microsoft.AspNetCore.Mvc.IntegrationTests.csproj",
-      "src\\Mvc\\shared\\Mvc.TestDiagnosticListener\\Microsoft.AspNetCore.Mvc.TestDiagnosticListener.csproj",
-      "src\\Mvc\\Mvc.Testing\\src\\Microsoft.AspNetCore.Mvc.Testing.csproj",
-      "src\\Mvc\\Mvc.Testing.Tasks\\src\\Microsoft.AspNetCore.Mvc.Testing.Tasks.csproj",
-      "src\\Mvc\\shared\\Mvc.Core.TestCommon\\Microsoft.AspNetCore.Mvc.Core.TestCommon.csproj",
-      "src\\Mvc\\Mvc.NewtonsoftJson\\src\\Microsoft.AspNetCore.Mvc.NewtonsoftJson.csproj",
-      "src\\Mvc\\Mvc.NewtonsoftJson\\test\\Microsoft.AspNetCore.Mvc.NewtonsoftJson.Test.csproj",
-      "src\\Mvc\\Mvc.Razor.RuntimeCompilation\\src\\Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.csproj",
-      "src\\Mvc\\Mvc.Razor.RuntimeCompilation\\test\\Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test.csproj",
-      "src\\Components\\Server\\src\\Microsoft.AspNetCore.Components.Server.csproj",
-      "src\\Components\\Web\\src\\Microsoft.AspNetCore.Components.Web.csproj",
-      "src\\Middleware\\WebSockets\\src\\Microsoft.AspNetCore.WebSockets.csproj",
-      "src\\SignalR\\common\\Http.Connections\\src\\Microsoft.AspNetCore.Http.Connections.csproj",
+      "src\\Mvc\\test\\WebSites\\ApiExplorerWebSite\\ApiExplorerWebSite.csproj",
+      "src\\Mvc\\test\\WebSites\\ApplicationModelWebSite\\ApplicationModelWebSite.csproj",
+      "src\\Mvc\\test\\WebSites\\BasicWebSite\\BasicWebSite.csproj",
+      "src\\Mvc\\test\\WebSites\\ControllersFromServicesClassLibrary\\ControllersFromServicesClassLibrary.csproj",
+      "src\\Mvc\\test\\WebSites\\ControllersFromServicesWebSite\\ControllersFromServicesWebSite.csproj",
+      "src\\Mvc\\test\\WebSites\\CorsWebSite\\CorsWebSite.csproj",
+      "src\\Mvc\\test\\WebSites\\ErrorPageMiddlewareWebSite\\ErrorPageMiddlewareWebSite.csproj",
+      "src\\Mvc\\test\\WebSites\\FilesWebSite\\FilesWebSite.csproj",
+      "src\\Mvc\\test\\WebSites\\FormatterWebSite\\FormatterWebSite.csproj",
+      "src\\Mvc\\test\\WebSites\\GenericHostWebSite\\GenericHostWebSite.csproj",
+      "src\\Mvc\\test\\WebSites\\HtmlGenerationWebSite\\HtmlGenerationWebSite.csproj",
+      "src\\Mvc\\test\\WebSites\\RazorBuildWebSite.PrecompiledViews\\RazorBuildWebSite.PrecompiledViews.csproj",
+      "src\\Mvc\\test\\WebSites\\RazorBuildWebSite.Views\\RazorBuildWebSite.Views.csproj",
+      "src\\Mvc\\test\\WebSites\\RazorBuildWebSite\\RazorBuildWebSite.csproj",
+      "src\\Mvc\\test\\WebSites\\RazorPagesClassLibrary\\RazorPagesClassLibrary.csproj",
+      "src\\Mvc\\test\\WebSites\\RazorPagesWebSite\\RazorPagesWebSite.csproj",
+      "src\\Mvc\\test\\WebSites\\RazorWebSite\\RazorWebSite.csproj",
+      "src\\Mvc\\test\\WebSites\\RoutingWebSite\\Mvc.RoutingWebSite.csproj",
+      "src\\Mvc\\test\\WebSites\\SecurityWebSite\\SecurityWebSite.csproj",
+      "src\\Mvc\\test\\WebSites\\SimpleWebSite\\SimpleWebSite.csproj",
+      "src\\Mvc\\test\\WebSites\\TagHelpersWebSite\\TagHelpersWebSite.csproj",
+      "src\\Mvc\\test\\WebSites\\VersioningWebSite\\VersioningWebSite.csproj",
+      "src\\Mvc\\test\\WebSites\\XmlFormattersWebSite\\XmlFormattersWebSite.csproj",
+      "src\\Razor\\Razor.Runtime\\src\\Microsoft.AspNetCore.Razor.Runtime.csproj",
+      "src\\Razor\\Razor\\src\\Microsoft.AspNetCore.Razor.csproj",
+      "src\\Security\\Authentication\\Cookies\\src\\Microsoft.AspNetCore.Authentication.Cookies.csproj",
+      "src\\Security\\Authentication\\Core\\src\\Microsoft.AspNetCore.Authentication.csproj",
+      "src\\Security\\Authentication\\JwtBearer\\src\\Microsoft.AspNetCore.Authentication.JwtBearer.csproj",
+      "src\\Security\\Authorization\\Core\\src\\Microsoft.AspNetCore.Authorization.csproj",
+      "src\\Security\\Authorization\\Policy\\src\\Microsoft.AspNetCore.Authorization.Policy.csproj",
+      "src\\Security\\CookiePolicy\\src\\Microsoft.AspNetCore.CookiePolicy.csproj",
+      "src\\Servers\\Connections.Abstractions\\src\\Microsoft.AspNetCore.Connections.Abstractions.csproj",
+      "src\\Servers\\IIS\\IISIntegration\\src\\Microsoft.AspNetCore.Server.IISIntegration.csproj",
+      "src\\Servers\\Kestrel\\Core\\src\\Microsoft.AspNetCore.Server.Kestrel.Core.csproj",
+      "src\\Servers\\Kestrel\\Kestrel\\src\\Microsoft.AspNetCore.Server.Kestrel.csproj",
+      "src\\Servers\\Kestrel\\Transport.Sockets\\src\\Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.csproj",
       "src\\SignalR\\common\\Http.Connections.Common\\src\\Microsoft.AspNetCore.Http.Connections.Common.csproj",
-      "src\\SignalR\\server\\SignalR\\src\\Microsoft.AspNetCore.SignalR.csproj",
-      "src\\SignalR\\common\\SignalR.Common\\src\\Microsoft.AspNetCore.SignalR.Common.csproj",
-      "src\\SignalR\\server\\Core\\src\\Microsoft.AspNetCore.SignalR.Core.csproj",
+      "src\\SignalR\\common\\Http.Connections\\src\\Microsoft.AspNetCore.Http.Connections.csproj",
+      "src\\SignalR\\common\\Protocols.Json\\src\\Microsoft.AspNetCore.SignalR.Protocols.Json.csproj",
       "src\\SignalR\\common\\Protocols.MessagePack\\src\\Microsoft.AspNetCore.SignalR.Protocols.MessagePack.csproj",
       "src\\SignalR\\common\\Protocols.NewtonsoftJson\\src\\Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson.csproj",
-      "src\\Mvc\\test\\WebSites\\RazorBuildWebSite.PrecompiledViews\\RazorBuildWebSite.PrecompiledViews.csproj",
-      "src\\SignalR\\common\\Protocols.Json\\src\\Microsoft.AspNetCore.SignalR.Protocols.Json.csproj",
-      "src\\Http\\Metadata\\src\\Microsoft.AspNetCore.Metadata.csproj",
-      "src\\Components\\Authorization\\src\\Microsoft.AspNetCore.Components.Authorization.csproj",
-      "src\\Components\\Forms\\src\\Microsoft.AspNetCore.Components.Forms.csproj",
-      "src\\DataProtection\\Extensions\\src\\Microsoft.AspNetCore.DataProtection.Extensions.csproj",
-      "src\\FileProviders\\Embedded\\src\\Microsoft.Extensions.FileProviders.Embedded.csproj",
-      "src\\Localization\\Abstractions\\src\\Microsoft.Extensions.Localization.Abstractions.csproj",
-      "src\\Localization\\Localization\\src\\Microsoft.Extensions.Localization.csproj",
-      "src\\JSInterop\\Microsoft.JSInterop\\src\\Microsoft.JSInterop.csproj"
+      "src\\SignalR\\common\\SignalR.Common\\src\\Microsoft.AspNetCore.SignalR.Common.csproj",
+      "src\\SignalR\\server\\Core\\src\\Microsoft.AspNetCore.SignalR.Core.csproj",
+      "src\\SignalR\\server\\SignalR\\src\\Microsoft.AspNetCore.SignalR.csproj"
     ]
   }
-}
+}