From 77e578438bff3c01e4818a85d1123522e1521c20 Mon Sep 17 00:00:00 2001
From: Stephen Halter <halter73@gmail.com>
Date: Fri, 9 Apr 2021 15:31:37 -0700
Subject: [PATCH] Remove form support from minimal APIs (#31646)

* Remove form support from minimal APIs

* Add

* MapActionSample -> MinimalSample

* Remove MapActionSample.csproj from Mvc.slnf
---
 AspNetCore.sln                                |  30 ++--
 .../src/Metadata/IFromFormMetadata.cs         |  16 ---
 .../src/PublicAPI.Unshipped.txt               |   2 -
 .../src/RequestDelegateFactory.cs             | 123 ++++------------
 .../test/RequestDelegateFactoryTests.cs       | 133 +-----------------
 src/Http/HttpAbstractions.slnf                |   2 +-
 .../MinimalSample.csproj}                     |   0
 .../Program.cs                                |   4 +-
 .../Properties/launchSettings.json            |   0
 .../appsettings.Development.json              |   0
 .../appsettings.json                          |   0
 src/Mvc/Mvc.Core/src/FromFormAttribute.cs     |   2 +-
 src/Mvc/Mvc.slnf                              |   1 -
 13 files changed, 46 insertions(+), 267 deletions(-)
 delete mode 100644 src/Http/Http.Abstractions/src/Metadata/IFromFormMetadata.cs
 rename src/Http/samples/{MapActionSample/MapActionSample.csproj => MinimalSample/MinimalSample.csproj} (100%)
 rename src/Http/samples/{MapActionSample => MinimalSample}/Program.cs (86%)
 rename src/Http/samples/{MapActionSample => MinimalSample}/Properties/launchSettings.json (100%)
 rename src/Http/samples/{MapActionSample => MinimalSample}/appsettings.Development.json (100%)
 rename src/Http/samples/{MapActionSample => MinimalSample}/appsettings.json (100%)

diff --git a/AspNetCore.sln b/AspNetCore.sln
index 8160c08a5c5..81801038c95 100644
--- a/AspNetCore.sln
+++ b/AspNetCore.sln
@@ -1600,8 +1600,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorWinFormsApp", "src\Co
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Http.Abstractions.Microbenchmarks", "src\Http\Http.Abstractions\perf\Microbenchmarks\Microsoft.AspNetCore.Http.Abstractions.Microbenchmarks.csproj", "{3F752B48-2936-4FCA-B0DC-4AB0F788F897}"
 EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MapActionSample", "src\Http\samples\MapActionSample\MapActionSample.csproj", "{A661D867-708A-494E-8B6B-6558804F9A3F}"
-EndProject
 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "testassets", "testassets", "{F0849E7E-61DB-4849-9368-9E7BC125DCB0}"
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WinFormsTestApp", "src\Components\WebView\Platforms\WindowsForms\testassets\WinFormsTestApp\WinFormsTestApp.csproj", "{99EE7769-3C81-477B-B947-0A5CBCD5B27D}"
@@ -1626,6 +1624,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.SpaSer
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.SpaServices.Extensions.Tests", "src\Middleware\Spa\SpaServices.Extensions\test\Microsoft.AspNetCore.SpaServices.Extensions.Tests.csproj", "{AAB50C64-39AA-4AED-8E9C-50D68E7751AD}"
 EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MinimalSample", "src\Http\samples\MinimalSample\MinimalSample.csproj", "{9647D8B7-4616-4E05-B258-BAD5CAEEDD38}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -7613,18 +7613,6 @@ Global
 		{3F752B48-2936-4FCA-B0DC-4AB0F788F897}.Release|x64.Build.0 = Release|Any CPU
 		{3F752B48-2936-4FCA-B0DC-4AB0F788F897}.Release|x86.ActiveCfg = Release|Any CPU
 		{3F752B48-2936-4FCA-B0DC-4AB0F788F897}.Release|x86.Build.0 = Release|Any CPU
-		{A661D867-708A-494E-8B6B-6558804F9A3F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
-		{A661D867-708A-494E-8B6B-6558804F9A3F}.Debug|Any CPU.Build.0 = Debug|Any CPU
-		{A661D867-708A-494E-8B6B-6558804F9A3F}.Debug|x64.ActiveCfg = Debug|Any CPU
-		{A661D867-708A-494E-8B6B-6558804F9A3F}.Debug|x64.Build.0 = Debug|Any CPU
-		{A661D867-708A-494E-8B6B-6558804F9A3F}.Debug|x86.ActiveCfg = Debug|Any CPU
-		{A661D867-708A-494E-8B6B-6558804F9A3F}.Debug|x86.Build.0 = Debug|Any CPU
-		{A661D867-708A-494E-8B6B-6558804F9A3F}.Release|Any CPU.ActiveCfg = Release|Any CPU
-		{A661D867-708A-494E-8B6B-6558804F9A3F}.Release|Any CPU.Build.0 = Release|Any CPU
-		{A661D867-708A-494E-8B6B-6558804F9A3F}.Release|x64.ActiveCfg = Release|Any CPU
-		{A661D867-708A-494E-8B6B-6558804F9A3F}.Release|x64.Build.0 = Release|Any CPU
-		{A661D867-708A-494E-8B6B-6558804F9A3F}.Release|x86.ActiveCfg = Release|Any CPU
-		{A661D867-708A-494E-8B6B-6558804F9A3F}.Release|x86.Build.0 = Release|Any CPU
 		{99EE7769-3C81-477B-B947-0A5CBCD5B27D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{99EE7769-3C81-477B-B947-0A5CBCD5B27D}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{99EE7769-3C81-477B-B947-0A5CBCD5B27D}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -7709,6 +7697,18 @@ Global
 		{AAB50C64-39AA-4AED-8E9C-50D68E7751AD}.Release|x64.Build.0 = Release|Any CPU
 		{AAB50C64-39AA-4AED-8E9C-50D68E7751AD}.Release|x86.ActiveCfg = Release|Any CPU
 		{AAB50C64-39AA-4AED-8E9C-50D68E7751AD}.Release|x86.Build.0 = Release|Any CPU
+		{9647D8B7-4616-4E05-B258-BAD5CAEEDD38}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{9647D8B7-4616-4E05-B258-BAD5CAEEDD38}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{9647D8B7-4616-4E05-B258-BAD5CAEEDD38}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{9647D8B7-4616-4E05-B258-BAD5CAEEDD38}.Debug|x64.Build.0 = Debug|Any CPU
+		{9647D8B7-4616-4E05-B258-BAD5CAEEDD38}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{9647D8B7-4616-4E05-B258-BAD5CAEEDD38}.Debug|x86.Build.0 = Debug|Any CPU
+		{9647D8B7-4616-4E05-B258-BAD5CAEEDD38}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{9647D8B7-4616-4E05-B258-BAD5CAEEDD38}.Release|Any CPU.Build.0 = Release|Any CPU
+		{9647D8B7-4616-4E05-B258-BAD5CAEEDD38}.Release|x64.ActiveCfg = Release|Any CPU
+		{9647D8B7-4616-4E05-B258-BAD5CAEEDD38}.Release|x64.Build.0 = Release|Any CPU
+		{9647D8B7-4616-4E05-B258-BAD5CAEEDD38}.Release|x86.ActiveCfg = Release|Any CPU
+		{9647D8B7-4616-4E05-B258-BAD5CAEEDD38}.Release|x86.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
@@ -8501,7 +8501,6 @@ Global
 		{3BA297F8-1CA1-492D-AE64-A60B825D8501} = {D4E9A2C5-0838-42DF-BC80-C829C4C9137E}
 		{CC740832-D268-47A3-9058-B9054F8397E2} = {D3B76F4E-A980-45BF-AEA1-EA3175B0B5A1}
 		{3F752B48-2936-4FCA-B0DC-4AB0F788F897} = {DCBBDB52-4A49-4141-8F4D-81C0FFFB7BD5}
-		{A661D867-708A-494E-8B6B-6558804F9A3F} = {EB5E294B-9ED5-43BF-AFA9-1CD2327F3DC1}
 		{F0849E7E-61DB-4849-9368-9E7BC125DCB0} = {D4E9A2C5-0838-42DF-BC80-C829C4C9137E}
 		{99EE7769-3C81-477B-B947-0A5CBCD5B27D} = {F0849E7E-61DB-4849-9368-9E7BC125DCB0}
 		{94D0D6F3-8632-41DE-908B-47A787D570FF} = {5241CF68-66A0-4724-9BAA-36DB959A5B11}
@@ -8514,6 +8513,7 @@ Global
 		{7F99E967-3DC1-4198-9D55-47CD9471D0B6} = {0A064174-8E5C-4F97-B941-A4E302661DF2}
 		{DF4637DA-5F07-4903-8461-4E2DAB235F3C} = {7F99E967-3DC1-4198-9D55-47CD9471D0B6}
 		{AAB50C64-39AA-4AED-8E9C-50D68E7751AD} = {7F99E967-3DC1-4198-9D55-47CD9471D0B6}
+		{9647D8B7-4616-4E05-B258-BAD5CAEEDD38} = {EB5E294B-9ED5-43BF-AFA9-1CD2327F3DC1}
 	EndGlobalSection
 	GlobalSection(ExtensibilityGlobals) = postSolution
 		SolutionGuid = {3E8720B3-DBDD-498C-B383-2CC32A054E8F}
diff --git a/src/Http/Http.Abstractions/src/Metadata/IFromFormMetadata.cs b/src/Http/Http.Abstractions/src/Metadata/IFromFormMetadata.cs
deleted file mode 100644
index 054628d5006..00000000000
--- a/src/Http/Http.Abstractions/src/Metadata/IFromFormMetadata.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-// Copyright (c) .NET Foundation. All rights reserved.
-// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
-
-namespace Microsoft.AspNetCore.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; }
-    }
-}
diff --git a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt
index 28f8d0317d6..4ff4474b6bf 100644
--- a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt
+++ b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt
@@ -8,8 +8,6 @@ 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
diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs
index d96cddbdfc6..d21b481b7e6 100644
--- a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs
+++ b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs
@@ -203,48 +203,20 @@ namespace Microsoft.AspNetCore.Http
             }
             else if (parameterCustomAttributes.OfType<IFromBodyMetadata>().FirstOrDefault() is { } bodyAttribute)
             {
-                if (factoryContext.RequestBodyMode is RequestBodyMode.AsJson)
+                if (factoryContext.JsonRequestBodyType is not null)
                 {
                     throw new InvalidOperationException("Action cannot have more than one FromBody attribute.");
                 }
 
-                if (factoryContext.RequestBodyMode is RequestBodyMode.AsForm)
-                {
-                    ThrowCannotReadBodyDirectlyAndAsForm();
-                }
-
-                factoryContext.RequestBodyMode = RequestBodyMode.AsJson;
                 factoryContext.JsonRequestBodyType = parameter.ParameterType;
                 factoryContext.AllowEmptyRequestBody = bodyAttribute.AllowEmpty;
 
                 return Expression.Convert(BodyValueExpr, parameter.ParameterType);
             }
-            else if (parameterCustomAttributes.OfType<IFromFormMetadata>().FirstOrDefault() is { } formAttribute)
-            {
-                if (factoryContext.RequestBodyMode is RequestBodyMode.AsJson)
-                {
-                    ThrowCannotReadBodyDirectlyAndAsForm();
-                }
-
-                factoryContext.RequestBodyMode = RequestBodyMode.AsForm;
-
-                return BindParameterFromProperty(parameter, FormExpr, formAttribute.Name ?? parameter.Name, factoryContext);
-            }
             else if (parameter.CustomAttributes.Any(a => typeof(IFromServiceMetadata).IsAssignableFrom(a.AttributeType)))
             {
                 return Expression.Call(GetRequiredServiceMethod.MakeGenericMethod(parameter.ParameterType), RequestServicesExpr);
             }
-            else if (parameter.ParameterType == typeof(IFormCollection))
-            {
-                if (factoryContext.RequestBodyMode is RequestBodyMode.AsJson)
-                {
-                    ThrowCannotReadBodyDirectlyAndAsForm();
-                }
-
-                factoryContext.RequestBodyMode = RequestBodyMode.AsForm;
-
-                return Expression.Property(HttpRequestExpr, nameof(HttpRequest.Form));
-            }
             else if (parameter.ParameterType == typeof(HttpContext))
             {
                 return HttpContextExpr;
@@ -446,67 +418,41 @@ namespace Microsoft.AspNetCore.Http
 
         private static Func<object?, HttpContext, Task> HandleRequestBodyAndCompileRequestDelegate(Expression responseWritingMethodCall, FactoryContext factoryContext)
         {
-            if (factoryContext.RequestBodyMode is RequestBodyMode.AsJson)
+            if (factoryContext.JsonRequestBodyType is null)
             {
-                // We need to generate the code for reading from the body before calling into the delegate
-                var invoker = Expression.Lambda<Func<object?, HttpContext, object?, Task>>(
-                    responseWritingMethodCall, TargetExpr, HttpContextExpr, BodyValueExpr).Compile();
-
-                var bodyType = factoryContext.JsonRequestBodyType!;
-                object? defaultBodyValue = null;
-
-                if (factoryContext.AllowEmptyRequestBody && bodyType.IsValueType)
-                {
-                    defaultBodyValue = Activator.CreateInstance(bodyType);
-                }
+                return Expression.Lambda<Func<object?, HttpContext, Task>>(
+                    responseWritingMethodCall, TargetExpr, HttpContextExpr).Compile();
+            }
 
-                return async (target, httpContext) =>
-                {
-                    object? bodyValue;
+            // We need to generate the code for reading from the body before calling into the delegate
+            var invoker = Expression.Lambda<Func<object?, HttpContext, object?, Task>>(
+                responseWritingMethodCall, TargetExpr, HttpContextExpr, BodyValueExpr).Compile();
 
-                    if (factoryContext.AllowEmptyRequestBody && httpContext.Request.ContentLength == 0)
-                    {
-                        bodyValue = defaultBodyValue;
-                    }
-                    else
-                    {
-                        try
-                        {
-                            bodyValue = await httpContext.Request.ReadFromJsonAsync(bodyType);
-                        }
-                        catch (IOException ex)
-                        {
-                            Log.RequestBodyIOException(httpContext, ex);
-                            return;
-                        }
-                        catch (InvalidDataException ex)
-                        {
-                            Log.RequestBodyInvalidDataException(httpContext, ex);
-                            httpContext.Response.StatusCode = 400;
-                            return;
-                        }
-                    }
+            var bodyType = factoryContext.JsonRequestBodyType!;
+            object? defaultBodyValue = null;
 
-                    await invoker(target, httpContext, bodyValue);
-                };
+            if (factoryContext.AllowEmptyRequestBody && bodyType.IsValueType)
+            {
+                defaultBodyValue = Activator.CreateInstance(bodyType);
             }
-            else if (factoryContext.RequestBodyMode is RequestBodyMode.AsForm)
+
+            return async (target, httpContext) =>
             {
-                var invoker = Expression.Lambda<Func<object?, HttpContext, Task>>(
-                    responseWritingMethodCall, TargetExpr, HttpContextExpr).Compile();
+                object? bodyValue;
 
-                return async (target, httpContext) =>
+                if (factoryContext.AllowEmptyRequestBody && httpContext.Request.ContentLength == 0)
+                {
+                    bodyValue = defaultBodyValue;
+                }
+                else
                 {
-                    // 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();
+                        bodyValue = await httpContext.Request.ReadFromJsonAsync(bodyType);
                     }
                     catch (IOException ex)
                     {
                         Log.RequestBodyIOException(httpContext, ex);
-                        httpContext.Abort();
                         return;
                     }
                     catch (InvalidDataException ex)
@@ -515,15 +461,10 @@ namespace Microsoft.AspNetCore.Http
                         httpContext.Response.StatusCode = 400;
                         return;
                     }
+                }
 
-                    await invoker(target, httpContext);
-                };
-            }
-            else
-            {
-                return Expression.Lambda<Func<object?, HttpContext, Task>>(
-                    responseWritingMethodCall, TargetExpr, HttpContextExpr).Compile();
-            }
+                await invoker(target, httpContext, bodyValue);
+            };
         }
 
         private static MethodInfo GetEnumTryParseMethod()
@@ -793,22 +734,8 @@ namespace Microsoft.AspNetCore.Http
             await (await task).ExecuteAsync(httpContext);
         }
 
-        [StackTraceHidden]
-        private static void ThrowCannotReadBodyDirectlyAndAsForm()
-        {
-            throw new InvalidOperationException("Action cannot mix FromBody and FromForm on the same method.");
-        }
-
-        private enum RequestBodyMode
-        {
-            None,
-            AsJson,
-            AsForm,
-        }
-
         private class FactoryContext
         {
-            public RequestBodyMode RequestBodyMode { get; set; }
             public Type? JsonRequestBodyType { get; set; }
             public bool AllowEmptyRequestBody { get; set; }
 
diff --git a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs
index 80e4532746f..79ece71eddd 100644
--- a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs
+++ b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs
@@ -400,6 +400,7 @@ namespace Microsoft.AspNetCore.Routing.Internal
             var httpContext = new DefaultHttpContext();
 
             httpContext.Request.RouteValues["tryParsable"] = "42";
+
             httpContext.Request.Query = new QueryCollection(new Dictionary<string, StringValues>
             {
                 ["tryParsable"] = "invalid!"
@@ -422,14 +423,12 @@ namespace Microsoft.AspNetCore.Routing.Internal
                 void InvalidFromRoute([FromRoute] object notTryParsable) { }
                 void InvalidFromQuery([FromQuery] object notTryParsable) { }
                 void InvalidFromHeader([FromHeader] object notTryParsable) { }
-                void InvalidFromForm([FromForm] object notTryParsable) { }
 
                 return new[]
                 {
                     new object[] { (Action<object>)InvalidFromRoute },
                     new object[] { (Action<object>)InvalidFromQuery },
                     new object[] { (Action<object>)InvalidFromHeader },
-                    new object[] { (Action<object>)InvalidFromForm },
                 };
             }
         }
@@ -700,111 +699,6 @@ namespace Microsoft.AspNetCore.Routing.Internal
             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 = RequestDelegateFactory.Create((Action<int>)TestAction);
-
-            await requestDelegate(httpContext);
-
-            Assert.Equal(originalQueryParam, deserializedRouteParam);
-        }
-
-        [Fact]
-        public async Task RequestDelegateLogsFromFormIOExceptionsAsDebugAndAborts()
-        {
-            var invoked = false;
-
-            void TestAction([FromForm] int value)
-            {
-                invoked = true;
-            }
-
-            var ioException = new IOException();
-            var serviceCollection = new ServiceCollection();
-            serviceCollection.AddSingleton(LoggerFactory);
-
-            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 = RequestDelegateFactory.Create((Action<int>)TestAction);
-
-            await requestDelegate(httpContext);
-
-            Assert.False(invoked);
-            Assert.True(httpContext.RequestAborted.IsCancellationRequested);
-
-            var logMessage = Assert.Single(TestSink.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;
-
-            void TestAction([FromForm] int value)
-            {
-                invoked = true;
-            }
-
-            var invalidDataException = new InvalidDataException();
-            var serviceCollection = new ServiceCollection();
-            serviceCollection.AddSingleton(LoggerFactory);
-
-            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 = RequestDelegateFactory.Create((Action<int>)TestAction);
-
-            await requestDelegate(httpContext);
-
-            Assert.False(invoked);
-            Assert.False(httpContext.RequestAborted.IsCancellationRequested);
-            Assert.Equal(400, httpContext.Response.StatusCode);
-
-            var logMessage = Assert.Single(TestSink.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>(() => RequestDelegateFactory.Create((Action<int, int>)TestAction));
-            Assert.Throws<InvalidOperationException>(() => RequestDelegateFactory.Create((Action<int, int>)TestActionWithFlippedParams));
-        }
-
         [Fact]
         public void BuildRequestDelegateThrowsInvalidOperationExceptionGivenFromBodyOnMultipleParameters()
         {
@@ -885,26 +779,6 @@ namespace Microsoft.AspNetCore.Routing.Internal
             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 = RequestDelegateFactory.Create((Action<IFormCollection>)TestAction);
-
-            await requestDelegate(httpContext);
-
-            Assert.Same(httpContext.Request.Form, formCollectionArgument);
-        }
-
         [Fact]
         public async Task RequestDelegatePassHttpContextRequestAbortedAsCancelationToken()
         {
@@ -1180,11 +1054,6 @@ namespace Microsoft.AspNetCore.Routing.Internal
             public bool AllowEmpty { get; set; }
         }
 
-        private class FromFormAttribute : Attribute, IFromFormMetadata
-        {
-            public string? Name { get; set; }
-        }
-
         private class FromServiceAttribute : Attribute, IFromServiceMetadata
         {
         }
diff --git a/src/Http/HttpAbstractions.slnf b/src/Http/HttpAbstractions.slnf
index 6fad000bf86..2b9c64391b8 100644
--- a/src/Http/HttpAbstractions.slnf
+++ b/src/Http/HttpAbstractions.slnf
@@ -36,7 +36,7 @@
       "src\\Http\\WebUtilities\\perf\\Microbenchmarks\\Microsoft.AspNetCore.WebUtilities.Microbenchmarks.csproj",
       "src\\Http\\WebUtilities\\src\\Microsoft.AspNetCore.WebUtilities.csproj",
       "src\\Http\\WebUtilities\\test\\Microsoft.AspNetCore.WebUtilities.Tests.csproj",
-      "src\\Http\\samples\\MapActionSample\\MapActionSample.csproj",
+      "src\\Http\\samples\\MinimalSample\\MinimalSample.csproj",
       "src\\Http\\samples\\SampleApp\\HttpAbstractions.SampleApp.csproj",
       "src\\Middleware\\CORS\\src\\Microsoft.AspNetCore.Cors.csproj",
       "src\\Middleware\\HttpOverrides\\src\\Microsoft.AspNetCore.HttpOverrides.csproj",
diff --git a/src/Http/samples/MapActionSample/MapActionSample.csproj b/src/Http/samples/MinimalSample/MinimalSample.csproj
similarity index 100%
rename from src/Http/samples/MapActionSample/MapActionSample.csproj
rename to src/Http/samples/MinimalSample/MinimalSample.csproj
diff --git a/src/Http/samples/MapActionSample/Program.cs b/src/Http/samples/MinimalSample/Program.cs
similarity index 86%
rename from src/Http/samples/MapActionSample/Program.cs
rename to src/Http/samples/MinimalSample/Program.cs
index 1df9c761fcb..40697fdc666 100644
--- a/src/Http/samples/MapActionSample/Program.cs
+++ b/src/Http/samples/MinimalSample/Program.cs
@@ -22,8 +22,10 @@ using var host = Host.CreateDefaultBuilder(args)
 
                 object Json() => new { message = "Hello, World!" };
                 endpoints.MapGet("/json", (Func<object>)Json);
-            });
 
+                string SayHello(string name) => $"Hello {name}";
+                endpoints.MapGet("/hello/{name}", (Func<string, string>)SayHello);
+            });
         });
     })
     .Build();
diff --git a/src/Http/samples/MapActionSample/Properties/launchSettings.json b/src/Http/samples/MinimalSample/Properties/launchSettings.json
similarity index 100%
rename from src/Http/samples/MapActionSample/Properties/launchSettings.json
rename to src/Http/samples/MinimalSample/Properties/launchSettings.json
diff --git a/src/Http/samples/MapActionSample/appsettings.Development.json b/src/Http/samples/MinimalSample/appsettings.Development.json
similarity index 100%
rename from src/Http/samples/MapActionSample/appsettings.Development.json
rename to src/Http/samples/MinimalSample/appsettings.Development.json
diff --git a/src/Http/samples/MapActionSample/appsettings.json b/src/Http/samples/MinimalSample/appsettings.json
similarity index 100%
rename from src/Http/samples/MapActionSample/appsettings.json
rename to src/Http/samples/MinimalSample/appsettings.json
diff --git a/src/Mvc/Mvc.Core/src/FromFormAttribute.cs b/src/Mvc/Mvc.Core/src/FromFormAttribute.cs
index a2237fe1f1f..9be8b34fc53 100644
--- a/src/Mvc/Mvc.Core/src/FromFormAttribute.cs
+++ b/src/Mvc/Mvc.Core/src/FromFormAttribute.cs
@@ -11,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, IFromFormMetadata
+    public class FromFormAttribute : Attribute, IBindingSourceMetadata, IModelNameProvider
     {
         /// <inheritdoc />
         public BindingSource BindingSource => BindingSource.Form;
diff --git a/src/Mvc/Mvc.slnf b/src/Mvc/Mvc.slnf
index a534f723618..39a4d5afaf6 100644
--- a/src/Mvc/Mvc.slnf
+++ b/src/Mvc/Mvc.slnf
@@ -29,7 +29,6 @@
       "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",
-- 
GitLab