Browse Source

Add support for binding from form body to RDG (#47768)

* Migrate form-related tests to shared infrastructure

* Add support for form-binding in RDG

* Move form logging tests to shared infrastructure

* Add snapshot test for generated form code

* Pass readFormEmitted state by ref
Safia Abdalla 2 years ago
parent
commit
531f5cf52d

+ 6 - 0
src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs

@@ -165,6 +165,7 @@ public sealed class RequestDelegateGenerator : IIncrementalGenerator
             {
             {
                 var hasJsonBodyOrService = endpoints.Any(endpoint => endpoint!.EmitterContext.HasJsonBodyOrService);
                 var hasJsonBodyOrService = endpoints.Any(endpoint => endpoint!.EmitterContext.HasJsonBodyOrService);
                 var hasJsonBody = endpoints.Any(endpoint => endpoint!.EmitterContext.HasJsonBody);
                 var hasJsonBody = endpoints.Any(endpoint => endpoint!.EmitterContext.HasJsonBody);
+                var hasFormBody = endpoints.Any(endpoint => endpoint!.EmitterContext.HasFormBody);
                 var hasRouteOrQuery = endpoints.Any(endpoint => endpoint!.EmitterContext.HasRouteOrQuery);
                 var hasRouteOrQuery = endpoints.Any(endpoint => endpoint!.EmitterContext.HasRouteOrQuery);
                 var hasBindAsync = endpoints.Any(endpoint => endpoint!.EmitterContext.HasBindAsync);
                 var hasBindAsync = endpoints.Any(endpoint => endpoint!.EmitterContext.HasBindAsync);
                 var hasParsable = endpoints.Any(endpoint => endpoint!.EmitterContext.HasParsable);
                 var hasParsable = endpoints.Any(endpoint => endpoint!.EmitterContext.HasParsable);
@@ -188,6 +189,11 @@ public sealed class RequestDelegateGenerator : IIncrementalGenerator
                     codeWriter.WriteLine(RequestDelegateGeneratorSources.TryResolveBodyAsyncMethod);
                     codeWriter.WriteLine(RequestDelegateGeneratorSources.TryResolveBodyAsyncMethod);
                 }
                 }
 
 
+                if (hasFormBody)
+                {
+                    codeWriter.WriteLine(RequestDelegateGeneratorSources.TryResolveFormAsyncMethod);
+                }
+
                 if (hasBindAsync)
                 if (hasBindAsync)
                 {
                 {
                     codeWriter.WriteLine(RequestDelegateGeneratorSources.BindAsyncMethod);
                     codeWriter.WriteLine(RequestDelegateGeneratorSources.BindAsyncMethod);

+ 48 - 1
src/Http/Http.Extensions/gen/RequestDelegateGeneratorSources.cs

@@ -20,7 +20,7 @@ internal static class RequestDelegateGeneratorSources
 
 
     public static string GeneratedCodeAttribute => $@"[System.CodeDom.Compiler.GeneratedCodeAttribute(""{typeof(RequestDelegateGeneratorSources).Assembly.FullName}"", ""{typeof(RequestDelegateGeneratorSources).Assembly.GetName().Version}"")]";
     public static string GeneratedCodeAttribute => $@"[System.CodeDom.Compiler.GeneratedCodeAttribute(""{typeof(RequestDelegateGeneratorSources).Assembly.FullName}"", ""{typeof(RequestDelegateGeneratorSources).Assembly.GetName().Version}"")]";
 
 
-    public static string TryResolveBodyAsyncMethod => $$"""
+    public static string TryResolveBodyAsyncMethod => """
         private static async ValueTask<(bool, T?)> TryResolveBodyAsync<T>(HttpContext httpContext, LogOrThrowExceptionHelper logOrThrowExceptionHelper, bool allowEmpty, string parameterTypeName, string parameterName, bool isInferred = false)
         private static async ValueTask<(bool, T?)> TryResolveBodyAsync<T>(HttpContext httpContext, LogOrThrowExceptionHelper logOrThrowExceptionHelper, bool allowEmpty, string parameterTypeName, string parameterName, bool isInferred = false)
         {
         {
             var feature = httpContext.Features.Get<Microsoft.AspNetCore.Http.Features.IHttpRequestBodyDetectionFeature>();
             var feature = httpContext.Features.Get<Microsoft.AspNetCore.Http.Features.IHttpRequestBodyDetectionFeature>();
@@ -79,6 +79,53 @@ internal static class RequestDelegateGeneratorSources
         }
         }
 """;
 """;
 
 
+    public static string TryResolveFormAsyncMethod => """
+        private static async Task<(bool, object?)> TryResolveFormAsync(
+            HttpContext httpContext,
+            LogOrThrowExceptionHelper logOrThrowExceptionHelper,
+            string parameterTypeName,
+            string parameterName)
+        {
+            object? formValue = null;
+            var feature = httpContext.Features.Get<Microsoft.AspNetCore.Http.Features.IHttpRequestBodyDetectionFeature>();
+
+            if (feature?.CanHaveBody == true)
+            {
+                if (!httpContext.Request.HasFormContentType)
+                {
+                    logOrThrowExceptionHelper.UnexpectedNonFormContentType(httpContext.Request.ContentType);
+                    httpContext.Response.StatusCode = StatusCodes.Status415UnsupportedMediaType;
+                    return (false, null);
+                }
+
+                try
+                {
+                    formValue = await httpContext.Request.ReadFormAsync();
+                }
+                catch (BadHttpRequestException ex)
+                {
+                    logOrThrowExceptionHelper.RequestBodyIOException(ex);
+                    httpContext.Response.StatusCode = ex.StatusCode;
+                    return (false, null);
+                }
+                catch (IOException ex)
+                {
+                    logOrThrowExceptionHelper.RequestBodyIOException(ex);
+                    httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
+                    return (false, null);
+                }
+                catch (InvalidDataException ex)
+                {
+                    logOrThrowExceptionHelper.InvalidFormRequestBody(parameterTypeName, parameterName, ex);
+                    httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
+                    return (false, null);
+                }
+            }
+
+            return (true, formValue);
+        }
+""";
+
     public static string TryParseExplicitMethod => """
     public static string TryParseExplicitMethod => """
         private static bool TryParseExplicit<T>(string? s, IFormatProvider? provider, [MaybeNullWhen(returnValue: false)] out T result) where T: IParsable<T>
         private static bool TryParseExplicit<T>(string? s, IFormatProvider? provider, [MaybeNullWhen(returnValue: false)] out T result) where T: IParsable<T>
             => T.TryParse(s, provider, out result);
             => T.TryParse(s, provider, out result);

+ 1 - 0
src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Emitters/EmitterContext.cs

@@ -6,6 +6,7 @@ internal sealed class EmitterContext
 {
 {
     public bool HasJsonBodyOrService { get; set; }
     public bool HasJsonBodyOrService { get; set; }
     public bool HasJsonBody { get; set; }
     public bool HasJsonBody { get; set; }
+    public bool HasFormBody { get; set; }
     public bool HasRouteOrQuery { get; set; }
     public bool HasRouteOrQuery { get; set; }
     public bool HasBindAsync { get; set; }
     public bool HasBindAsync { get; set; }
     public bool HasParsable { get; set; }
     public bool HasParsable { get; set; }

+ 1 - 0
src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Emitters/EmitterExtensions.cs

@@ -13,6 +13,7 @@ internal static class EmitterExtensions
         EndpointParameterSource.Header => "header",
         EndpointParameterSource.Header => "header",
         EndpointParameterSource.Query => "query string",
         EndpointParameterSource.Query => "query string",
         EndpointParameterSource.RouteOrQuery => "route or query string",
         EndpointParameterSource.RouteOrQuery => "route or query string",
+        EndpointParameterSource.FormBody => "form",
         EndpointParameterSource.BindAsync => endpointParameter.BindMethod == BindabilityMethod.BindAsync
         EndpointParameterSource.BindAsync => endpointParameter.BindMethod == BindabilityMethod.BindAsync
             ? $"{endpointParameter.Type.ToDisplayString(SymbolDisplayFormat.CSharpShortErrorMessageFormat)}.BindAsync(HttpContext)"
             ? $"{endpointParameter.Type.ToDisplayString(SymbolDisplayFormat.CSharpShortErrorMessageFormat)}.BindAsync(HttpContext)"
             : $"{endpointParameter.Type.ToDisplayString(SymbolDisplayFormat.CSharpShortErrorMessageFormat)}.BindAsync(HttpContext, ParameterInfo)",
             : $"{endpointParameter.Type.ToDisplayString(SymbolDisplayFormat.CSharpShortErrorMessageFormat)}.BindAsync(HttpContext, ParameterInfo)",

+ 4 - 0
src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Emitters/EndpointEmitter.cs

@@ -14,6 +14,7 @@ internal static class EndpointEmitter
     {
     {
         using var stringWriter = new StringWriter(CultureInfo.InvariantCulture);
         using var stringWriter = new StringWriter(CultureInfo.InvariantCulture);
         using var parameterPreparationBuilder = new CodeWriter(stringWriter, baseIndent);
         using var parameterPreparationBuilder = new CodeWriter(stringWriter, baseIndent);
+        var readFormEmitted = false;
 
 
         foreach (var parameter in endpoint.Parameters)
         foreach (var parameter in endpoint.Parameters)
         {
         {
@@ -38,6 +39,9 @@ internal static class EndpointEmitter
                 case EndpointParameterSource.JsonBody:
                 case EndpointParameterSource.JsonBody:
                     parameter.EmitJsonBodyParameterPreparationString(parameterPreparationBuilder);
                     parameter.EmitJsonBodyParameterPreparationString(parameterPreparationBuilder);
                     break;
                     break;
+                case EndpointParameterSource.FormBody:
+                    parameter.EmitFormParameterPreparation(parameterPreparationBuilder, ref readFormEmitted);
+                    break;
                 case EndpointParameterSource.JsonBodyOrService:
                 case EndpointParameterSource.JsonBodyOrService:
                     parameter.EmitJsonBodyOrServiceParameterPreparationString(parameterPreparationBuilder);
                     parameter.EmitJsonBodyOrServiceParameterPreparationString(parameterPreparationBuilder);
                     break;
                     break;

+ 36 - 1
src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Emitters/EndpointParameterEmitter.cs

@@ -53,6 +53,41 @@ internal static class EndpointParameterEmitter
         endpointParameter.EmitParsingBlock(codeWriter);
         endpointParameter.EmitParsingBlock(codeWriter);
     }
     }
 
 
+    internal static void EmitFormParameterPreparation(this EndpointParameter endpointParameter, CodeWriter codeWriter, ref bool readFormEmitted)
+    {
+        codeWriter.WriteLine(endpointParameter.EmitParameterDiagnosticComment());
+
+        // Invoke TryResolveFormAsync once per handler so that we can
+        // avoid the blocking code-path that occurs when `httpContext.Request.Form`
+        // is invoked.
+        if (!readFormEmitted)
+        {
+            var shortParameterTypeName = endpointParameter.Type.ToDisplayString(SymbolDisplayFormat.CSharpShortErrorMessageFormat);
+            var assigningCode = $"await GeneratedRouteBuilderExtensionsCore.TryResolveFormAsync(httpContext, logOrThrowExceptionHelper, {SymbolDisplay.FormatLiteral(shortParameterTypeName, true)}, {SymbolDisplay.FormatLiteral(endpointParameter.SymbolName, true)})";
+            var resolveFormResult = $"{endpointParameter.SymbolName}_resolveFormResult";
+            codeWriter.WriteLine($"var {resolveFormResult} = {assigningCode};");
+
+            // Exit early if binding from the form has failed.
+            codeWriter.WriteLine($"if (!{resolveFormResult}.Item1)");
+            codeWriter.StartBlock();
+            codeWriter.WriteLine("return;");
+            codeWriter.EndBlock();
+            readFormEmitted = true;
+        }
+
+        codeWriter.WriteLine($"var {endpointParameter.EmitAssigningCodeResult()} = {endpointParameter.AssigningCode};");
+        if (!endpointParameter.IsOptional)
+        {
+            codeWriter.WriteLine($"if ({endpointParameter.EmitAssigningCodeResult()} == null)");
+            codeWriter.StartBlock();
+            codeWriter.WriteLine("wasParamCheckFailure = true;");
+            codeWriter.WriteLine($@"logOrThrowExceptionHelper.RequiredParameterNotProvided({SymbolDisplay.FormatLiteral(endpointParameter.Type.ToDisplayString(SymbolDisplayFormat.CSharpShortErrorMessageFormat), true)}, {SymbolDisplay.FormatLiteral(endpointParameter.SymbolName, true)}, {SymbolDisplay.FormatLiteral(endpointParameter.ToMessageString(), true)});");
+            codeWriter.EndBlock();
+        }
+        codeWriter.WriteLine($"var {endpointParameter.EmitTempArgument()} = {endpointParameter.EmitAssigningCodeResult()};");
+        endpointParameter.EmitParsingBlock(codeWriter);
+    }
+
     internal static void EmitParsingBlock(this EndpointParameter endpointParameter, CodeWriter codeWriter)
     internal static void EmitParsingBlock(this EndpointParameter endpointParameter, CodeWriter codeWriter)
     {
     {
         if (endpointParameter.IsArray && endpointParameter.IsParsable)
         if (endpointParameter.IsArray && endpointParameter.IsParsable)
@@ -255,7 +290,7 @@ internal static class EndpointParameterEmitter
 
 
     public static string EmitArgument(this EndpointParameter endpointParameter) => endpointParameter.Source switch
     public static string EmitArgument(this EndpointParameter endpointParameter) => endpointParameter.Source switch
     {
     {
-        EndpointParameterSource.JsonBody or EndpointParameterSource.Route or EndpointParameterSource.RouteOrQuery or EndpointParameterSource.JsonBodyOrService => endpointParameter.IsOptional ? endpointParameter.EmitHandlerArgument() : $"{endpointParameter.EmitHandlerArgument()}!",
+        EndpointParameterSource.JsonBody or EndpointParameterSource.Route or EndpointParameterSource.RouteOrQuery or EndpointParameterSource.JsonBodyOrService or EndpointParameterSource.FormBody => endpointParameter.IsOptional ? endpointParameter.EmitHandlerArgument() : $"{endpointParameter.EmitHandlerArgument()}!",
         EndpointParameterSource.Unknown => throw new Exception("Unreachable!"),
         EndpointParameterSource.Unknown => throw new Exception("Unreachable!"),
         _ => endpointParameter.EmitHandlerArgument()
         _ => endpointParameter.EmitHandlerArgument()
     };
     };

+ 2 - 0
src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Endpoint.cs

@@ -72,6 +72,7 @@ internal class Endpoint
                     break;
                     break;
                 case EndpointParameterSource.JsonBody:
                 case EndpointParameterSource.JsonBody:
                 case EndpointParameterSource.JsonBodyOrService:
                 case EndpointParameterSource.JsonBodyOrService:
+                case EndpointParameterSource.FormBody:
                     IsAwaitable = true;
                     IsAwaitable = true;
                     break;
                     break;
                 case EndpointParameterSource.Unknown:
                 case EndpointParameterSource.Unknown:
@@ -89,6 +90,7 @@ internal class Endpoint
 
 
         EmitterContext.HasJsonBodyOrService = Parameters.Any(parameter => parameter.Source == EndpointParameterSource.JsonBodyOrService);
         EmitterContext.HasJsonBodyOrService = Parameters.Any(parameter => parameter.Source == EndpointParameterSource.JsonBodyOrService);
         EmitterContext.HasJsonBody = Parameters.Any(parameter => parameter.Source == EndpointParameterSource.JsonBody);
         EmitterContext.HasJsonBody = Parameters.Any(parameter => parameter.Source == EndpointParameterSource.JsonBody);
+        EmitterContext.HasFormBody = Parameters.Any(parameter => parameter.Source == EndpointParameterSource.FormBody);
         EmitterContext.HasRouteOrQuery = Parameters.Any(parameter => parameter.Source == EndpointParameterSource.RouteOrQuery);
         EmitterContext.HasRouteOrQuery = Parameters.Any(parameter => parameter.Source == EndpointParameterSource.RouteOrQuery);
         EmitterContext.HasBindAsync = Parameters.Any(parameter => parameter.Source == EndpointParameterSource.BindAsync);
         EmitterContext.HasBindAsync = Parameters.Any(parameter => parameter.Source == EndpointParameterSource.BindAsync);
         EmitterContext.HasParsable = Parameters.Any(parameter => parameter.IsParsable);
         EmitterContext.HasParsable = Parameters.Any(parameter => parameter.IsParsable);

+ 37 - 7
src/Http/Http.Extensions/gen/StaticRouteHandlerModel/EndpointParameter.cs

@@ -49,9 +49,28 @@ internal class EndpointParameter
             IsParsable = TryGetParsability(parameter, wellKnownTypes, out var parsingBlockEmitter);
             IsParsable = TryGetParsability(parameter, wellKnownTypes, out var parsingBlockEmitter);
             ParsingBlockEmitter = parsingBlockEmitter;
             ParsingBlockEmitter = parsingBlockEmitter;
         }
         }
-        else if (parameter.HasAttributeImplementingInterface(wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_Metadata_IFromFormMetadata), out _))
+        else if (parameter.HasAttributeImplementingInterface(wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_Metadata_IFromFormMetadata), out var fromFormAttribute))
         {
         {
-            Source = EndpointParameterSource.Unknown;
+            Source = EndpointParameterSource.FormBody;
+            LookupName = GetEscapedParameterName(fromFormAttribute, parameter.Name);
+            if (SymbolEqualityComparer.Default.Equals(parameter.Type, wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_IFormFileCollection)))
+            {
+                AssigningCode = "httpContext.Request.Form.Files";
+            }
+            else if (SymbolEqualityComparer.Default.Equals(parameter.Type, wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_IFormFile)))
+            {
+                AssigningCode = $"httpContext.Request.Form.Files[{SymbolDisplay.FormatLiteral(LookupName, true)}]";
+            }
+            else if (SymbolEqualityComparer.Default.Equals(parameter.Type, wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_IFormCollection)))
+            {
+                AssigningCode = "httpContext.Request.Form";
+            }
+            else
+            {
+                AssigningCode = $"(string?)httpContext.Request.Form[{SymbolDisplay.FormatLiteral(LookupName, true)}]";
+                IsParsable = TryGetParsability(parameter, wellKnownTypes, out var parsingBlockEmitter);
+                ParsingBlockEmitter = parsingBlockEmitter;
+            }
         }
         }
         else if (TryGetExplicitFromJsonBody(parameter, wellKnownTypes, out var isOptional))
         else if (TryGetExplicitFromJsonBody(parameter, wellKnownTypes, out var isOptional))
         {
         {
@@ -59,7 +78,6 @@ internal class EndpointParameter
             {
             {
                 Source = EndpointParameterSource.SpecialType;
                 Source = EndpointParameterSource.SpecialType;
                 AssigningCode = "httpContext.Request.Body";
                 AssigningCode = "httpContext.Request.Body";
-
             }
             }
             else if (SymbolEqualityComparer.Default.Equals(parameter.Type, wellKnownTypes.Get(WellKnownType.System_IO_Pipelines_PipeReader)))
             else if (SymbolEqualityComparer.Default.Equals(parameter.Type, wellKnownTypes.Get(WellKnownType.System_IO_Pipelines_PipeReader)))
             {
             {
@@ -85,11 +103,23 @@ internal class EndpointParameter
             Source = EndpointParameterSource.SpecialType;
             Source = EndpointParameterSource.SpecialType;
             AssigningCode = specialTypeAssigningCode;
             AssigningCode = specialTypeAssigningCode;
         }
         }
-        else if (SymbolEqualityComparer.Default.Equals(parameter.Type, wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_IFormFile)) ||
-                 SymbolEqualityComparer.Default.Equals(parameter.Type, wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_IFormFileCollection)) ||
-                 SymbolEqualityComparer.Default.Equals(parameter.Type, wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_IFormCollection)))
+        else if (SymbolEqualityComparer.Default.Equals(parameter.Type, wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_IFormFileCollection)))
         {
         {
-            Source = EndpointParameterSource.Unknown;
+            Source = EndpointParameterSource.FormBody;
+            LookupName = parameter.Name;
+            AssigningCode = "httpContext.Request.Form.Files";
+        }
+        else if (SymbolEqualityComparer.Default.Equals(parameter.Type, wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_IFormFile)))
+        {
+            Source = EndpointParameterSource.FormBody;
+            LookupName = parameter.Name;
+            AssigningCode = $"httpContext.Request.Form.Files[{SymbolDisplay.FormatLiteral(LookupName, true)}]";
+        }
+        else if (SymbolEqualityComparer.Default.Equals(parameter.Type, wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_IFormCollection)))
+        {
+            Source = EndpointParameterSource.FormBody;
+            LookupName = parameter.Name;
+            AssigningCode = "httpContext.Request.Form";
         }
         }
         else if (HasBindAsync(parameter, wellKnownTypes, out var bindMethod))
         else if (HasBindAsync(parameter, wellKnownTypes, out var bindMethod))
         {
         {

+ 21 - 910
src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs

@@ -2893,84 +2893,6 @@ public partial class RequestDelegateFactoryTests : LoggedTest
         Assert.Throws<InvalidOperationException>(() => RequestDelegateFactory.Create(TestJsonAndFormWithAttribute));
         Assert.Throws<InvalidOperationException>(() => RequestDelegateFactory.Create(TestJsonAndFormWithAttribute));
     }
     }
 
 
-    [Fact]
-    public async Task RequestDelegatePopulatesFromIFormFileCollectionParameter()
-    {
-        IFormFileCollection? formFilesArgument = null;
-
-        void TestAction(IFormFileCollection formFiles)
-        {
-            formFilesArgument = formFiles;
-        }
-
-        var fileContent = new StringContent("hello", Encoding.UTF8, "application/octet-stream");
-        var form = new MultipartFormDataContent("some-boundary");
-        form.Add(fileContent, "file", "file.txt");
-
-        var stream = new MemoryStream();
-        await form.CopyToAsync(stream);
-
-        stream.Seek(0, SeekOrigin.Begin);
-
-        var httpContext = CreateHttpContext();
-        httpContext.Request.Body = stream;
-        httpContext.Request.Headers["Content-Type"] = "multipart/form-data;boundary=some-boundary";
-        httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
-
-        var factoryResult = RequestDelegateFactory.Create(TestAction);
-        var requestDelegate = factoryResult.RequestDelegate;
-
-        await requestDelegate(httpContext);
-
-        Assert.Equal(httpContext.Request.Form.Files, formFilesArgument);
-        Assert.NotNull(formFilesArgument!["file"]);
-
-        var allAcceptsMetadata = factoryResult.EndpointMetadata.OfType<IAcceptsMetadata>();
-        var acceptsMetadata = Assert.Single(allAcceptsMetadata);
-
-        Assert.NotNull(acceptsMetadata);
-        Assert.Equal(new[] { "multipart/form-data" }, acceptsMetadata.ContentTypes);
-    }
-
-    [Fact]
-    public async Task RequestDelegatePopulatesFromIFormFileCollectionParameterWithAttribute()
-    {
-        IFormFileCollection? formFilesArgument = null;
-
-        void TestAction([FromForm] IFormFileCollection formFiles)
-        {
-            formFilesArgument = formFiles;
-        }
-
-        var fileContent = new StringContent("hello", Encoding.UTF8, "application/octet-stream");
-        var form = new MultipartFormDataContent("some-boundary");
-        form.Add(fileContent, "file", "file.txt");
-
-        var stream = new MemoryStream();
-        await form.CopyToAsync(stream);
-
-        stream.Seek(0, SeekOrigin.Begin);
-
-        var httpContext = CreateHttpContext();
-        httpContext.Request.Body = stream;
-        httpContext.Request.Headers["Content-Type"] = "multipart/form-data;boundary=some-boundary";
-        httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
-
-        var factoryResult = RequestDelegateFactory.Create(TestAction);
-        var requestDelegate = factoryResult.RequestDelegate;
-
-        await requestDelegate(httpContext);
-
-        Assert.Equal(httpContext.Request.Form.Files, formFilesArgument);
-        Assert.NotNull(formFilesArgument!["file"]);
-
-        var allAcceptsMetadata = factoryResult.EndpointMetadata.OfType<IAcceptsMetadata>();
-        var acceptsMetadata = Assert.Single(allAcceptsMetadata);
-
-        Assert.NotNull(acceptsMetadata);
-        Assert.Equal(new[] { "multipart/form-data" }, acceptsMetadata.ContentTypes);
-    }
-
     [Fact]
     [Fact]
     public void CreateThrowsNotSupportedExceptionIfIFormFileCollectionHasMetadataParameterName()
     public void CreateThrowsNotSupportedExceptionIfIFormFileCollectionHasMetadataParameterName()
     {
     {
@@ -2985,866 +2907,55 @@ public partial class RequestDelegateFactoryTests : LoggedTest
         Assert.Equal("Assigning a value to the IFromFormMetadata.Name property is not supported for parameters of type IFormFileCollection.", nse.Message);
         Assert.Equal("Assigning a value to the IFromFormMetadata.Name property is not supported for parameters of type IFormFileCollection.", nse.Message);
     }
     }
 
 
-    [Fact]
-    public async Task RequestDelegatePopulatesFromIFormFileParameter()
-    {
-        IFormFile? fileArgument = null;
-
-        void TestAction(IFormFile file)
-        {
-            fileArgument = file;
-        }
-
-        var fileContent = new StringContent("hello", Encoding.UTF8, "application/octet-stream");
-        var form = new MultipartFormDataContent("some-boundary");
-        form.Add(fileContent, "file", "file.txt");
-
-        var stream = new MemoryStream();
-        await form.CopyToAsync(stream);
-
-        stream.Seek(0, SeekOrigin.Begin);
-
-        var httpContext = CreateHttpContext();
-        httpContext.Request.Body = stream;
-        httpContext.Request.Headers["Content-Type"] = "multipart/form-data;boundary=some-boundary";
-        httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
-
-        var factoryResult = RequestDelegateFactory.Create(TestAction);
-        var requestDelegate = factoryResult.RequestDelegate;
-
-        await requestDelegate(httpContext);
-
-        Assert.Equal(httpContext.Request.Form.Files["file"], fileArgument);
-        Assert.Equal("file.txt", fileArgument!.FileName);
-        Assert.Equal("file", fileArgument.Name);
-    }
-
-    [Fact]
-    public async Task RequestDelegatePopulatesFromOptionalIFormFileParameter()
+    private readonly struct TraceIdentifier
     {
     {
-        IFormFile? fileArgument = null;
-
-        void TestAction(IFormFile? file)
+        private TraceIdentifier(string id)
         {
         {
-            fileArgument = file;
+            Id = id;
         }
         }
 
 
-        var fileContent = new StringContent("hello", Encoding.UTF8, "application/octet-stream");
-        var form = new MultipartFormDataContent("some-boundary");
-        form.Add(fileContent, "file", "file.txt");
-
-        var stream = new MemoryStream();
-        await form.CopyToAsync(stream);
-
-        stream.Seek(0, SeekOrigin.Begin);
-
-        var httpContext = CreateHttpContext();
-        httpContext.Request.Body = stream;
-        httpContext.Request.Headers["Content-Type"] = "multipart/form-data;boundary=some-boundary";
-        httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
-
-        var factoryResult = RequestDelegateFactory.Create(TestAction);
-        var requestDelegate = factoryResult.RequestDelegate;
-
-        await requestDelegate(httpContext);
-
-        Assert.Equal(httpContext.Request.Form.Files["file"], fileArgument);
-        Assert.Equal("file.txt", fileArgument!.FileName);
-        Assert.Equal("file", fileArgument.Name);
-    }
+        public string Id { get; }
 
 
-    [Fact]
-    public async Task RequestDelegatePopulatesFromMultipleRequiredIFormFileParameters()
-    {
-        IFormFile? file1Argument = null;
-        IFormFile? file2Argument = null;
+        public static implicit operator string(TraceIdentifier value) => value.Id;
 
 
-        void TestAction(IFormFile file1, IFormFile file2)
+        public static ValueTask<TraceIdentifier> BindAsync(HttpContext context)
         {
         {
-            file1Argument = file1;
-            file2Argument = file2;
+            return ValueTask.FromResult(new TraceIdentifier(context.TraceIdentifier));
         }
         }
-
-        var fileContent1 = new StringContent("hello", Encoding.UTF8, "application/octet-stream");
-        var fileContent2 = new StringContent("there", Encoding.UTF8, "application/octet-stream");
-        var form = new MultipartFormDataContent("some-boundary");
-        form.Add(fileContent1, "file1", "file1.txt");
-        form.Add(fileContent2, "file2", "file2.txt");
-
-        var stream = new MemoryStream();
-        await form.CopyToAsync(stream);
-
-        stream.Seek(0, SeekOrigin.Begin);
-
-        var httpContext = CreateHttpContext();
-        httpContext.Request.Body = stream;
-        httpContext.Request.Headers["Content-Type"] = "multipart/form-data;boundary=some-boundary";
-        httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
-
-        var factoryResult = RequestDelegateFactory.Create(TestAction);
-        var requestDelegate = factoryResult.RequestDelegate;
-
-        await requestDelegate(httpContext);
-
-        Assert.Equal(httpContext.Request.Form.Files["file1"], file1Argument);
-        Assert.Equal("file1.txt", file1Argument!.FileName);
-        Assert.Equal("file1", file1Argument.Name);
-
-        Assert.Equal(httpContext.Request.Form.Files["file2"], file2Argument);
-        Assert.Equal("file2.txt", file2Argument!.FileName);
-        Assert.Equal("file2", file2Argument.Name);
     }
     }
 
 
-    [Fact]
-    public async Task RequestDelegatePopulatesFromOptionalMissingIFormFileParameter()
+    public static TheoryData<HttpContent, string> FormContent
     {
     {
-        IFormFile? file1Argument = null;
-        IFormFile? file2Argument = null;
-
-        void TestAction(IFormFile? file1, IFormFile? file2)
+        get
         {
         {
-            file1Argument = file1;
-            file2Argument = file2;
-        }
-
-        var fileContent = new StringContent("hello", Encoding.UTF8, "application/octet-stream");
-        var form = new MultipartFormDataContent("some-boundary");
-        form.Add(fileContent, "file1", "file.txt");
-
-        var stream = new MemoryStream();
-        await form.CopyToAsync(stream);
-
-        stream.Seek(0, SeekOrigin.Begin);
-
-        var httpContext = CreateHttpContext();
-        httpContext.Request.Body = stream;
-        httpContext.Request.Headers["Content-Type"] = "multipart/form-data;boundary=some-boundary";
-        httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
-
-        var factoryResult = RequestDelegateFactory.Create(TestAction);
-        var requestDelegate = factoryResult.RequestDelegate;
-
-        await requestDelegate(httpContext);
-
-        Assert.Equal(httpContext.Request.Form.Files["file1"], file1Argument);
-        Assert.NotNull(file1Argument);
-
-        Assert.Equal(httpContext.Request.Form.Files["file2"], file2Argument);
-        Assert.Null(file2Argument);
-
-        var allAcceptsMetadata = factoryResult.EndpointMetadata.OfType<IAcceptsMetadata>();
-        var acceptsMetadata = Assert.Single(allAcceptsMetadata);
+            var dataset = new TheoryData<HttpContent, string>();
 
 
-        Assert.NotNull(acceptsMetadata);
-        Assert.Equal(new[] { "multipart/form-data" }, acceptsMetadata.ContentTypes);
-    }
+            var multipartFormData = new MultipartFormDataContent("some-boundary");
+            multipartFormData.Add(new StringContent("hello"), "message");
+            multipartFormData.Add(new StringContent("foo"), "name");
+            dataset.Add(multipartFormData, "multipart/form-data;boundary=some-boundary");
 
 
-    [Fact]
-    public async Task RequestDelegatePopulatesFromIFormFileParameterWithMetadata()
-    {
-        IFormFile? fileArgument = null;
+            var urlEncondedForm = new FormUrlEncodedContent(new Dictionary<string, string> { ["message"] = "hello", ["name"] = "foo" });
+            dataset.Add(urlEncondedForm, "application/x-www-form-urlencoded");
 
 
-        void TestAction([FromForm(Name = "my_file")] IFormFile file)
-        {
-            fileArgument = file;
+            return dataset;
         }
         }
-
-        var fileContent = new StringContent("hello", Encoding.UTF8, "application/octet-stream");
-        var form = new MultipartFormDataContent("some-boundary");
-        form.Add(fileContent, "my_file", "file.txt");
-
-        var stream = new MemoryStream();
-        await form.CopyToAsync(stream);
-
-        stream.Seek(0, SeekOrigin.Begin);
-
-        var httpContext = CreateHttpContext();
-        httpContext.Request.Body = stream;
-        httpContext.Request.Headers["Content-Type"] = "multipart/form-data;boundary=some-boundary";
-        httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
-
-        var factoryResult = RequestDelegateFactory.Create(TestAction);
-        var requestDelegate = factoryResult.RequestDelegate;
-
-        await requestDelegate(httpContext);
-
-        Assert.Equal(httpContext.Request.Form.Files["my_file"], fileArgument);
-        Assert.Equal("file.txt", fileArgument!.FileName);
-        Assert.Equal("my_file", fileArgument.Name);
     }
     }
 
 
     [Fact]
     [Fact]
-    public async Task RequestDelegatePopulatesFromIFormFileAndBoundParameter()
-    {
-        IFormFile? fileArgument = null;
-        TraceIdentifier traceIdArgument = default;
-
-        void TestAction(IFormFile? file, TraceIdentifier traceId)
-        {
-            fileArgument = file;
-            traceIdArgument = traceId;
-        }
-
-        var fileContent = new StringContent("hello", Encoding.UTF8, "application/octet-stream");
-        var form = new MultipartFormDataContent("some-boundary");
-        form.Add(fileContent, "file", "file.txt");
-
-        var stream = new MemoryStream();
-        await form.CopyToAsync(stream);
-
-        stream.Seek(0, SeekOrigin.Begin);
-
-        var httpContext = CreateHttpContext();
-        httpContext.Request.Body = stream;
-        httpContext.Request.Headers["Content-Type"] = "multipart/form-data;boundary=some-boundary";
-        httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
-        httpContext.TraceIdentifier = "my-trace-id";
-
-        var factoryResult = RequestDelegateFactory.Create(TestAction);
-        var requestDelegate = factoryResult.RequestDelegate;
-
-        await requestDelegate(httpContext);
-
-        Assert.Equal(httpContext.Request.Form.Files["file"], fileArgument);
-        Assert.Equal("file.txt", fileArgument!.FileName);
-        Assert.Equal("file", fileArgument.Name);
-
-        Assert.Equal("my-trace-id", traceIdArgument.Id);
-    }
-
-    private readonly struct TraceIdentifier
+    public void CreateThrowsNotSupportedExceptionIfIFormCollectionHasMetadataParameterName()
     {
     {
-        private TraceIdentifier(string id)
-        {
-            Id = id;
-        }
-
-        public string Id { get; }
-
-        public static implicit operator string(TraceIdentifier value) => value.Id;
+        IFormCollection? formArgument = null;
 
 
-        public static ValueTask<TraceIdentifier> BindAsync(HttpContext context)
+        void TestAction([FromForm(Name = "foo")] IFormCollection formCollection)
         {
         {
-            return ValueTask.FromResult(new TraceIdentifier(context.TraceIdentifier));
-        }
-    }
-
-    [Theory]
-    [InlineData(true)]
-    [InlineData(false)]
-    public async Task RequestDelegateRejectsNonFormContent(bool shouldThrow)
-    {
-        var httpContext = new DefaultHttpContext();
-        httpContext.Request.Headers["Content-Type"] = "application/xml";
-        httpContext.Request.Headers["Content-Length"] = "1";
-        httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
-
-        var serviceCollection = new ServiceCollection();
-        serviceCollection.AddSingleton(LoggerFactory);
-        httpContext.RequestServices = serviceCollection.BuildServiceProvider();
-
-        var factoryResult = RequestDelegateFactory.Create((HttpContext context, IFormFile file) =>
-        {
-        }, new RequestDelegateFactoryOptions() { ThrowOnBadRequest = shouldThrow });
-        var requestDelegate = factoryResult.RequestDelegate;
-
-        var request = requestDelegate(httpContext);
-
-        if (shouldThrow)
-        {
-            var ex = await Assert.ThrowsAsync<BadHttpRequestException>(() => request);
-            Assert.Equal("Expected a supported form media type but got \"application/xml\".", ex.Message);
-            Assert.Equal(StatusCodes.Status415UnsupportedMediaType, ex.StatusCode);
-        }
-        else
-        {
-            await request;
-
-            Assert.Equal(415, httpContext.Response.StatusCode);
-            var logMessage = Assert.Single(TestSink.Writes);
-            Assert.Equal(new EventId(7, "UnexpectedContentType"), logMessage.EventId);
-            Assert.Equal(LogLevel.Debug, logMessage.LogLevel);
-            Assert.Equal("Expected a supported form media type but got \"application/xml\".", logMessage.Message);
-        }
-    }
-
-    [Fact]
-    public async Task RequestDelegateSets400ResponseIfRequiredFileNotSpecified()
-    {
-        var invoked = false;
-
-        void TestAction(IFormFile file)
-        {
-            invoked = true;
-        }
-
-        var fileContent = new StringContent("hello", Encoding.UTF8, "application/octet-stream");
-        var form = new MultipartFormDataContent("some-boundary");
-        form.Add(fileContent, "some-other-file", "file.txt");
-
-        var stream = new MemoryStream();
-        await form.CopyToAsync(stream);
-
-        stream.Seek(0, SeekOrigin.Begin);
-
-        var httpContext = CreateHttpContext();
-        httpContext.Request.Body = stream;
-        httpContext.Request.Headers["Content-Type"] = "multipart/form-data;boundary=some-boundary";
-        httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
-
-        var factoryResult = RequestDelegateFactory.Create(TestAction);
-        var requestDelegate = factoryResult.RequestDelegate;
-
-        await requestDelegate(httpContext);
-
-        Assert.False(invoked);
-        Assert.Equal(400, httpContext.Response.StatusCode);
-    }
-
-    [Fact]
-    public async Task RequestDelegatePopulatesFromBothFormFileCollectionAndFormFileParameters()
-    {
-        IFormFileCollection? formFilesArgument = null;
-        IFormFile? fileArgument = null;
-
-        void TestAction(IFormFileCollection formFiles, IFormFile file)
-        {
-            formFilesArgument = formFiles;
-            fileArgument = file;
-        }
-
-        var fileContent = new StringContent("hello", Encoding.UTF8, "application/octet-stream");
-        var form = new MultipartFormDataContent("some-boundary");
-        form.Add(fileContent, "file", "file.txt");
-
-        var stream = new MemoryStream();
-        await form.CopyToAsync(stream);
-
-        stream.Seek(0, SeekOrigin.Begin);
-
-        var httpContext = CreateHttpContext();
-        httpContext.Request.Body = stream;
-        httpContext.Request.Headers["Content-Type"] = "multipart/form-data;boundary=some-boundary";
-        httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
-
-        var factoryResult = RequestDelegateFactory.Create(TestAction);
-        var requestDelegate = factoryResult.RequestDelegate;
-
-        await requestDelegate(httpContext);
-
-        Assert.Equal(httpContext.Request.Form.Files, formFilesArgument);
-        Assert.NotNull(formFilesArgument!["file"]);
-
-        Assert.Equal(httpContext.Request.Form.Files["file"], fileArgument);
-        Assert.Equal("file.txt", fileArgument!.FileName);
-        Assert.Equal("file", fileArgument.Name);
-
-        var allAcceptsMetadata = factoryResult.EndpointMetadata.OfType<IAcceptsMetadata>();
-        var acceptsMetadata = Assert.Single(allAcceptsMetadata);
-
-        Assert.NotNull(acceptsMetadata);
-        Assert.Equal(new[] { "multipart/form-data" }, acceptsMetadata.ContentTypes);
-    }
-
-    [Theory]
-    [InlineData("Authorization", "bearer my-token")]
-    [InlineData("Cookie", ".AspNetCore.Auth=abc123")]
-    public async Task RequestDelegatePopulatesFromIFormFileParameterIfRequestContainsSecureHeader(
-        string headerName,
-        string headerValue)
-    {
-        IFormFile? fileArgument = null;
-        TraceIdentifier traceIdArgument = default;
-
-        void TestAction(IFormFile? file, TraceIdentifier traceId)
-        {
-            fileArgument = file;
-            traceIdArgument = traceId;
-        }
-
-        var fileContent = new StringContent("hello", Encoding.UTF8, "application/octet-stream");
-        var form = new MultipartFormDataContent("some-boundary");
-        form.Add(fileContent, "file", "file.txt");
-
-        var stream = new MemoryStream();
-        await form.CopyToAsync(stream);
-
-        stream.Seek(0, SeekOrigin.Begin);
-
-        var httpContext = CreateHttpContext();
-        httpContext.Request.Body = stream;
-        httpContext.Request.Headers[headerName] = headerValue;
-        httpContext.Request.Headers["Content-Type"] = "multipart/form-data;boundary=some-boundary";
-        httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
-        httpContext.TraceIdentifier = "my-trace-id";
-
-        var factoryResult = RequestDelegateFactory.Create(TestAction);
-        var requestDelegate = factoryResult.RequestDelegate;
-
-        await requestDelegate(httpContext);
-
-        Assert.Equal(httpContext.Request.Form.Files["file"], fileArgument);
-        Assert.Equal("file.txt", fileArgument!.FileName);
-        Assert.Equal("file", fileArgument.Name);
-
-        Assert.Equal("my-trace-id", traceIdArgument.Id);
-    }
-
-    [Fact]
-    public async Task RequestDelegatePopulatesFromIFormFileParameterIfRequestHasClientCertificate()
-    {
-        IFormFile? fileArgument = null;
-        TraceIdentifier traceIdArgument = default;
-
-        void TestAction(IFormFile? file, TraceIdentifier traceId)
-        {
-            fileArgument = file;
-            traceIdArgument = traceId;
-        }
-
-        var fileContent = new StringContent("hello", Encoding.UTF8, "application/octet-stream");
-        var form = new MultipartFormDataContent("some-boundary");
-        form.Add(fileContent, "file", "file.txt");
-
-        var stream = new MemoryStream();
-        await form.CopyToAsync(stream);
-
-        stream.Seek(0, SeekOrigin.Begin);
-
-        var httpContext = CreateHttpContext();
-        httpContext.Request.Body = stream;
-        httpContext.Request.Headers["Content-Type"] = "multipart/form-data;boundary=some-boundary";
-        httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
-        httpContext.TraceIdentifier = "my-trace-id";
-
-#pragma warning disable SYSLIB0026 // Type or member is obsolete
-        var clientCertificate = new X509Certificate2();
-#pragma warning restore SYSLIB0026 // Type or member is obsolete
-
-        httpContext.Features.Set<ITlsConnectionFeature>(new TlsConnectionFeature(clientCertificate));
-
-        var factoryResult = RequestDelegateFactory.Create(TestAction);
-        var requestDelegate = factoryResult.RequestDelegate;
-
-        await requestDelegate(httpContext);
-
-        Assert.Equal(httpContext.Request.Form.Files["file"], fileArgument);
-        Assert.Equal("file.txt", fileArgument!.FileName);
-        Assert.Equal("file", fileArgument.Name);
-
-        Assert.Equal("my-trace-id", traceIdArgument.Id);
-    }
-
-    public static TheoryData<HttpContent, string> FormContent
-    {
-        get
-        {
-            var dataset = new TheoryData<HttpContent, string>();
-
-            var multipartFormData = new MultipartFormDataContent("some-boundary");
-            multipartFormData.Add(new StringContent("hello"), "message");
-            multipartFormData.Add(new StringContent("foo"), "name");
-            dataset.Add(multipartFormData, "multipart/form-data;boundary=some-boundary");
-
-            var urlEncondedForm = new FormUrlEncodedContent(new Dictionary<string, string> { ["message"] = "hello", ["name"] = "foo" });
-            dataset.Add(urlEncondedForm, "application/x-www-form-urlencoded");
-
-            return dataset;
-        }
-    }
-
-    [Fact]
-    public void CreateThrowsNotSupportedExceptionIfIFormCollectionHasMetadataParameterName()
-    {
-        IFormCollection? formArgument = null;
-
-        void TestAction([FromForm(Name = "foo")] IFormCollection formCollection)
-        {
-            formArgument = formCollection;
+            formArgument = formCollection;
         }
         }
 
 
         var nse = Assert.Throws<NotSupportedException>(() => RequestDelegateFactory.Create(TestAction));
         var nse = Assert.Throws<NotSupportedException>(() => RequestDelegateFactory.Create(TestAction));
         Assert.Equal("Assigning a value to the IFromFormMetadata.Name property is not supported for parameters of type IFormCollection.", nse.Message);
         Assert.Equal("Assigning a value to the IFromFormMetadata.Name property is not supported for parameters of type IFormCollection.", nse.Message);
     }
     }
 
 
-    [Theory]
-    [MemberData(nameof(FormContent))]
-    public async Task RequestDelegatePopulatesFromIFormCollectionParameter(HttpContent content, string contentType)
-    {
-        IFormCollection? formArgument = null;
-
-        void TestAction(IFormCollection formCollection)
-        {
-            formArgument = formCollection;
-        }
-
-        var stream = new MemoryStream();
-        await content.CopyToAsync(stream);
-
-        stream.Seek(0, SeekOrigin.Begin);
-
-        var httpContext = CreateHttpContext();
-        httpContext.Request.Body = stream;
-        httpContext.Request.Headers["Content-Type"] = contentType;
-        httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
-
-        var factoryResult = RequestDelegateFactory.Create(TestAction);
-        var requestDelegate = factoryResult.RequestDelegate;
-
-        await requestDelegate(httpContext);
-
-        Assert.Equal(httpContext.Request.Form, formArgument);
-        Assert.NotNull(formArgument);
-        Assert.Collection(formArgument!,
-            (item) =>
-            {
-                Assert.Equal("message", item.Key);
-                Assert.Equal("hello", item.Value);
-            },
-            (item) =>
-            {
-                Assert.Equal("name", item.Key);
-                Assert.Equal("foo", item.Value);
-            });
-
-        var allAcceptsMetadata = factoryResult.EndpointMetadata.OfType<IAcceptsMetadata>();
-        var acceptsMetadata = Assert.Single(allAcceptsMetadata);
-
-        Assert.NotNull(acceptsMetadata);
-        Assert.Equal(new[] { "multipart/form-data", "application/x-www-form-urlencoded" }, acceptsMetadata.ContentTypes);
-    }
-
-    [Theory]
-    [MemberData(nameof(FormContent))]
-    public async Task RequestDelegatePopulatesFromIFormCollectionParameterWithAttribute(HttpContent content, string contentType)
-    {
-        IFormCollection? formArgument = null;
-
-        void TestAction([FromForm] IFormCollection formCollection)
-        {
-            formArgument = formCollection;
-        }
-
-        var stream = new MemoryStream();
-        await content.CopyToAsync(stream);
-
-        stream.Seek(0, SeekOrigin.Begin);
-
-        var httpContext = CreateHttpContext();
-        httpContext.Request.Body = stream;
-        httpContext.Request.Headers["Content-Type"] = contentType;
-        httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
-
-        var factoryResult = RequestDelegateFactory.Create(TestAction);
-        var requestDelegate = factoryResult.RequestDelegate;
-
-        await requestDelegate(httpContext);
-
-        Assert.Equal(httpContext.Request.Form, formArgument);
-        Assert.NotNull(formArgument);
-        Assert.Collection(formArgument!,
-            (item) =>
-            {
-                Assert.Equal("message", item.Key);
-                Assert.Equal("hello", item.Value);
-            },
-            (item) =>
-            {
-                Assert.Equal("name", item.Key);
-                Assert.Equal("foo", item.Value);
-            });
-
-        var allAcceptsMetadata = factoryResult.EndpointMetadata.OfType<IAcceptsMetadata>();
-        var acceptsMetadata = Assert.Single(allAcceptsMetadata);
-
-        Assert.NotNull(acceptsMetadata);
-        Assert.Equal(new[] { "multipart/form-data", "application/x-www-form-urlencoded" }, acceptsMetadata.ContentTypes);
-    }
-
-    [Theory]
-    [MemberData(nameof(FormContent))]
-    public async Task RequestDelegatePopulatesFromOptionalFormParameter(HttpContent content, string contentType)
-    {
-        string? messageArgument = null;
-
-        void TestAction([FromForm] string? message)
-        {
-            messageArgument = message;
-        }
-
-        var stream = new MemoryStream();
-        await content.CopyToAsync(stream);
-
-        stream.Seek(0, SeekOrigin.Begin);
-
-        var httpContext = CreateHttpContext();
-        httpContext.Request.Body = stream;
-        httpContext.Request.Headers["Content-Type"] = contentType;
-        httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
-
-        var factoryResult = RequestDelegateFactory.Create(TestAction);
-        var requestDelegate = factoryResult.RequestDelegate;
-
-        await requestDelegate(httpContext);
-
-        Assert.Equal(httpContext.Request.Form["message"], messageArgument);
-    }
-
-    [Theory]
-    [MemberData(nameof(FormContent))]
-    public async Task RequestDelegatePopulatesFromMultipleRequiredFormParameters(HttpContent content, string contentType)
-    {
-        string? messageArgument = null;
-        string? nameArgument = null;
-
-        void TestAction([FromForm] string message, [FromForm] string name)
-        {
-            messageArgument = message;
-            nameArgument = name;
-        }
-
-        var stream = new MemoryStream();
-        await content.CopyToAsync(stream);
-
-        stream.Seek(0, SeekOrigin.Begin);
-
-        var httpContext = CreateHttpContext();
-        httpContext.Request.Body = stream;
-        httpContext.Request.Headers["Content-Type"] = contentType;
-        httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
-
-        var factoryResult = RequestDelegateFactory.Create(TestAction);
-        var requestDelegate = factoryResult.RequestDelegate;
-
-        await requestDelegate(httpContext);
-
-        Assert.Equal(httpContext.Request.Form["message"], messageArgument);
-        Assert.NotNull(messageArgument);
-
-        Assert.Equal(httpContext.Request.Form["name"], nameArgument);
-        Assert.NotNull(nameArgument);
-    }
-
-    [Theory]
-    [MemberData(nameof(FormContent))]
-    public async Task RequestDelegatePopulatesFromOptionalMissingFormParameter(HttpContent content, string contentType)
-    {
-        string? messageArgument = null;
-        string? additionalMessageArgument = null;
-
-        void TestAction([FromForm] string? message, [FromForm] string? additionalMessage)
-        {
-            messageArgument = message;
-            additionalMessageArgument = additionalMessage;
-        }
-
-        var stream = new MemoryStream();
-        await content.CopyToAsync(stream);
-
-        stream.Seek(0, SeekOrigin.Begin);
-
-        var httpContext = CreateHttpContext();
-        httpContext.Request.Body = stream;
-        httpContext.Request.Headers["Content-Type"] = contentType;
-        httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
-
-        var factoryResult = RequestDelegateFactory.Create(TestAction);
-        var requestDelegate = factoryResult.RequestDelegate;
-
-        await requestDelegate(httpContext);
-
-        Assert.Equal(httpContext.Request.Form["message"], messageArgument);
-        Assert.NotNull(messageArgument);
-        Assert.Null(additionalMessageArgument);
-    }
-
-    [Theory]
-    [MemberData(nameof(FormContent))]
-    public async Task RequestDelegatePopulatesFromFormParameterWithMetadata(HttpContent content, string contentType)
-    {
-        string? textArgument = null;
-
-        void TestAction([FromForm(Name = "message")] string text)
-        {
-            textArgument = text;
-        }
-
-        var stream = new MemoryStream();
-        await content.CopyToAsync(stream);
-
-        stream.Seek(0, SeekOrigin.Begin);
-
-        var httpContext = CreateHttpContext();
-        httpContext.Request.Body = stream;
-        httpContext.Request.Headers["Content-Type"] = contentType;
-        httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
-
-        var factoryResult = RequestDelegateFactory.Create(TestAction);
-        var requestDelegate = factoryResult.RequestDelegate;
-
-        await requestDelegate(httpContext);
-
-        Assert.Equal(httpContext.Request.Form["message"], textArgument);
-        Assert.NotNull(textArgument);
-    }
-
-    [Theory]
-    [MemberData(nameof(FormContent))]
-    public async Task RequestDelegatePopulatesFromFormAndBoundParameter(HttpContent content, string contentType)
-    {
-        string? messageArgument = null;
-        TraceIdentifier traceIdArgument = default;
-
-        void TestAction([FromForm] string? message, TraceIdentifier traceId)
-        {
-            messageArgument = message;
-            traceIdArgument = traceId;
-        }
-
-        var stream = new MemoryStream();
-        await content.CopyToAsync(stream);
-
-        stream.Seek(0, SeekOrigin.Begin);
-
-        var httpContext = CreateHttpContext();
-        httpContext.Request.Body = stream;
-        httpContext.Request.Headers["Content-Type"] = contentType;
-        httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
-        httpContext.TraceIdentifier = "my-trace-id";
-
-        var factoryResult = RequestDelegateFactory.Create(TestAction);
-        var requestDelegate = factoryResult.RequestDelegate;
-
-        await requestDelegate(httpContext);
-
-        Assert.Equal(httpContext.Request.Form["message"], messageArgument);
-        Assert.NotNull(messageArgument);
-
-        Assert.Equal("my-trace-id", traceIdArgument.Id);
-    }
-
-    public static IEnumerable<object[]> FormAndFormFileParametersDelegates
-    {
-        get
-        {
-            void TestAction(HttpContext context, IFormCollection form, IFormFileCollection formFiles)
-            {
-                context.Items["FormFilesArgument"] = formFiles;
-                context.Items["FormArgument"] = form;
-            }
-
-            void TestActionDifferentOrder(HttpContext context, IFormFileCollection formFiles, IFormCollection form)
-            {
-                context.Items["FormFilesArgument"] = formFiles;
-                context.Items["FormArgument"] = form;
-            }
-
-            return new List<object[]>
-                {
-                    new object[] { (Action<HttpContext, IFormCollection, IFormFileCollection>)TestAction },
-                    new object[] { (Action<HttpContext, IFormFileCollection, IFormCollection>)TestActionDifferentOrder },
-                };
-        }
-    }
-
-    [Theory]
-    [MemberData(nameof(FormAndFormFileParametersDelegates))]
-    public async Task RequestDelegatePopulatesFromBothIFormCollectionAndIFormFileParameters(Delegate action)
-    {
-        var fileContent = new StringContent("hello", Encoding.UTF8, "application/octet-stream");
-        var form = new MultipartFormDataContent("some-boundary");
-        form.Add(fileContent, "file", "file.txt");
-        form.Add(new StringContent("foo"), "name");
-
-        var stream = new MemoryStream();
-        await form.CopyToAsync(stream);
-
-        stream.Seek(0, SeekOrigin.Begin);
-
-        var httpContext = CreateHttpContext();
-        httpContext.Request.Body = stream;
-        httpContext.Request.Headers["Content-Type"] = "multipart/form-data;boundary=some-boundary";
-        httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
-
-        var factoryResult = RequestDelegateFactory.Create(action);
-        var requestDelegate = factoryResult.RequestDelegate;
-
-        await requestDelegate(httpContext);
-
-        IFormFileCollection? formFilesArgument = httpContext.Items["FormFilesArgument"] as IFormFileCollection;
-        IFormCollection? formArgument = httpContext.Items["FormArgument"] as IFormCollection;
-
-        Assert.Equal(httpContext.Request.Form.Files, formFilesArgument);
-        Assert.NotNull(formFilesArgument!["file"]);
-        Assert.Equal("file.txt", formFilesArgument!["file"]!.FileName);
-
-        Assert.Equal(httpContext.Request.Form, formArgument);
-        Assert.NotNull(formArgument);
-        Assert.Collection(formArgument!,
-            (item) =>
-            {
-                Assert.Equal("name", item.Key);
-                Assert.Equal("foo", item.Value);
-            });
-
-        var allAcceptsMetadata = factoryResult.EndpointMetadata.OfType<IAcceptsMetadata>();
-        Assert.Collection(allAcceptsMetadata,
-            (m) => Assert.Equal(new[] { "multipart/form-data" }, m.ContentTypes));
-    }
-
-    [Theory]
-    [MemberData(nameof(FormContent))]
-    public async Task RequestDelegateSets400ResponseIfRequiredFormItemNotSpecified(HttpContent content, string contentType)
-    {
-        var invoked = false;
-
-        void TestAction([FromForm] string unknownParameter)
-        {
-            invoked = true;
-        }
-
-        var stream = new MemoryStream();
-        await content.CopyToAsync(stream);
-
-        stream.Seek(0, SeekOrigin.Begin);
-
-        var httpContext = CreateHttpContext();
-        httpContext.Request.Body = stream;
-        httpContext.Request.Headers["Content-Type"] = contentType;
-        httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
-
-        var factoryResult = RequestDelegateFactory.Create(TestAction);
-        var requestDelegate = factoryResult.RequestDelegate;
-
-        await requestDelegate(httpContext);
-
-        Assert.False(invoked);
-        Assert.Equal(400, httpContext.Response.StatusCode);
-    }
-
-    [Fact]
-    public async Task RequestDelegatePopulatesTryParsableParametersFromForm()
-    {
-        var httpContext = CreateHttpContext();
-
-        httpContext.Request.Form = new FormCollection(new Dictionary<string, StringValues>
-        {
-            ["tryParsable"] = "https://example.org"
-        });
-
-        var factoryResult = RequestDelegateFactory.Create((HttpContext httpContext, [FromForm] MyTryParseRecord tryParsable) =>
-        {
-            httpContext.Items["tryParsable"] = tryParsable;
-        });
-
-        var requestDelegate = factoryResult.RequestDelegate;
-
-        await requestDelegate(httpContext);
-
-        var content = Assert.IsType<MyTryParseRecord>(httpContext.Items["tryParsable"]);
-        Assert.Equal(new Uri("https://example.org"), content.Uri);
-    }
-
     private record struct ParameterListRecordStruct(HttpContext HttpContext, [FromRoute] int Value);
     private record struct ParameterListRecordStruct(HttpContext HttpContext, [FromRoute] int Value);
 
 
     private record ParameterListRecordClass(HttpContext HttpContext, [FromRoute] int Value);
     private record ParameterListRecordClass(HttpContext HttpContext, [FromRoute] int Value);

+ 490 - 0
src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/RequestDelegateValidateGeneratedFormCode.generated.txt

@@ -0,0 +1,490 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+//     This code was generated by a tool.
+//
+//     Changes to this file may cause incorrect behavior and will be lost if
+//     the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+#nullable enable
+
+namespace Microsoft.AspNetCore.Builder
+{
+    %GENERATEDCODEATTRIBUTE%
+    internal class SourceKey
+    {
+        public string Path { get; init; }
+        public int Line { get; init; }
+
+        public SourceKey(string path, int line)
+        {
+            Path = path;
+            Line = line;
+        }
+    }
+
+    // This class needs to be internal so that the compiled application
+    // has access to the strongly-typed endpoint definitions that are
+    // generated by the compiler so that they will be favored by
+    // overload resolution and opt the runtime in to the code generated
+    // implementation produced here.
+    %GENERATEDCODEATTRIBUTE%
+    internal static class GenerateRouteBuilderEndpoints
+    {
+        private static readonly string[] GetVerb = new[] { global::Microsoft.AspNetCore.Http.HttpMethods.Get };
+        private static readonly string[] PostVerb = new[] { global::Microsoft.AspNetCore.Http.HttpMethods.Post };
+        private static readonly string[] PutVerb = new[]  { global::Microsoft.AspNetCore.Http.HttpMethods.Put };
+        private static readonly string[] DeleteVerb = new[] { global::Microsoft.AspNetCore.Http.HttpMethods.Delete };
+        private static readonly string[] PatchVerb = new[] { global::Microsoft.AspNetCore.Http.HttpMethods.Patch };
+
+        internal static global::Microsoft.AspNetCore.Builder.RouteHandlerBuilder MapPost(
+            this global::Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints,
+            [global::System.Diagnostics.CodeAnalysis.StringSyntax("Route")] string pattern,
+            global::System.Action<global::Microsoft.AspNetCore.Http.HttpContext, global::Microsoft.AspNetCore.Http.IFormFile, global::Microsoft.AspNetCore.Http.IFormFileCollection, global::Microsoft.AspNetCore.Http.IFormCollection, global::Microsoft.AspNetCore.Http.Generators.Tests.MyTryParseRecord> handler,
+            [global::System.Runtime.CompilerServices.CallerFilePath] string filePath = "",
+            [global::System.Runtime.CompilerServices.CallerLineNumber]int lineNumber = 0)
+        {
+            return global::Microsoft.AspNetCore.Http.Generated.GeneratedRouteBuilderExtensionsCore.MapCore(
+                endpoints,
+                pattern,
+                handler,
+                PostVerb,
+                filePath,
+                lineNumber);
+        }
+
+    }
+}
+
+namespace Microsoft.AspNetCore.Http.Generated
+{
+    using System;
+    using System.Collections;
+    using System.Collections.Generic;
+    using System.Collections.ObjectModel;
+    using System.Diagnostics;
+    using System.Diagnostics.CodeAnalysis;
+    using System.Globalization;
+    using System.Linq;
+    using System.Reflection;
+    using System.Text.Json;
+    using System.Text.Json.Serialization.Metadata;
+    using System.Threading.Tasks;
+    using System.IO;
+    using Microsoft.AspNetCore.Routing;
+    using Microsoft.AspNetCore.Routing.Patterns;
+    using Microsoft.AspNetCore.Builder;
+    using Microsoft.AspNetCore.Http;
+    using Microsoft.AspNetCore.Http.Json;
+    using Microsoft.AspNetCore.Http.Metadata;
+    using Microsoft.Extensions.DependencyInjection;
+    using Microsoft.Extensions.FileProviders;
+    using Microsoft.Extensions.Logging;
+    using Microsoft.Extensions.Primitives;
+    using Microsoft.Extensions.Options;
+
+    using MetadataPopulator = System.Func<System.Reflection.MethodInfo, Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions?, Microsoft.AspNetCore.Http.RequestDelegateMetadataResult>;
+    using RequestDelegateFactoryFunc = System.Func<System.Delegate, Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions, Microsoft.AspNetCore.Http.RequestDelegateMetadataResult?, Microsoft.AspNetCore.Http.RequestDelegateResult>;
+
+    file static class GeneratedRouteBuilderExtensionsCore
+    {
+
+        private static readonly Dictionary<(string, int), (MetadataPopulator, RequestDelegateFactoryFunc)> map = new()
+        {
+            [(@"TestMapActions.cs", 29)] = (
+                (methodInfo, options) =>
+                {
+                    Debug.Assert(options?.EndpointBuilder != null, "EndpointBuilder not found.");
+                    options.EndpointBuilder.Metadata.Add(new SourceKey(@"TestMapActions.cs", 29));
+                    return new RequestDelegateMetadataResult { EndpointMetadata = options.EndpointBuilder.Metadata.AsReadOnly() };
+                },
+                (del, options, inferredMetadataResult) =>
+                {
+                    var handler = (System.Action<global::Microsoft.AspNetCore.Http.HttpContext, global::Microsoft.AspNetCore.Http.IFormFile, global::Microsoft.AspNetCore.Http.IFormFileCollection, global::Microsoft.AspNetCore.Http.IFormCollection, global::Microsoft.AspNetCore.Http.Generators.Tests.MyTryParseRecord>)del;
+                    EndpointFilterDelegate? filteredInvocation = null;
+                    var serviceProvider = options?.ServiceProvider ?? options?.EndpointBuilder?.ApplicationServices;
+                    var logOrThrowExceptionHelper = new LogOrThrowExceptionHelper(serviceProvider, options);
+
+                    if (options?.EndpointBuilder?.FilterFactories.Count > 0)
+                    {
+                        filteredInvocation = GeneratedRouteBuilderExtensionsCore.BuildFilterDelegate(ic =>
+                        {
+                            if (ic.HttpContext.Response.StatusCode == 400)
+                            {
+                                return ValueTask.FromResult<object?>(Results.Empty);
+                            }
+                            handler(ic.GetArgument<global::Microsoft.AspNetCore.Http.HttpContext>(0)!, ic.GetArgument<global::Microsoft.AspNetCore.Http.IFormFile>(1)!, ic.GetArgument<global::Microsoft.AspNetCore.Http.IFormFileCollection>(2)!, ic.GetArgument<global::Microsoft.AspNetCore.Http.IFormCollection>(3)!, ic.GetArgument<global::Microsoft.AspNetCore.Http.Generators.Tests.MyTryParseRecord>(4)!);
+                            return ValueTask.FromResult<object?>(Results.Empty);
+                        },
+                        options.EndpointBuilder,
+                        handler.Method);
+                    }
+
+                    async Task RequestHandler(HttpContext httpContext)
+                    {
+                        var wasParamCheckFailure = false;
+                        var httpContext_local = httpContext;
+                        // Endpoint Parameter: file (Type = Microsoft.AspNetCore.Http.IFormFile, IsOptional = False, IsParsable = False, IsArray = False, Source = FormBody)
+                        var file_resolveFormResult = await GeneratedRouteBuilderExtensionsCore.TryResolveFormAsync(httpContext, logOrThrowExceptionHelper, "IFormFile", "file");
+                        if (!file_resolveFormResult.Item1)
+                        {
+                            return;
+                        }
+                        var file_raw = httpContext.Request.Form.Files["file"];
+                        if (file_raw == null)
+                        {
+                            wasParamCheckFailure = true;
+                            logOrThrowExceptionHelper.RequiredParameterNotProvided("IFormFile", "file", "form");
+                        }
+                        var file_temp = file_raw;
+                        global::Microsoft.AspNetCore.Http.IFormFile file_local = file_temp!;
+                        // Endpoint Parameter: fileCollection (Type = Microsoft.AspNetCore.Http.IFormFileCollection, IsOptional = False, IsParsable = False, IsArray = False, Source = FormBody)
+                        var fileCollection_raw = httpContext.Request.Form.Files;
+                        if (fileCollection_raw == null)
+                        {
+                            wasParamCheckFailure = true;
+                            logOrThrowExceptionHelper.RequiredParameterNotProvided("IFormFileCollection", "fileCollection", "form");
+                        }
+                        var fileCollection_temp = fileCollection_raw;
+                        global::Microsoft.AspNetCore.Http.IFormFileCollection fileCollection_local = fileCollection_temp!;
+                        // Endpoint Parameter: collection (Type = Microsoft.AspNetCore.Http.IFormCollection, IsOptional = False, IsParsable = False, IsArray = False, Source = FormBody)
+                        var collection_raw = httpContext.Request.Form;
+                        if (collection_raw == null)
+                        {
+                            wasParamCheckFailure = true;
+                            logOrThrowExceptionHelper.RequiredParameterNotProvided("IFormCollection", "collection", "form");
+                        }
+                        var collection_temp = collection_raw;
+                        global::Microsoft.AspNetCore.Http.IFormCollection collection_local = collection_temp!;
+                        // Endpoint Parameter: tryParseRecord (Type = Microsoft.AspNetCore.Http.Generators.Tests.MyTryParseRecord, IsOptional = False, IsParsable = True, IsArray = False, Source = FormBody)
+                        var tryParseRecord_raw = (string?)httpContext.Request.Form["tryParseRecord"];
+                        if (tryParseRecord_raw == null)
+                        {
+                            wasParamCheckFailure = true;
+                            logOrThrowExceptionHelper.RequiredParameterNotProvided("MyTryParseRecord", "tryParseRecord", "form");
+                        }
+                        var tryParseRecord_temp = tryParseRecord_raw;
+                        if (!global::Microsoft.AspNetCore.Http.Generators.Tests.MyTryParseRecord.TryParse(tryParseRecord_temp!, out var tryParseRecord_parsed_temp))
+                        {
+                            if (!string.IsNullOrEmpty(tryParseRecord_temp))
+                            {
+                                logOrThrowExceptionHelper.ParameterBindingFailed("MyTryParseRecord", "tryParseRecord", tryParseRecord_temp);
+                                wasParamCheckFailure = true;
+                            }
+                        }
+                        global::Microsoft.AspNetCore.Http.Generators.Tests.MyTryParseRecord tryParseRecord_local = tryParseRecord_parsed_temp!;
+
+                        if (wasParamCheckFailure)
+                        {
+                            httpContext.Response.StatusCode = 400;
+                            return;
+                        }
+                        handler(httpContext_local, file_local!, fileCollection_local!, collection_local!, tryParseRecord_local!);
+                    }
+
+                    async Task RequestHandlerFiltered(HttpContext httpContext)
+                    {
+                        var wasParamCheckFailure = false;
+                        var httpContext_local = httpContext;
+                        // Endpoint Parameter: file (Type = Microsoft.AspNetCore.Http.IFormFile, IsOptional = False, IsParsable = False, IsArray = False, Source = FormBody)
+                        var file_resolveFormResult = await GeneratedRouteBuilderExtensionsCore.TryResolveFormAsync(httpContext, logOrThrowExceptionHelper, "IFormFile", "file");
+                        if (!file_resolveFormResult.Item1)
+                        {
+                            return;
+                        }
+                        var file_raw = httpContext.Request.Form.Files["file"];
+                        if (file_raw == null)
+                        {
+                            wasParamCheckFailure = true;
+                            logOrThrowExceptionHelper.RequiredParameterNotProvided("IFormFile", "file", "form");
+                        }
+                        var file_temp = file_raw;
+                        global::Microsoft.AspNetCore.Http.IFormFile file_local = file_temp!;
+                        // Endpoint Parameter: fileCollection (Type = Microsoft.AspNetCore.Http.IFormFileCollection, IsOptional = False, IsParsable = False, IsArray = False, Source = FormBody)
+                        var fileCollection_raw = httpContext.Request.Form.Files;
+                        if (fileCollection_raw == null)
+                        {
+                            wasParamCheckFailure = true;
+                            logOrThrowExceptionHelper.RequiredParameterNotProvided("IFormFileCollection", "fileCollection", "form");
+                        }
+                        var fileCollection_temp = fileCollection_raw;
+                        global::Microsoft.AspNetCore.Http.IFormFileCollection fileCollection_local = fileCollection_temp!;
+                        // Endpoint Parameter: collection (Type = Microsoft.AspNetCore.Http.IFormCollection, IsOptional = False, IsParsable = False, IsArray = False, Source = FormBody)
+                        var collection_raw = httpContext.Request.Form;
+                        if (collection_raw == null)
+                        {
+                            wasParamCheckFailure = true;
+                            logOrThrowExceptionHelper.RequiredParameterNotProvided("IFormCollection", "collection", "form");
+                        }
+                        var collection_temp = collection_raw;
+                        global::Microsoft.AspNetCore.Http.IFormCollection collection_local = collection_temp!;
+                        // Endpoint Parameter: tryParseRecord (Type = Microsoft.AspNetCore.Http.Generators.Tests.MyTryParseRecord, IsOptional = False, IsParsable = True, IsArray = False, Source = FormBody)
+                        var tryParseRecord_raw = (string?)httpContext.Request.Form["tryParseRecord"];
+                        if (tryParseRecord_raw == null)
+                        {
+                            wasParamCheckFailure = true;
+                            logOrThrowExceptionHelper.RequiredParameterNotProvided("MyTryParseRecord", "tryParseRecord", "form");
+                        }
+                        var tryParseRecord_temp = tryParseRecord_raw;
+                        if (!global::Microsoft.AspNetCore.Http.Generators.Tests.MyTryParseRecord.TryParse(tryParseRecord_temp!, out var tryParseRecord_parsed_temp))
+                        {
+                            if (!string.IsNullOrEmpty(tryParseRecord_temp))
+                            {
+                                logOrThrowExceptionHelper.ParameterBindingFailed("MyTryParseRecord", "tryParseRecord", tryParseRecord_temp);
+                                wasParamCheckFailure = true;
+                            }
+                        }
+                        global::Microsoft.AspNetCore.Http.Generators.Tests.MyTryParseRecord tryParseRecord_local = tryParseRecord_parsed_temp!;
+
+                        if (wasParamCheckFailure)
+                        {
+                            httpContext.Response.StatusCode = 400;
+                        }
+                        var result = await filteredInvocation(EndpointFilterInvocationContext.Create<global::Microsoft.AspNetCore.Http.HttpContext, global::Microsoft.AspNetCore.Http.IFormFile, global::Microsoft.AspNetCore.Http.IFormFileCollection, global::Microsoft.AspNetCore.Http.IFormCollection, global::Microsoft.AspNetCore.Http.Generators.Tests.MyTryParseRecord>(httpContext, httpContext_local, file_local!, fileCollection_local!, collection_local!, tryParseRecord_local!));
+                        await GeneratedRouteBuilderExtensionsCore.ExecuteObjectResult(result, httpContext);
+                    }
+
+                    RequestDelegate targetDelegate = filteredInvocation is null ? RequestHandler : RequestHandlerFiltered;
+                    var metadata = inferredMetadataResult?.EndpointMetadata ?? ReadOnlyCollection<object>.Empty;
+                    return new RequestDelegateResult(targetDelegate, metadata);
+                }),
+
+        };
+
+        internal static RouteHandlerBuilder MapCore(
+            this IEndpointRouteBuilder routes,
+            string pattern,
+            Delegate handler,
+            IEnumerable<string> httpMethods,
+            string filePath,
+            int lineNumber)
+        {
+            var (populateMetadata, createRequestDelegate) = map[(filePath, lineNumber)];
+            return RouteHandlerServices.Map(routes, pattern, handler, httpMethods, populateMetadata, createRequestDelegate);
+        }
+
+        private static EndpointFilterDelegate BuildFilterDelegate(EndpointFilterDelegate filteredInvocation, EndpointBuilder builder, MethodInfo mi)
+        {
+            var routeHandlerFilters =  builder.FilterFactories;
+            var context0 = new EndpointFilterFactoryContext
+            {
+                MethodInfo = mi,
+                ApplicationServices = builder.ApplicationServices,
+            };
+            var initialFilteredInvocation = filteredInvocation;
+            for (var i = routeHandlerFilters.Count - 1; i >= 0; i--)
+            {
+                var filterFactory = routeHandlerFilters[i];
+                filteredInvocation = filterFactory(context0, filteredInvocation);
+            }
+            return filteredInvocation;
+        }
+
+        private static Task ExecuteObjectResult(object? obj, HttpContext httpContext)
+        {
+            if (obj is IResult r)
+            {
+                return r.ExecuteAsync(httpContext);
+            }
+            else if (obj is string s)
+            {
+                return httpContext.Response.WriteAsync(s);
+            }
+            else
+            {
+                return httpContext.Response.WriteAsJsonAsync(obj);
+            }
+        }
+
+        private static async Task<(bool, object?)> TryResolveFormAsync(
+            HttpContext httpContext,
+            LogOrThrowExceptionHelper logOrThrowExceptionHelper,
+            string parameterTypeName,
+            string parameterName)
+        {
+            object? formValue = null;
+            var feature = httpContext.Features.Get<Microsoft.AspNetCore.Http.Features.IHttpRequestBodyDetectionFeature>();
+
+            if (feature?.CanHaveBody == true)
+            {
+                if (!httpContext.Request.HasFormContentType)
+                {
+                    logOrThrowExceptionHelper.UnexpectedNonFormContentType(httpContext.Request.ContentType);
+                    httpContext.Response.StatusCode = StatusCodes.Status415UnsupportedMediaType;
+                    return (false, null);
+                }
+
+                try
+                {
+                    formValue = await httpContext.Request.ReadFormAsync();
+                }
+                catch (BadHttpRequestException ex)
+                {
+                    logOrThrowExceptionHelper.RequestBodyIOException(ex);
+                    httpContext.Response.StatusCode = ex.StatusCode;
+                    return (false, null);
+                }
+                catch (IOException ex)
+                {
+                    logOrThrowExceptionHelper.RequestBodyIOException(ex);
+                    httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
+                    return (false, null);
+                }
+                catch (InvalidDataException ex)
+                {
+                    logOrThrowExceptionHelper.InvalidFormRequestBody(parameterTypeName, parameterName, ex);
+                    httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
+                    return (false, null);
+                }
+            }
+
+            return (true, formValue);
+        }
+        private static bool TryParseExplicit<T>(string? s, IFormatProvider? provider, [MaybeNullWhen(returnValue: false)] out T result) where T: IParsable<T>
+            => T.TryParse(s, provider, out result);
+
+    }
+
+    file class LogOrThrowExceptionHelper
+    {
+        private readonly ILogger? _rdgLogger;
+        private readonly bool _shouldThrow;
+
+        public LogOrThrowExceptionHelper(IServiceProvider? serviceProvider, RequestDelegateFactoryOptions? options)
+        {
+            var loggerFactory = serviceProvider?.GetRequiredService<ILoggerFactory>();
+            _rdgLogger = loggerFactory?.CreateLogger("Microsoft.AspNetCore.Http.RequestDelegateGenerator.RequestDelegateGenerator");
+            _shouldThrow = options?.ThrowOnBadRequest ?? false;
+        }
+
+        public void RequestBodyIOException(IOException exception)
+        {
+            if (_rdgLogger != null)
+            {
+                _requestBodyIOException(_rdgLogger, exception);
+            }
+        }
+
+        private static readonly Action<ILogger, Exception?> _requestBodyIOException =
+            LoggerMessage.Define(LogLevel.Debug, new EventId(1, "RequestBodyIOException"), "Reading the request body failed with an IOException.");
+
+        public void InvalidJsonRequestBody(string parameterTypeName, string parameterName, Exception exception)
+        {
+            if (_shouldThrow)
+            {
+                var message = string.Format(CultureInfo.InvariantCulture, "Failed to read parameter \"{0} {1}\" from the request body as JSON.", parameterTypeName, parameterName);
+                throw new BadHttpRequestException(message, exception);
+            }
+
+            if (_rdgLogger != null)
+            {
+                _invalidJsonRequestBody(_rdgLogger, parameterTypeName, parameterName, exception);
+            }
+        }
+
+        private static readonly Action<ILogger, string, string, Exception?> _invalidJsonRequestBody =
+            LoggerMessage.Define<string, string>(LogLevel.Debug, new EventId(2, "InvalidJsonRequestBody"), "Failed to read parameter \"{ParameterType} {ParameterName}\" from the request body as JSON.");
+
+        public void ParameterBindingFailed(string parameterTypeName, string parameterName, string sourceValue)
+        {
+            if (_shouldThrow)
+            {
+                var message = string.Format(CultureInfo.InvariantCulture, "Failed to bind parameter \"{0} {1}\" from \"{2}\".", parameterTypeName, parameterName, sourceValue);
+                throw new BadHttpRequestException(message);
+            }
+
+            if (_rdgLogger != null)
+            {
+                _parameterBindingFailed(_rdgLogger, parameterTypeName, parameterName, sourceValue, null);
+            }
+        }
+
+        private static readonly Action<ILogger, string, string, string, Exception?> _parameterBindingFailed =
+            LoggerMessage.Define<string, string, string>(LogLevel.Debug, new EventId(3, "ParameterBindingFailed"), "Failed to bind parameter \"{ParameterType} {ParameterName}\" from \"{SourceValue}\".");
+
+        public void RequiredParameterNotProvided(string parameterTypeName, string parameterName, string source)
+        {
+            if (_shouldThrow)
+            {
+                var message = string.Format(CultureInfo.InvariantCulture, "Required parameter \"{0} {1}\" was not provided from {2}.", parameterTypeName, parameterName, source);
+                throw new BadHttpRequestException(message);
+            }
+
+            if (_rdgLogger != null)
+            {
+                _requiredParameterNotProvided(_rdgLogger, parameterTypeName, parameterName, source, null);
+            }
+        }
+
+        private static readonly Action<ILogger, string, string, string, Exception?> _requiredParameterNotProvided =
+            LoggerMessage.Define<string, string, string>(LogLevel.Debug, new EventId(4, "RequiredParameterNotProvided"), "Required parameter \"{ParameterType} {ParameterName}\" was not provided from {Source}.");
+
+        public void ImplicitBodyNotProvided(string parameterName)
+        {
+            if (_shouldThrow)
+            {
+                var message = string.Format(CultureInfo.InvariantCulture, "Implicit body inferred for parameter \"{0}\" but no body was provided. Did you mean to use a Service instead?", parameterName);
+                throw new BadHttpRequestException(message);
+            }
+
+            if (_rdgLogger != null)
+            {
+                _implicitBodyNotProvided(_rdgLogger, parameterName, null);
+            }
+        }
+
+        private static readonly Action<ILogger, string, Exception?> _implicitBodyNotProvided =
+            LoggerMessage.Define<string>(LogLevel.Debug, new EventId(5, "ImplicitBodyNotProvided"), "Implicit body inferred for parameter \"{ParameterName}\" but no body was provided. Did you mean to use a Service instead?");
+
+        public void UnexpectedJsonContentType(string? contentType)
+        {
+            if (_shouldThrow)
+            {
+                var message = string.Format(CultureInfo.InvariantCulture, "Expected a supported JSON media type but got \"{0}\".", contentType);
+                throw new BadHttpRequestException(message, StatusCodes.Status415UnsupportedMediaType);
+            }
+
+            if (_rdgLogger != null)
+            {
+                _unexpectedJsonContentType(_rdgLogger, contentType ?? "(none)", null);
+            }
+        }
+
+        private static readonly Action<ILogger, string, Exception?> _unexpectedJsonContentType =
+            LoggerMessage.Define<string>(LogLevel.Debug, new EventId(6, "UnexpectedContentType"), "Expected a supported JSON media type but got \"{ContentType}\".");
+
+        public void UnexpectedNonFormContentType(string? contentType)
+        {
+            if (_shouldThrow)
+            {
+                var message = string.Format(CultureInfo.InvariantCulture, "Expected a supported form media type but got \"{0}\".", contentType);
+                throw new BadHttpRequestException(message, StatusCodes.Status415UnsupportedMediaType);
+            }
+
+            if (_rdgLogger != null)
+            {
+                _unexpectedNonFormContentType(_rdgLogger, contentType ?? "(none)", null);
+            }
+        }
+
+        private static readonly Action<ILogger, string, Exception?> _unexpectedNonFormContentType =
+            LoggerMessage.Define<string>(LogLevel.Debug, new EventId(7, "UnexpectedNonFormContentType"), "Expected a supported form media type but got \"{ContentType}\".");
+
+        public void InvalidFormRequestBody(string parameterTypeName, string parameterName, Exception exception)
+        {
+            if (_shouldThrow)
+            {
+                var message = string.Format(CultureInfo.InvariantCulture, "Failed to read parameter \"{0} {1}\" from the request body as form.", parameterTypeName, parameterName);
+                throw new BadHttpRequestException(message, exception);
+            }
+
+            if (_rdgLogger != null)
+            {
+                _invalidFormRequestBody(_rdgLogger, parameterTypeName, parameterName, exception);
+            }
+        }
+
+        private static readonly Action<ILogger, string, string, Exception?> _invalidFormRequestBody =
+            LoggerMessage.Define<string, string>(LogLevel.Debug, new EventId(8, "InvalidFormRequestBody"), "Failed to read parameter \"{ParameterType} {ParameterName}\" from the request body as form.");
+    }
+}

+ 0 - 25
src/Http/Http.Extensions/test/RequestDelegateGenerator/CompileTimeCreationTests.cs

@@ -58,31 +58,6 @@ app.MapGet("/hello", (HttpContext context) => Task.CompletedTask);
         Assert.Equal("'invalidName' is not a route parameter.", exception.Message);
         Assert.Equal("'invalidName' is not a route parameter.", exception.Message);
     }
     }
 
 
-    [Theory]
-    [InlineData(@"app.MapGet(""/"", (IFormFile? form) => ""Hello world!"");")]
-    [InlineData(@"app.MapGet(""/"", (IFormCollection? form) => ""Hello world!"");")]
-    [InlineData(@"app.MapGet(""/"", (IFormFileCollection? form) => ""Hello world!"");")]
-    [InlineData(@"app.MapGet(""/"", ([FromForm] TryParseTodo? form) => ""Hello world!"");")]
-    public async Task MapAction_WarnsForUnsupportedFormTypes(string source)
-    {
-        var (generatorRunResult, compilation) = await RunGeneratorAsync(source);
-
-        // Emits diagnostic but generates no source
-        var result = Assert.IsType<GeneratorRunResult>(generatorRunResult);
-        var diagnostic = Assert.Single(result.Diagnostics);
-        Assert.Equal(DiagnosticDescriptors.UnableToResolveParameterDescriptor.Id, diagnostic.Id);
-        Assert.Empty(result.GeneratedSources);
-
-        // Falls back to runtime-generated endpoint
-        var endpoint = GetEndpointFromCompilation(compilation, false);
-
-        var httpContext = CreateHttpContext();
-        httpContext.Request.Headers["Content-Type"] = "application/x-www-form-urlencoded";
-        httpContext.Request.Headers["Content-Length"] = "0";
-        await endpoint.RequestDelegate(httpContext);
-        await VerifyResponseBodyAsync(httpContext, "Hello world!");
-    }
-
     [Fact]
     [Fact]
     public async Task MapAction_WarnsForUnsupportedAsParametersAttribute()
     public async Task MapAction_WarnsForUnsupportedAsParametersAttribute()
     {
     {

+ 1047 - 0
src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.Forms.cs

@@ -0,0 +1,1047 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+using System.Net.Http;
+using System.Security.Cryptography.X509Certificates;
+using System.Text;
+using Castle.Core.Internal;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.AspNetCore.Http.Metadata;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Primitives;
+namespace Microsoft.AspNetCore.Http.Generators.Tests;
+
+public abstract partial class RequestDelegateCreationTests : RequestDelegateCreationTestBase
+{
+    [Fact]
+    public async Task RequestDelegatePopulatesFromIFormFileCollectionParameter()
+    {
+        var source = """app.MapPost("/", (IFormFileCollection formFiles, HttpContext httpContext) => httpContext.Items["formFiles"] = formFiles);""";
+        var (_, compilation) = await RunGeneratorAsync(source);
+        var endpoint = GetEndpointFromCompilation(compilation);
+
+        var fileContent = new StringContent("hello", Encoding.UTF8, "application/octet-stream");
+        var form = new MultipartFormDataContent("some-boundary");
+        form.Add(fileContent, "file", "file.txt");
+
+        var stream = new MemoryStream();
+        await form.CopyToAsync(stream);
+
+        stream.Seek(0, SeekOrigin.Begin);
+
+        var httpContext = CreateHttpContext();
+        httpContext.Request.Body = stream;
+        httpContext.Request.Headers["Content-Type"] = "multipart/form-data;boundary=some-boundary";
+        httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
+
+        await endpoint.RequestDelegate(httpContext);
+
+        Assert.Equal(httpContext.Request.Form.Files, httpContext.Items["formFiles"]);
+        var formFilesArgument = Assert.IsAssignableFrom<IFormFileCollection>(httpContext.Items["formFiles"]);
+        Assert.NotNull(formFilesArgument!["file"]);
+
+        if (!IsGeneratorEnabled)
+        {
+            var allAcceptsMetadata = endpoint.Metadata.OfType<IAcceptsMetadata>();
+            var acceptsMetadata = Assert.Single(allAcceptsMetadata);
+
+            Assert.NotNull(acceptsMetadata);
+            Assert.Equal(new[] { "multipart/form-data" }, acceptsMetadata.ContentTypes);
+        }
+    }
+
+    [Fact]
+    public async Task RequestDelegatePopulatesFromIFormFileCollectionParameterWithAttribute()
+    {
+        var source = """app.MapPost("/", ([FromForm] IFormFileCollection formFiles, HttpContext httpContext) => httpContext.Items["formFiles"] = formFiles);""";
+        var (_, compilation) = await RunGeneratorAsync(source);
+        var endpoint = GetEndpointFromCompilation(compilation);
+
+        var fileContent = new StringContent("hello", Encoding.UTF8, "application/octet-stream");
+        var form = new MultipartFormDataContent("some-boundary");
+        form.Add(fileContent, "file", "file.txt");
+
+        var stream = new MemoryStream();
+        await form.CopyToAsync(stream);
+
+        stream.Seek(0, SeekOrigin.Begin);
+
+        var httpContext = CreateHttpContext();
+        httpContext.Request.Body = stream;
+        httpContext.Request.Headers["Content-Type"] = "multipart/form-data;boundary=some-boundary";
+        httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
+
+        await endpoint.RequestDelegate(httpContext);
+
+        Assert.Equal(httpContext.Request.Form.Files, httpContext.Items["formFiles"]);
+        var formFiles = Assert.IsAssignableFrom<IFormFileCollection>(httpContext.Items["formFiles"]);
+        Assert.NotNull(formFiles["file"]);
+
+        if (!IsGeneratorEnabled)
+        {
+            var allAcceptsMetadata = endpoint.Metadata.OfType<IAcceptsMetadata>();
+            var acceptsMetadata = Assert.Single(allAcceptsMetadata);
+
+            Assert.NotNull(acceptsMetadata);
+            Assert.Equal(new[] { "multipart/form-data" }, acceptsMetadata.ContentTypes);
+        }
+    }
+
+    [Fact]
+    public async Task RequestDelegatePopulatesFromIFormFileParameter()
+    {
+        var source = """app.MapPost("/", (IFormFile file, HttpContext httpContext) => httpContext.Items["formFiles"] = file);""";
+        var (_, compilation) = await RunGeneratorAsync(source);
+        var endpoint = GetEndpointFromCompilation(compilation);
+
+        var fileContent = new StringContent("hello", Encoding.UTF8, "application/octet-stream");
+        var form = new MultipartFormDataContent("some-boundary");
+        form.Add(fileContent, "file", "file.txt");
+
+        var stream = new MemoryStream();
+        await form.CopyToAsync(stream);
+
+        stream.Seek(0, SeekOrigin.Begin);
+
+        var httpContext = CreateHttpContext();
+        httpContext.Request.Body = stream;
+        httpContext.Request.Headers["Content-Type"] = "multipart/form-data;boundary=some-boundary";
+        httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
+
+        await endpoint.RequestDelegate(httpContext);
+
+        Assert.Equal(httpContext.Request.Form.Files["file"], httpContext.Items["formFiles"]);
+        var fileArgument = Assert.IsAssignableFrom<IFormFile>(httpContext.Items["formFiles"]);
+        Assert.Equal("file.txt", fileArgument!.FileName);
+        Assert.Equal("file", fileArgument.Name);
+    }
+
+    [Fact]
+    public async Task RequestDelegatePopulatesFromOptionalIFormFileParameter()
+    {
+        var source = """
+app.MapPost("/", (IFormFile? file, HttpContext httpContext) =>
+{
+    if (file is not null)
+    {
+        httpContext.Items["formFiles"] = file;
+    }
+});
+""";
+        var (_, compilation) = await RunGeneratorAsync(source);
+        var endpoint = GetEndpointFromCompilation(compilation);
+
+        var fileContent = new StringContent("hello", Encoding.UTF8, "application/octet-stream");
+        var form = new MultipartFormDataContent("some-boundary");
+        form.Add(fileContent, "file", "file.txt");
+
+        var stream = new MemoryStream();
+        await form.CopyToAsync(stream);
+
+        stream.Seek(0, SeekOrigin.Begin);
+
+        var httpContext = CreateHttpContext();
+        httpContext.Request.Body = stream;
+        httpContext.Request.Headers["Content-Type"] = "multipart/form-data;boundary=some-boundary";
+        httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
+
+        await endpoint.RequestDelegate(httpContext);
+
+        Assert.Equal(httpContext.Request.Form.Files["file"], httpContext.Items["formFiles"]);
+        var fileArgument = Assert.IsAssignableFrom<IFormFile>(httpContext.Items["formFiles"]);
+        Assert.Equal("file.txt", fileArgument!.FileName);
+        Assert.Equal("file", fileArgument.Name);
+    }
+
+    [Fact]
+    public async Task RequestDelegatePopulatesFromMultipleRequiredIFormFileParameters()
+    {
+        var source = """
+app.MapPost("/", (IFormFile file1, IFormFile file2, HttpContext httpContext) =>
+{
+    httpContext.Items["file1"] = file1;
+    httpContext.Items["file2"] = file2;
+});
+""";
+        var (_, compilation) = await RunGeneratorAsync(source);
+        var endpoint = GetEndpointFromCompilation(compilation);
+
+        var fileContent1 = new StringContent("hello", Encoding.UTF8, "application/octet-stream");
+        var fileContent2 = new StringContent("there", Encoding.UTF8, "application/octet-stream");
+        var form = new MultipartFormDataContent("some-boundary");
+        form.Add(fileContent1, "file1", "file1.txt");
+        form.Add(fileContent2, "file2", "file2.txt");
+
+        var stream = new MemoryStream();
+        await form.CopyToAsync(stream);
+
+        stream.Seek(0, SeekOrigin.Begin);
+
+        var httpContext = CreateHttpContext();
+        httpContext.Request.Body = stream;
+        httpContext.Request.Headers["Content-Type"] = "multipart/form-data;boundary=some-boundary";
+        httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
+
+        await endpoint.RequestDelegate(httpContext);
+
+        Assert.Equal(httpContext.Request.Form.Files["file1"], httpContext.Items["file1"]);
+        var file1Argument = Assert.IsAssignableFrom<IFormFile>(httpContext.Items["file1"]);
+        Assert.Equal("file1.txt", file1Argument!.FileName);
+        Assert.Equal("file1", file1Argument.Name);
+
+        Assert.Equal(httpContext.Request.Form.Files["file2"], httpContext.Items["file2"]);
+        var file2Argument = Assert.IsAssignableFrom<IFormFile>(httpContext.Items["file2"]);
+        Assert.Equal("file2.txt", file2Argument!.FileName);
+        Assert.Equal("file2", file2Argument.Name);
+    }
+
+    [Fact]
+    public async Task RequestDelegatePopulatesFromOptionalMissingIFormFileParameter()
+    {
+        var source = """
+app.MapPost("/", (IFormFile? file1, IFormFile? file2, HttpContext httpContext) =>
+{
+    httpContext.Items["file1"] = file1;
+    httpContext.Items["file2"] = file2;
+});
+""";
+        var (_, compilation) = await RunGeneratorAsync(source);
+        var endpoint = GetEndpointFromCompilation(compilation);
+
+        var fileContent = new StringContent("hello", Encoding.UTF8, "application/octet-stream");
+        var form = new MultipartFormDataContent("some-boundary");
+        form.Add(fileContent, "file1", "file.txt");
+
+        var stream = new MemoryStream();
+        await form.CopyToAsync(stream);
+
+        stream.Seek(0, SeekOrigin.Begin);
+
+        var httpContext = CreateHttpContext();
+        httpContext.Request.Body = stream;
+        httpContext.Request.Headers["Content-Type"] = "multipart/form-data;boundary=some-boundary";
+        httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
+
+        await endpoint.RequestDelegate(httpContext);
+
+        Assert.Equal(httpContext.Request.Form.Files["file1"], httpContext.Items["file1"]);
+        Assert.NotNull(httpContext.Items["file1"]);
+
+        Assert.Equal(httpContext.Request.Form.Files["file2"], httpContext.Items["file2"]);
+        Assert.Null(httpContext.Items["file2"]);
+
+        if (!IsGeneratorEnabled)
+        {
+            var allAcceptsMetadata = endpoint.Metadata.OfType<IAcceptsMetadata>();
+            var acceptsMetadata = Assert.Single(allAcceptsMetadata);
+
+            Assert.NotNull(acceptsMetadata);
+            Assert.Equal(new[] { "multipart/form-data" }, acceptsMetadata.ContentTypes);
+        }
+    }
+
+    [Fact]
+    public async Task RequestDelegatePopulatesFromIFormFileParameterWithMetadata()
+    {
+        var source = """app.MapPost("/", ([FromForm(Name = "my_file")] IFormFile file, HttpContext httpContext) => httpContext.Items["formFiles"] = file);""";
+        var (_, compilation) = await RunGeneratorAsync(source);
+        var endpoint = GetEndpointFromCompilation(compilation);
+
+        var fileContent = new StringContent("hello", Encoding.UTF8, "application/octet-stream");
+        var form = new MultipartFormDataContent("some-boundary");
+        form.Add(fileContent, "my_file", "file.txt");
+
+        var stream = new MemoryStream();
+        await form.CopyToAsync(stream);
+
+        stream.Seek(0, SeekOrigin.Begin);
+
+        var httpContext = CreateHttpContext();
+        httpContext.Request.Body = stream;
+        httpContext.Request.Headers["Content-Type"] = "multipart/form-data;boundary=some-boundary";
+        httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
+
+        await endpoint.RequestDelegate(httpContext);
+
+        Assert.Equal(httpContext.Request.Form.Files["my_file"], httpContext.Items["formFiles"]);
+        var fileArgument = Assert.IsAssignableFrom<IFormFile>(httpContext.Items["formFiles"]);
+        Assert.Equal("file.txt", fileArgument!.FileName);
+        Assert.Equal("my_file", fileArgument.Name);
+    }
+
+    [Fact]
+    public async Task RequestDelegatePopulatesFromIFormFileAndBoundParameter()
+    {
+        var source = """
+app.MapPost("/", (IFormFile? file, TraceIdentifier traceId, HttpContext httpContext) =>
+{
+    httpContext.Items["formFiles"] = file;
+    httpContext.Items["traceId"] = traceId;
+});
+""";
+        var (_, compilation) = await RunGeneratorAsync(source);
+        var endpoint = GetEndpointFromCompilation(compilation);
+
+        var fileContent = new StringContent("hello", Encoding.UTF8, "application/octet-stream");
+        var form = new MultipartFormDataContent("some-boundary");
+        form.Add(fileContent, "file", "file.txt");
+
+        var stream = new MemoryStream();
+        await form.CopyToAsync(stream);
+
+        stream.Seek(0, SeekOrigin.Begin);
+
+        var httpContext = CreateHttpContext();
+        httpContext.Request.Body = stream;
+        httpContext.Request.Headers["Content-Type"] = "multipart/form-data;boundary=some-boundary";
+        httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
+        httpContext.TraceIdentifier = "my-trace-id";
+
+        await endpoint.RequestDelegate(httpContext);
+
+        Assert.Equal(httpContext.Request.Form.Files["file"], httpContext.Items["formFiles"]);
+        var fileArgument = Assert.IsAssignableFrom<IFormFile>(httpContext.Items["formFiles"]);
+        Assert.Equal("file.txt", fileArgument!.FileName);
+        Assert.Equal("file", fileArgument.Name);
+
+        var traceIdArgument = Assert.IsType<TraceIdentifier>(httpContext.Items["traceId"]);
+        Assert.Equal("my-trace-id", traceIdArgument.Id);
+    }
+
+    [Theory]
+    [InlineData(true)]
+    [InlineData(false)]
+    public async Task RequestDelegateRejectsNonFormContent(bool shouldThrow)
+    {
+        var source = """app.MapPost("/", (IFormFile file, HttpContext httpContext) => httpContext.Items["formFiles"] = file);""";
+        var (_, compilation) = await RunGeneratorAsync(source);
+        var endpoint = GetEndpointFromCompilation(compilation, serviceProvider: CreateServiceProvider());
+
+        var httpContext = CreateHttpContext();
+        httpContext.Request.Headers["Content-Type"] = "application/xml";
+        httpContext.Request.Headers["Content-Length"] = "1";
+        httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
+
+        var factoryResult = RequestDelegateFactory.Create((HttpContext context, IFormFile file) =>
+        {
+        }, new RequestDelegateFactoryOptions() { ThrowOnBadRequest = shouldThrow });
+        var requestDelegate = factoryResult.RequestDelegate;
+
+        var request = requestDelegate(httpContext);
+
+        if (shouldThrow)
+        {
+            var ex = await Assert.ThrowsAsync<BadHttpRequestException>(() => request);
+            Assert.Equal("Expected a supported form media type but got \"application/xml\".", ex.Message);
+            Assert.Equal(StatusCodes.Status415UnsupportedMediaType, ex.StatusCode);
+        }
+        else
+        {
+            await request;
+
+            Assert.Equal(415, httpContext.Response.StatusCode);
+            var logMessage = Assert.Single(TestSink.Writes);
+            Assert.Equal(new EventId(7, "UnexpectedContentType"), logMessage.EventId);
+            Assert.Equal(LogLevel.Debug, logMessage.LogLevel);
+            Assert.Equal("Expected a supported form media type but got \"application/xml\".", logMessage.Message);
+        }
+    }
+
+    [Fact]
+    public async Task RequestDelegateSets400ResponseIfRequiredFileNotSpecified()
+    {
+        var source = """app.MapPost("/", (IFormFile file, HttpContext httpContext) => httpContext.Items["invoked"] = true);""";
+        var (_, compilation) = await RunGeneratorAsync(source);
+        var endpoint = GetEndpointFromCompilation(compilation);
+
+        var fileContent = new StringContent("hello", Encoding.UTF8, "application/octet-stream");
+        var form = new MultipartFormDataContent("some-boundary");
+        form.Add(fileContent, "some-other-file", "file.txt");
+
+        var stream = new MemoryStream();
+        await form.CopyToAsync(stream);
+
+        stream.Seek(0, SeekOrigin.Begin);
+
+        var httpContext = CreateHttpContext();
+        httpContext.Items["invoked"] = false;
+        httpContext.Request.Body = stream;
+        httpContext.Request.Headers["Content-Type"] = "multipart/form-data;boundary=some-boundary";
+        httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
+
+        await endpoint.RequestDelegate(httpContext);
+
+        Assert.False((bool)httpContext.Items["invoked"]);
+        Assert.Equal(400, httpContext.Response.StatusCode);
+    }
+
+    [Fact]
+    public async Task RequestDelegatePopulatesFromBothFormFileCollectionAndFormFileParameters()
+    {
+        var source = """
+app.MapPost("/", (IFormFile file, IFormFileCollection formFiles, HttpContext httpContext) =>
+{
+    httpContext.Items["file"] = file;
+    httpContext.Items["formFiles"] = formFiles;
+});
+""";
+        var (_, compilation) = await RunGeneratorAsync(source);
+        var endpoint = GetEndpointFromCompilation(compilation);
+
+        var fileContent = new StringContent("hello", Encoding.UTF8, "application/octet-stream");
+        var form = new MultipartFormDataContent("some-boundary");
+        form.Add(fileContent, "file", "file.txt");
+
+        var stream = new MemoryStream();
+        await form.CopyToAsync(stream);
+
+        stream.Seek(0, SeekOrigin.Begin);
+
+        var httpContext = CreateHttpContext();
+        httpContext.Request.Body = stream;
+        httpContext.Request.Headers["Content-Type"] = "multipart/form-data;boundary=some-boundary";
+        httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
+
+        await endpoint.RequestDelegate(httpContext);
+
+        Assert.Equal(httpContext.Request.Form.Files, httpContext.Items["formFiles"]);
+        var formFilesArgument = Assert.IsType<FormFileCollection>(httpContext.Items["formFiles"]);
+        Assert.NotNull(formFilesArgument!["file"]);
+
+        Assert.Equal(httpContext.Request.Form.Files["file"], httpContext.Items["file"]);
+        var fileArgument = Assert.IsAssignableFrom<IFormFile>(httpContext.Items["file"]);
+        Assert.Equal("file.txt", fileArgument!.FileName);
+        Assert.Equal("file", fileArgument.Name);
+
+        if (!IsGeneratorEnabled)
+        {
+            var allAcceptsMetadata = endpoint.Metadata.OfType<IAcceptsMetadata>();
+            var acceptsMetadata = Assert.Single(allAcceptsMetadata);
+
+            Assert.NotNull(acceptsMetadata);
+            Assert.Equal(new[] { "multipart/form-data" }, acceptsMetadata.ContentTypes);
+        }
+    }
+
+    [Theory]
+    [InlineData("Authorization", "bearer my-token")]
+    [InlineData("Cookie", ".AspNetCore.Auth=abc123")]
+    public async Task RequestDelegatePopulatesFromIFormFileParameterIfRequestContainsSecureHeader(
+        string headerName,
+        string headerValue)
+    {
+        var source = """
+app.MapPost("/", (IFormFile? file, TraceIdentifier traceId, HttpContext httpContext) =>
+{
+    httpContext.Items["file"] = file;
+    httpContext.Items["traceId"] = traceId;
+});
+""";
+        var (_, compilation) = await RunGeneratorAsync(source);
+        var endpoint = GetEndpointFromCompilation(compilation);
+
+        var fileContent = new StringContent("hello", Encoding.UTF8, "application/octet-stream");
+        var form = new MultipartFormDataContent("some-boundary");
+        form.Add(fileContent, "file", "file.txt");
+
+        var stream = new MemoryStream();
+        await form.CopyToAsync(stream);
+
+        stream.Seek(0, SeekOrigin.Begin);
+
+        var httpContext = CreateHttpContext();
+        httpContext.Request.Body = stream;
+        httpContext.Request.Headers[headerName] = headerValue;
+        httpContext.Request.Headers["Content-Type"] = "multipart/form-data;boundary=some-boundary";
+        httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
+        httpContext.TraceIdentifier = "my-trace-id";
+
+        await endpoint.RequestDelegate(httpContext);
+
+        Assert.Equal(httpContext.Request.Form.Files["file"], httpContext.Items["file"]);
+        var fileArgument = Assert.IsAssignableFrom<IFormFile>(httpContext.Items["file"]);
+        Assert.Equal("file.txt", fileArgument!.FileName);
+        Assert.Equal("file", fileArgument.Name);
+
+        var traceIdArgument = Assert.IsAssignableFrom<TraceIdentifier>(httpContext.Items["traceId"]);
+        Assert.Equal("my-trace-id", traceIdArgument.Id);
+    }
+
+    [Fact]
+    public async Task RequestDelegatePopulatesFromIFormFileParameterIfRequestHasClientCertificate()
+    {
+        var source = """
+app.MapPost("/", (IFormFile? file, TraceIdentifier traceId, HttpContext httpContext) =>
+{
+    httpContext.Items["file"] = file;
+    httpContext.Items["traceId"] = traceId;
+});
+""";
+        var (_, compilation) = await RunGeneratorAsync(source);
+        var endpoint = GetEndpointFromCompilation(compilation);
+
+        var fileContent = new StringContent("hello", Encoding.UTF8, "application/octet-stream");
+        var form = new MultipartFormDataContent("some-boundary");
+        form.Add(fileContent, "file", "file.txt");
+
+        var stream = new MemoryStream();
+        await form.CopyToAsync(stream);
+
+        stream.Seek(0, SeekOrigin.Begin);
+
+        var httpContext = CreateHttpContext();
+        httpContext.Request.Body = stream;
+        httpContext.Request.Headers["Content-Type"] = "multipart/form-data;boundary=some-boundary";
+        httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
+        httpContext.TraceIdentifier = "my-trace-id";
+
+#pragma warning disable SYSLIB0026 // Type or member is obsolete
+        var clientCertificate = new X509Certificate2();
+#pragma warning restore SYSLIB0026 // Type or member is obsolete
+
+        httpContext.Features.Set<ITlsConnectionFeature>(new TlsConnectionFeature(clientCertificate));
+
+        await endpoint.RequestDelegate(httpContext);
+
+        Assert.Equal(httpContext.Request.Form.Files["file"], httpContext.Items["file"]);
+        var fileArgument = Assert.IsAssignableFrom<IFormFile>(httpContext.Items["file"]);
+        Assert.Equal("file.txt", fileArgument!.FileName);
+        Assert.Equal("file", fileArgument.Name);
+
+        var traceIdArgument = Assert.IsAssignableFrom<TraceIdentifier>(httpContext.Items["traceId"]);
+        Assert.Equal("my-trace-id", traceIdArgument.Id);
+    }
+
+    public static TheoryData<HttpContent, string> FormContent
+    {
+        get
+        {
+            var dataset = new TheoryData<HttpContent, string>();
+
+            var multipartFormData = new MultipartFormDataContent("some-boundary");
+            multipartFormData.Add(new StringContent("hello"), "message");
+            multipartFormData.Add(new StringContent("foo"), "name");
+            dataset.Add(multipartFormData, "multipart/form-data;boundary=some-boundary");
+
+            var urlEncondedForm = new FormUrlEncodedContent(new Dictionary<string, string> { ["message"] = "hello", ["name"] = "foo" });
+            dataset.Add(urlEncondedForm, "application/x-www-form-urlencoded");
+
+            return dataset;
+        }
+    }
+
+    [Theory]
+    [MemberData(nameof(FormContent))]
+    public async Task RequestDelegatePopulatesFromIFormCollectionParameter(HttpContent content, string contentType)
+    {
+        var source = """
+app.MapPost("/", (IFormCollection formFiles, HttpContext httpContext) =>
+{
+    httpContext.Items["formFiles"] = formFiles;
+});
+""";
+        var (_, compilation) = await RunGeneratorAsync(source);
+        var endpoint = GetEndpointFromCompilation(compilation);
+
+        var stream = new MemoryStream();
+        await content.CopyToAsync(stream);
+
+        stream.Seek(0, SeekOrigin.Begin);
+
+        var httpContext = CreateHttpContext();
+        httpContext.Request.Body = stream;
+        httpContext.Request.Headers["Content-Type"] = contentType;
+        httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
+
+        await endpoint.RequestDelegate(httpContext);
+
+        Assert.Equal(httpContext.Request.Form, httpContext.Items["formFiles"]);
+        var formArgument = Assert.IsAssignableFrom<IFormCollection>(httpContext.Items["formFiles"]);
+        Assert.NotNull(formArgument);
+        Assert.Collection(formArgument!,
+            (item) =>
+            {
+                Assert.Equal("message", item.Key);
+                Assert.Equal("hello", item.Value);
+            },
+            (item) =>
+            {
+                Assert.Equal("name", item.Key);
+                Assert.Equal("foo", item.Value);
+            });
+
+        if (!IsGeneratorEnabled)
+        {
+            var allAcceptsMetadata = endpoint.Metadata.OfType<IAcceptsMetadata>();
+            var acceptsMetadata = Assert.Single(allAcceptsMetadata);
+
+            Assert.NotNull(acceptsMetadata);
+            Assert.Equal(new[] { "multipart/form-data", "application/x-www-form-urlencoded" }, acceptsMetadata.ContentTypes);
+        }
+    }
+
+    [Theory]
+    [MemberData(nameof(FormContent))]
+    public async Task RequestDelegatePopulatesFromIFormCollectionParameterWithAttribute(HttpContent content, string contentType)
+    {
+        var source = """
+app.MapPost("/", ([FromForm] IFormCollection formFiles, HttpContext httpContext) =>
+{
+    httpContext.Items["formFiles"] = formFiles;
+});
+""";
+        var (_, compilation) = await RunGeneratorAsync(source);
+        var endpoint = GetEndpointFromCompilation(compilation);
+
+        var stream = new MemoryStream();
+        await content.CopyToAsync(stream);
+
+        stream.Seek(0, SeekOrigin.Begin);
+
+        var httpContext = CreateHttpContext();
+        httpContext.Request.Body = stream;
+        httpContext.Request.Headers["Content-Type"] = contentType;
+        httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
+
+        await endpoint.RequestDelegate(httpContext);
+
+        Assert.Equal(httpContext.Request.Form, httpContext.Items["formFiles"]);
+        var formArgument = Assert.IsAssignableFrom<IFormCollection>(httpContext.Items["formFiles"]);
+        Assert.NotNull(formArgument);
+        Assert.Collection(formArgument!,
+            (item) =>
+            {
+                Assert.Equal("message", item.Key);
+                Assert.Equal("hello", item.Value);
+            },
+            (item) =>
+            {
+                Assert.Equal("name", item.Key);
+                Assert.Equal("foo", item.Value);
+            });
+
+        if (!IsGeneratorEnabled)
+        {
+            var allAcceptsMetadata = endpoint.Metadata.OfType<IAcceptsMetadata>();
+            var acceptsMetadata = Assert.Single(allAcceptsMetadata);
+
+            Assert.NotNull(acceptsMetadata);
+            Assert.Equal(new[] { "multipart/form-data", "application/x-www-form-urlencoded" }, acceptsMetadata.ContentTypes);
+        }
+    }
+
+    [Theory]
+    [MemberData(nameof(FormContent))]
+    public async Task RequestDelegatePopulatesFromOptionalFormParameter(HttpContent content, string contentType)
+    {
+        var source = """
+app.MapPost("/", ([FromForm] string? message, HttpContext httpContext) =>
+{
+    httpContext.Items["message"] = message;
+});
+""";
+        var (_, compilation) = await RunGeneratorAsync(source);
+        var endpoint = GetEndpointFromCompilation(compilation);
+
+        var stream = new MemoryStream();
+        await content.CopyToAsync(stream);
+
+        stream.Seek(0, SeekOrigin.Begin);
+
+        var httpContext = CreateHttpContext();
+        httpContext.Request.Body = stream;
+        httpContext.Request.Headers["Content-Type"] = contentType;
+        httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
+
+        await endpoint.RequestDelegate(httpContext);
+
+        Assert.Equal(httpContext.Request.Form["message"][0], httpContext.Items["message"]);
+    }
+
+    [Theory]
+    [MemberData(nameof(FormContent))]
+    public async Task RequestDelegatePopulatesFromMultipleRequiredFormParameters(HttpContent content, string contentType)
+    {
+        var source = """
+app.MapPost("/", ([FromForm] string message, [FromForm] string name, HttpContext httpContext) =>
+{
+    httpContext.Items["message"] = message;
+    httpContext.Items["name"] = name;
+});
+""";
+        var (_, compilation) = await RunGeneratorAsync(source);
+        var endpoint = GetEndpointFromCompilation(compilation);
+
+        var stream = new MemoryStream();
+        await content.CopyToAsync(stream);
+
+        stream.Seek(0, SeekOrigin.Begin);
+
+        var httpContext = CreateHttpContext();
+        httpContext.Request.Body = stream;
+        httpContext.Request.Headers["Content-Type"] = contentType;
+        httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
+
+        await endpoint.RequestDelegate(httpContext);
+
+        Assert.Equal(httpContext.Request.Form["message"][0], httpContext.Items["message"]);
+        Assert.NotNull(httpContext.Items["message"]);
+
+        Assert.Equal(httpContext.Request.Form["name"][0], httpContext.Items["name"]);
+        Assert.NotNull(httpContext.Items["name"]);
+    }
+
+    [Theory]
+    [MemberData(nameof(FormContent))]
+    public async Task RequestDelegatePopulatesFromOptionalMissingFormParameter(HttpContent content, string contentType)
+    {
+        var source = """
+app.MapPost("/", ([FromForm] string? message, [FromForm] string? additionalMessage, HttpContext httpContext) =>
+{
+    httpContext.Items["message"] = message;
+    httpContext.Items["additionalMessage"] = additionalMessage;
+});
+""";
+        var (_, compilation) = await RunGeneratorAsync(source);
+        var endpoint = GetEndpointFromCompilation(compilation);
+
+        var stream = new MemoryStream();
+        await content.CopyToAsync(stream);
+
+        stream.Seek(0, SeekOrigin.Begin);
+
+        var httpContext = CreateHttpContext();
+        httpContext.Request.Body = stream;
+        httpContext.Request.Headers["Content-Type"] = contentType;
+        httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
+
+        await endpoint.RequestDelegate(httpContext);
+
+        Assert.Equal(httpContext.Request.Form["message"][0], httpContext.Items["message"]);
+        Assert.NotNull(httpContext.Items["message"]);
+        Assert.Null(httpContext.Items["additionalMessage"]);
+    }
+
+    [Theory]
+    [MemberData(nameof(FormContent))]
+    public async Task RequestDelegatePopulatesFromFormParameterWithMetadata(HttpContent content, string contentType)
+    {
+        var source = """
+app.MapPost("/", ([FromForm(Name = "message")] string text, HttpContext httpContext) =>
+{
+    httpContext.Items["message"] = text;
+});
+""";
+        var (_, compilation) = await RunGeneratorAsync(source);
+        var endpoint = GetEndpointFromCompilation(compilation);
+
+        var stream = new MemoryStream();
+        await content.CopyToAsync(stream);
+
+        stream.Seek(0, SeekOrigin.Begin);
+
+        var httpContext = CreateHttpContext();
+        httpContext.Request.Body = stream;
+        httpContext.Request.Headers["Content-Type"] = contentType;
+        httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
+
+        await endpoint.RequestDelegate(httpContext);
+
+        Assert.Equal(httpContext.Request.Form["message"][0], httpContext.Items["message"]);
+        Assert.NotNull(httpContext.Items["message"]);
+    }
+
+    [Theory]
+    [MemberData(nameof(FormContent))]
+    public async Task RequestDelegatePopulatesFromFormAndBoundParameter(HttpContent content, string contentType)
+    {
+        var source = """
+app.MapPost("/", ([FromForm] string? message, TraceIdentifier traceId, HttpContext httpContext) =>
+{
+    httpContext.Items["message"] = message;
+    httpContext.Items["traceId"] = traceId;
+});
+""";
+        var (_, compilation) = await RunGeneratorAsync(source);
+        var endpoint = GetEndpointFromCompilation(compilation);
+
+        var stream = new MemoryStream();
+        await content.CopyToAsync(stream);
+
+        stream.Seek(0, SeekOrigin.Begin);
+
+        var httpContext = CreateHttpContext();
+        httpContext.Request.Body = stream;
+        httpContext.Request.Headers["Content-Type"] = contentType;
+        httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
+        httpContext.TraceIdentifier = "my-trace-id";
+        await endpoint.RequestDelegate(httpContext);
+
+        Assert.Equal(httpContext.Request.Form["message"][0], httpContext.Items["message"]);
+        Assert.NotNull(httpContext.Items["message"]);
+
+        var traceIdArgument = Assert.IsType<TraceIdentifier>(httpContext.Items["traceId"]);
+        Assert.Equal("my-trace-id", traceIdArgument.Id);
+    }
+
+    public static IEnumerable<object[]> FormAndFormFileParametersDelegates
+    {
+        get
+        {
+            var source = """
+void TestAction(HttpContext context, IFormCollection form, IFormFileCollection formFiles)
+{
+    context.Items["FormFilesArgument"] = formFiles;
+    context.Items["FormArgument"] = form;
+}
+""";
+
+            var sourceDifferentOrder = """
+void TestAction(HttpContext context, IFormFileCollection formFiles, IFormCollection form)
+{
+    context.Items["FormFilesArgument"] = formFiles;
+    context.Items["FormArgument"] = form;
+}
+""";
+
+            return new List<object[]>
+            {
+                new object[] { source },
+                new object[] { sourceDifferentOrder },
+            };
+        }
+    }
+
+    [Theory]
+    [MemberData(nameof(FormAndFormFileParametersDelegates))]
+    public async Task RequestDelegatePopulatesFromBothIFormCollectionAndIFormFileParameters(string innerSource)
+    {
+        var source = $"""
+{innerSource}
+app.MapPost("/", TestAction);
+""";
+        var (_, compilation) = await RunGeneratorAsync(source);
+        var endpoint = GetEndpointFromCompilation(compilation);
+
+        var fileContent = new StringContent("hello", Encoding.UTF8, "application/octet-stream");
+        var form = new MultipartFormDataContent("some-boundary");
+        form.Add(fileContent, "file", "file.txt");
+        form.Add(new StringContent("foo"), "name");
+
+        var stream = new MemoryStream();
+        await form.CopyToAsync(stream);
+
+        stream.Seek(0, SeekOrigin.Begin);
+
+        var httpContext = CreateHttpContext();
+        httpContext.Request.Body = stream;
+        httpContext.Request.Headers["Content-Type"] = "multipart/form-data;boundary=some-boundary";
+        httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
+
+        await endpoint.RequestDelegate(httpContext);
+
+        var formFilesArgument = Assert.IsAssignableFrom<FormFileCollection>(httpContext.Items["FormFilesArgument"]);
+        var formArgument = Assert.IsAssignableFrom<IFormCollection>(httpContext.Items["FormArgument"]);
+
+        Assert.Equal(httpContext.Request.Form.Files, formFilesArgument);
+        Assert.NotNull(formFilesArgument!["file"]);
+        Assert.Equal("file.txt", formFilesArgument!["file"]!.FileName);
+
+        Assert.Equal(httpContext.Request.Form, formArgument);
+        Assert.NotNull(formArgument);
+        Assert.Collection(formArgument!,
+            (item) =>
+            {
+                Assert.Equal("name", item.Key);
+                Assert.Equal("foo", item.Value);
+            });
+
+        if (!IsGeneratorEnabled)
+        {
+            var allAcceptsMetadata = endpoint.Metadata.OfType<IAcceptsMetadata>();
+            Assert.Collection(allAcceptsMetadata,
+                (m) => Assert.Equal(new[] { "multipart/form-data" }, m.ContentTypes));
+        }
+    }
+
+    [Theory]
+    [MemberData(nameof(FormContent))]
+    public async Task RequestDelegateSets400ResponseIfRequiredFormItemNotSpecified(HttpContent content, string contentType)
+    {
+        var source = """
+app.MapPost("/", ([FromForm] string unknownParameter, HttpContext httpContext) => httpContext.Items["invoked"] = true);
+""";
+        var (_, compilation) = await RunGeneratorAsync(source);
+        var endpoint = GetEndpointFromCompilation(compilation);
+
+        var stream = new MemoryStream();
+        await content.CopyToAsync(stream);
+
+        stream.Seek(0, SeekOrigin.Begin);
+
+        var httpContext = CreateHttpContext();
+        httpContext.Items["invoked"] = false;
+        httpContext.Request.Body = stream;
+        httpContext.Request.Headers["Content-Type"] = contentType;
+        httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
+
+        await endpoint.RequestDelegate(httpContext);
+
+        Assert.False((bool)httpContext.Items["invoked"]);
+        Assert.Equal(400, httpContext.Response.StatusCode);
+    }
+
+    [Fact]
+    public async Task RequestDelegatePopulatesTryParsableParametersFromForm()
+    {
+        var source = """
+app.MapPost("/", (HttpContext httpContext, [FromForm] MyTryParseRecord tryParsable) =>
+{
+    httpContext.Items["tryParsable"] = tryParsable;
+});
+""";
+        var (_, compilation) = await RunGeneratorAsync(source);
+        var endpoint = GetEndpointFromCompilation(compilation);
+
+        var httpContext = CreateHttpContext();
+
+        httpContext.Request.Form = new FormCollection(new Dictionary<string, StringValues>
+        {
+            ["tryParsable"] = "https://example.org"
+        });
+
+        await endpoint.RequestDelegate(httpContext);
+
+        var content = Assert.IsType<MyTryParseRecord>(httpContext.Items["tryParsable"]);
+        Assert.Equal(new Uri("https://example.org"), content.Uri);
+    }
+
+    [Theory]
+    [InlineData(true)]
+    [InlineData(false)]
+    public async Task RequestDelegateLogsIOExceptionsForFormAsDebugDoesNotAbortAndNeverThrows(bool throwOnBadRequests)
+    {
+        var source = """
+void TestAction(HttpContext httpContext, IFormFile file)
+{
+    httpContext.Items["invoked"] = true;
+}
+app.MapPost("/", TestAction);
+""";
+        var (_, compilation) = await RunGeneratorAsync(source);
+        var serviceProvider = CreateServiceProvider(serviceCollection =>
+        {
+            serviceCollection.Configure<RouteHandlerOptions>(options => options.ThrowOnBadRequest = throwOnBadRequests);
+        });
+        var endpoint = GetEndpointFromCompilation(compilation, serviceProvider: serviceProvider);
+
+        var ioException = new IOException();
+
+        var httpContext = CreateHttpContext();
+        httpContext.Request.Headers["Content-Type"] = "application/x-www-form-urlencoded";
+        httpContext.Request.Headers["Content-Length"] = "1";
+        httpContext.Request.Body = new ExceptionThrowingRequestBodyStream(ioException);
+        httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
+
+        await endpoint.RequestDelegate(httpContext);
+
+        Assert.Null(httpContext.Items["invoked"]);
+        Assert.False(httpContext.RequestAborted.IsCancellationRequested);
+
+        var logMessage = Assert.Single(TestSink.Writes);
+        Assert.Equal(new EventId(1, "RequestBodyIOException"), logMessage.EventId);
+        Assert.Equal(LogLevel.Debug, logMessage.LogLevel);
+        Assert.Equal("Reading the request body failed with an IOException.", logMessage.Message);
+        Assert.Same(ioException, logMessage.Exception);
+    }
+
+    [Fact]
+    public async Task RequestDelegateLogsMalformedFormAsDebugAndSets400Response()
+    {
+        var source = """
+void TestAction(HttpContext httpContext, IFormFile file)
+{
+    httpContext.Items["invoked"] = true;
+}
+app.MapPost("/", TestAction);
+""";
+        var (_, compilation) = await RunGeneratorAsync(source);
+        var serviceProvider = CreateServiceProvider();
+        var endpoint = GetEndpointFromCompilation(compilation, serviceProvider: serviceProvider);
+
+        var httpContext = CreateHttpContext();
+        httpContext.Request.Headers["Content-Type"] = "application/x-www-form-urlencoded";
+        httpContext.Request.Headers["Content-Length"] = "2049";
+        httpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(new string('x', 2049)));
+        httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
+
+        await endpoint.RequestDelegate(httpContext);
+
+        Assert.Null(httpContext.Items["invoked"]);
+        Assert.False(httpContext.RequestAborted.IsCancellationRequested);
+        Assert.Equal(400, httpContext.Response.StatusCode);
+        Assert.False(httpContext.Response.HasStarted);
+
+        var logMessage = Assert.Single(TestSink.Writes);
+        Assert.Equal(new EventId(8, "InvalidFormRequestBody"), logMessage.EventId);
+        Assert.Equal(LogLevel.Debug, logMessage.LogLevel);
+        Assert.Equal(@"Failed to read parameter ""IFormFile file"" from the request body as form.", logMessage.Message);
+        Assert.IsType<InvalidDataException>(logMessage.Exception);
+    }
+
+    [Fact]
+    public async Task RequestDelegateThrowsForMalformedFormIfThrowOnBadRequest()
+    {
+        var source = """
+void TestAction(HttpContext httpContext, IFormFile file)
+{
+    httpContext.Items["invoked"] = true;
+}
+app.MapPost("/", TestAction);
+""";
+        var (_, compilation) = await RunGeneratorAsync(source);
+        var serviceProvider = CreateServiceProvider(serviceCollection =>
+        {
+            serviceCollection.Configure<RouteHandlerOptions>(options => options.ThrowOnBadRequest = true);
+        });
+        var endpoint = GetEndpointFromCompilation(compilation, serviceProvider: serviceProvider);
+
+        var httpContext = CreateHttpContext();
+        httpContext.Request.Headers["Content-Type"] = "application/x-www-form-urlencoded";
+        httpContext.Request.Headers["Content-Length"] = "2049";
+        httpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(new string('x', 2049)));
+        httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
+
+        var badHttpRequestException = await Assert.ThrowsAsync<BadHttpRequestException>(() => endpoint.RequestDelegate(httpContext));
+
+        Assert.Null(httpContext.Items["invoked"]);
+
+        // The httpContext should be untouched.
+        Assert.False(httpContext.RequestAborted.IsCancellationRequested);
+        Assert.Equal(200, httpContext.Response.StatusCode);
+        Assert.False(httpContext.Response.HasStarted);
+
+        // We don't log bad requests when we throw.
+        Assert.Empty(TestSink.Writes);
+
+        Assert.Equal(@"Failed to read parameter ""IFormFile file"" from the request body as form.", badHttpRequestException.Message);
+        Assert.Equal(400, badHttpRequestException.StatusCode);
+        Assert.IsType<InvalidDataException>(badHttpRequestException.InnerException);
+    }
+
+    [Fact]
+    public async Task RequestDelegateValidateGeneratedFormCode()
+    {
+        var source = """
+void TestAction(HttpContext httpContext, IFormFile file, IFormFileCollection fileCollection, IFormCollection collection, [FromForm] MyTryParseRecord tryParseRecord)
+{
+    httpContext.Items["invoked"] = true;
+}
+app.MapPost("/", TestAction);
+""";
+        var (_, compilation) = await RunGeneratorAsync(source);
+
+        await VerifyAgainstBaselineUsingFile(compilation);
+    }
+}

+ 10 - 8
src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.Logging.cs

@@ -20,9 +20,8 @@ public abstract partial class RequestDelegateCreationTests : RequestDelegateCrea
 app.MapGet("/", (
 app.MapGet("/", (
     HttpContext httpContext,
     HttpContext httpContext,
     [FromHeader(Name = "foo")] StringValues headerValues,
     [FromHeader(Name = "foo")] StringValues headerValues,
-    [FromQuery(Name = "bar")] StringValues queryValues
-    // TODO: https://github.com/dotnet/aspnetcore/issues/47200
-    // [FromForm(Name = "form")] StringValues formValues
+    [FromQuery(Name = "bar")] StringValues queryValues,
+    [FromForm(Name = "form")] StringValues formValues
 ) =>
 ) =>
 {
 {
     httpContext.Items["invoked"] = true;
     httpContext.Items["invoked"] = true;
@@ -44,7 +43,7 @@ app.MapGet("/", (
 
 
         var logs = TestSink.Writes.ToArray();
         var logs = TestSink.Writes.ToArray();
 
 
-        Assert.Equal(2, logs.Length);
+        Assert.Equal(3, logs.Length);
 
 
         Assert.Equal(new EventId(4, "RequiredParameterNotProvided"), logs[0].EventId);
         Assert.Equal(new EventId(4, "RequiredParameterNotProvided"), logs[0].EventId);
         Assert.Equal(LogLevel.Debug, logs[0].LogLevel);
         Assert.Equal(LogLevel.Debug, logs[0].LogLevel);
@@ -62,10 +61,13 @@ app.MapGet("/", (
         Assert.Equal("queryValues", log2Values[1].Value);
         Assert.Equal("queryValues", log2Values[1].Value);
         Assert.Equal("query string", log2Values[2].Value);
         Assert.Equal("query string", log2Values[2].Value);
 
 
-        // TODO: https://github.com/dotnet/aspnetcore/issues/47200
-        // Assert.Equal(new EventId(4, "RequiredParameterNotProvided"), logs[2].EventId);
-        // Assert.Equal(LogLevel.Debug, logs[2].LogLevel);
-        // Assert.Equal(@"Required parameter ""StringValues formValues"" was not provided from form.", logs[2].Message);
+        Assert.Equal(new EventId(4, "RequiredParameterNotProvided"), logs[2].EventId);
+        Assert.Equal(LogLevel.Debug, logs[2].LogLevel);
+        Assert.Equal(@"Required parameter ""StringValues formValues"" was not provided from form.", logs[2].Message);
+        var log3Values = Assert.IsAssignableFrom<IReadOnlyList<KeyValuePair<string, object>>>(logs[2].State);
+        Assert.Equal("StringValues", log3Values[0].Value);
+        Assert.Equal("formValues", log3Values[1].Value);
+        Assert.Equal("form", log3Values[2].Value);
     }
     }
 
 
     [Fact]
     [Fact]

+ 0 - 114
src/Http/Http.Extensions/test/RequestDelegateGenerator/RuntimeCreationTests.cs

@@ -26,118 +26,4 @@ app.MapGet("/", ({{bindAsyncType}} myNotBindAsyncParam) => { });
         var ex = Assert.Throws<InvalidOperationException>(() => GetEndpointFromCompilation(compilation));
         var ex = Assert.Throws<InvalidOperationException>(() => GetEndpointFromCompilation(compilation));
         Assert.StartsWith($"BindAsync method found on {bindAsyncType} with incorrect format.", ex.Message);
         Assert.StartsWith($"BindAsync method found on {bindAsyncType} with incorrect format.", ex.Message);
     }
     }
-
-    // TODO: https://github.com/dotnet/aspnetcore/issues/47200
-    [Theory]
-    [InlineData(true)]
-    [InlineData(false)]
-    public async Task RequestDelegateLogsIOExceptionsForFormAsDebugDoesNotAbortAndNeverThrows(bool throwOnBadRequests)
-    {
-        var source = """
-void TestAction(HttpContext httpContext, IFormFile file)
-{
-    httpContext.Items["invoked"] = true;
-}
-app.MapPost("/", TestAction);
-""";
-        var (_, compilation) = await RunGeneratorAsync(source);
-        var serviceProvider = CreateServiceProvider(serviceCollection =>
-        {
-            serviceCollection.Configure<RouteHandlerOptions>(options => options.ThrowOnBadRequest = throwOnBadRequests);
-        });
-        var endpoint = GetEndpointFromCompilation(compilation, serviceProvider: serviceProvider);
-
-        var ioException = new IOException();
-
-        var httpContext = CreateHttpContext();
-        httpContext.Request.Headers["Content-Type"] = "application/x-www-form-urlencoded";
-        httpContext.Request.Headers["Content-Length"] = "1";
-        httpContext.Request.Body = new ExceptionThrowingRequestBodyStream(ioException);
-        httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
-
-        await endpoint.RequestDelegate(httpContext);
-
-        Assert.Null(httpContext.Items["invoked"]);
-        Assert.False(httpContext.RequestAborted.IsCancellationRequested);
-
-        var logMessage = Assert.Single(TestSink.Writes);
-        Assert.Equal(new EventId(1, "RequestBodyIOException"), logMessage.EventId);
-        Assert.Equal(LogLevel.Debug, logMessage.LogLevel);
-        Assert.Equal("Reading the request body failed with an IOException.", logMessage.Message);
-        Assert.Same(ioException, logMessage.Exception);
-    }
-
-    [Fact]
-    public async Task RequestDelegateLogsMalformedFormAsDebugAndSets400Response()
-    {
-        var source = """
-void TestAction(HttpContext httpContext, IFormFile file)
-{
-    httpContext.Items["invoked"] = true;
-}
-app.MapPost("/", TestAction);
-""";
-        var (_, compilation) = await RunGeneratorAsync(source);
-        var serviceProvider = CreateServiceProvider();
-        var endpoint = GetEndpointFromCompilation(compilation, serviceProvider: serviceProvider);
-
-        var httpContext = CreateHttpContext();
-        httpContext.Request.Headers["Content-Type"] = "application/x-www-form-urlencoded";
-        httpContext.Request.Headers["Content-Length"] = "2049";
-        httpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(new string('x', 2049)));
-        httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
-
-        await endpoint.RequestDelegate(httpContext);
-
-        Assert.Null(httpContext.Items["invoked"]);
-        Assert.False(httpContext.RequestAborted.IsCancellationRequested);
-        Assert.Equal(400, httpContext.Response.StatusCode);
-        Assert.False(httpContext.Response.HasStarted);
-
-        var logMessage = Assert.Single(TestSink.Writes);
-        Assert.Equal(new EventId(8, "InvalidFormRequestBody"), logMessage.EventId);
-        Assert.Equal(LogLevel.Debug, logMessage.LogLevel);
-        Assert.Equal(@"Failed to read parameter ""IFormFile file"" from the request body as form.", logMessage.Message);
-        Assert.IsType<InvalidDataException>(logMessage.Exception);
-    }
-
-    [Fact]
-    public async Task RequestDelegateThrowsForMalformedFormIfThrowOnBadRequest()
-    {
-        var source = """
-void TestAction(HttpContext httpContext, IFormFile file)
-{
-    httpContext.Items["invoked"] = true;
-}
-app.MapPost("/", TestAction);
-""";
-        var (_, compilation) = await RunGeneratorAsync(source);
-        var serviceProvider = CreateServiceProvider(serviceCollection =>
-        {
-            serviceCollection.Configure<RouteHandlerOptions>(options => options.ThrowOnBadRequest = true);
-        });
-        var endpoint = GetEndpointFromCompilation(compilation, serviceProvider: serviceProvider);
-
-        var httpContext = CreateHttpContext();
-        httpContext.Request.Headers["Content-Type"] = "application/x-www-form-urlencoded";
-        httpContext.Request.Headers["Content-Length"] = "2049";
-        httpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(new string('x', 2049)));
-        httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
-
-        var badHttpRequestException = await Assert.ThrowsAsync<BadHttpRequestException>(() => endpoint.RequestDelegate(httpContext));
-
-        Assert.Null(httpContext.Items["invoked"]);
-
-        // The httpContext should be untouched.
-        Assert.False(httpContext.RequestAborted.IsCancellationRequested);
-        Assert.Equal(200, httpContext.Response.StatusCode);
-        Assert.False(httpContext.Response.HasStarted);
-
-        // We don't log bad requests when we throw.
-        Assert.Empty(TestSink.Writes);
-
-        Assert.Equal(@"Failed to read parameter ""IFormFile file"" from the request body as form.", badHttpRequestException.Message);
-        Assert.Equal(400, badHttpRequestException.StatusCode);
-        Assert.IsType<InvalidDataException>(badHttpRequestException.InnerException);
-    }
 }
 }

+ 34 - 0
src/Http/Http.Extensions/test/RequestDelegateGenerator/SharedTypes.cs

@@ -4,6 +4,8 @@ using System.Diagnostics;
 using System.Reflection;
 using System.Reflection;
 using System.Text.Json;
 using System.Text.Json;
 using System.Text.Json.Serialization;
 using System.Text.Json.Serialization;
+using System.Security.Cryptography.X509Certificates;
+using Microsoft.AspNetCore.Http.Features;
 using Microsoft.AspNetCore.Http.Metadata;
 using Microsoft.AspNetCore.Http.Metadata;
 
 
 namespace Microsoft.AspNetCore.Http.Generators.Tests;
 namespace Microsoft.AspNetCore.Http.Generators.Tests;
@@ -471,3 +473,35 @@ public class ExceptionThrowingRequestBodyStream : Stream
         throw new NotImplementedException();
         throw new NotImplementedException();
     }
     }
 }
 }
+
+public readonly struct TraceIdentifier
+{
+    private TraceIdentifier(string id)
+    {
+        Id = id;
+    }
+
+    public string Id { get; }
+
+    public static implicit operator string(TraceIdentifier value) => value.Id;
+
+    public static ValueTask<TraceIdentifier> BindAsync(HttpContext context)
+    {
+        return ValueTask.FromResult(new TraceIdentifier(context.TraceIdentifier));
+    }
+}
+
+public class TlsConnectionFeature : ITlsConnectionFeature
+{
+    public TlsConnectionFeature(X509Certificate2 clientCertificate)
+    {
+        ClientCertificate = clientCertificate;
+    }
+
+    public X509Certificate2 ClientCertificate { get; set; }
+
+    public Task<X509Certificate2> GetClientCertificateAsync(CancellationToken cancellationToken)
+    {
+        throw new NotImplementedException();
+    }
+}