Просмотр исходного кода

Add support for logging/throwing exceptions in RDG (#47657)

* Add support for logging/throwing exceptions in RDG

* Fix event names, structured logs, and excessive allocation

* Remove unused vars, fix allocation, and update logger

* Use LogOrThrowExceptionHelper class
Safia Abdalla 2 лет назад
Родитель
Сommit
0e4f4bac9d
49 измененных файлов с 5341 добавлено и 982 удалено
  1. 1 0
      src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.RequestDelegateGenerator.csproj
  2. 5 0
      src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs
  3. 173 6
      src/Http/Http.Extensions/gen/RequestDelegateGeneratorSources.cs
  4. 1 0
      src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Emitters/EmitterContext.cs
  5. 32 0
      src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Emitters/EmitterExtensions.cs
  6. 13 2
      src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Emitters/EndpointEmitter.cs
  7. 1 2
      src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Emitters/EndpointJsonResponseEmitter.cs
  8. 13 5
      src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Emitters/EndpointParameterEmitter.cs
  9. 4 0
      src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Endpoint.cs
  10. 10 1
      src/Http/Http.Extensions/gen/StaticRouteHandlerModel/EndpointParameter.cs
  11. 3 2
      src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj
  12. 15 36
      src/Http/Http.Extensions/src/RequestDelegateFactory.cs
  13. 11 840
      src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs
  14. 198 0
      src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_BindAsync_Snapshot.generated.txt
  15. 173 7
      src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitBodyParam_ComplexReturn_Snapshot.generated.txt
  16. 155 2
      src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitHeader_ComplexTypeArrayParam.generated.txt
  17. 145 0
      src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitHeader_NullableStringArrayParam.generated.txt
  18. 145 0
      src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitHeader_StringArrayParam.generated.txt
  19. 155 2
      src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_ComplexTypeArrayParam.generated.txt
  20. 144 0
      src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_NullableStringArrayParam.generated.txt
  21. 144 0
      src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_StringArrayParam.generated.txt
  22. 150 0
      src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitServiceParam_SimpleReturn_Snapshot.generated.txt
  23. 164 0
      src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitSource_SimpleReturn_Snapshot.generated.txt
  24. 156 3
      src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_ComplexTypeArrayParam.generated.txt
  25. 144 0
      src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_NullableStringArrayParam.generated.txt
  26. 144 0
      src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_NullableStringArrayParam_EmptyQueryValues.generated.txt
  27. 144 0
      src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_NullableStringArrayParam_QueryNotPresent.generated.txt
  28. 144 0
      src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_StringArrayParam.generated.txt
  29. 172 8
      src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_JsonBodyOrService_HandlesBothJsonAndService.generated.txt
  30. 144 0
      src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_MultipleSpecialTypeParam_StringReturn.generated.txt
  31. 150 0
      src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_MultipleStringParam_StringReturn.generated.txt
  32. 144 0
      src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_NoParam_StringReturn_WithFilter.generated.txt
  33. 158 2
      src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_SingleComplexTypeParam_StringReturn.generated.txt
  34. 158 2
      src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_SingleEnumParam_StringReturn.generated.txt
  35. 146 0
      src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_SingleNullableStringParam_WithEmptyQueryStringValueProvided_StringReturn.generated.txt
  36. 171 7
      src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapPost_WithArrayQueryString_AndBody_ShouldUseBody.generated.txt
  37. 170 7
      src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapPost_WithArrayQueryString_ShouldFail.generated.txt
  38. 144 0
      src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/Multiple_MapAction_NoParam_StringReturn.generated.txt
  39. 144 0
      src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/Multiple_MapAction_WithParams_StringReturn.generated.txt
  40. 1 1
      src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTestBase.cs
  41. 9 0
      src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.BindAsync.cs
  42. 49 0
      src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.JsonBody.cs
  43. 52 1
      src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.JsonBodyOrService.cs
  44. 646 0
      src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.Logging.cs
  45. 57 45
      src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.QueryParameters.cs
  46. 119 0
      src/Http/Http.Extensions/test/RequestDelegateGenerator/RuntimeCreationTests.cs
  47. 66 1
      src/Http/Http.Extensions/test/RequestDelegateGenerator/SharedTypes.cs
  48. 46 0
      src/Shared/RequestDelegateCreationMessages.cs
  49. 8 0
      src/Shared/RoslynUtils/SymbolExtensions.cs

+ 1 - 0
src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.RequestDelegateGenerator.csproj

@@ -22,6 +22,7 @@
   <ItemGroup>
     <Compile Include="$(SharedSourceRoot)IsExternalInit.cs" LinkBase="Shared" />
     <Compile Include="$(SharedSourceRoot)HashCode.cs" LinkBase="Shared" />
+    <Compile Include="$(SharedSourceRoot)RequestDelegateCreationMessages.cs" LinkBase="Shared" />
     <Compile Include="$(SharedSourceRoot)RoslynUtils\BoundedCacheWithFactory.cs" LinkBase="Shared" />
     <Compile Include="$(SharedSourceRoot)RoslynUtils\WellKnownTypeData.cs" LinkBase="Shared" />
     <Compile Include="$(SharedSourceRoot)RoslynUtils\WellKnownTypes.cs" LinkBase="Shared" />

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

@@ -85,6 +85,11 @@ public sealed class RequestDelegateGenerator : IIncrementalGenerator
             codeWriter.StartBlock();
             codeWriter.WriteLine($"var handler = ({endpoint!.EmitHandlerDelegateType(considerOptionality: true)})del;");
             codeWriter.WriteLine("EndpointFilterDelegate? filteredInvocation = null;");
+            if (endpoint!.EmitterContext.RequiresLoggingHelper || endpoint!.EmitterContext.HasJsonBodyOrService || endpoint!.Response?.IsSerializableJsonResponse(out var _) is true)
+            {
+                codeWriter.WriteLine("var serviceProvider = options?.ServiceProvider ?? options?.EndpointBuilder?.ApplicationServices;");
+            }
+            endpoint!.EmitLoggingPreamble(codeWriter);
             endpoint!.EmitRouteOrQueryResolver(codeWriter);
             endpoint!.EmitJsonBodyOrServiceResolver(codeWriter);
             endpoint!.Response?.EmitJsonPreparation(codeWriter);

+ 173 - 6
src/Http/Http.Extensions/gen/RequestDelegateGeneratorSources.cs

@@ -1,6 +1,7 @@
 // Licensed to the .NET Foundation under one or more agreements.
 // The .NET Foundation licenses this file to you under the MIT license.
 
+using Microsoft.CodeAnalysis.CSharp;
 namespace Microsoft.AspNetCore.Http.RequestDelegateGenerator;
 
 internal static class RequestDelegateGeneratorSources
@@ -19,8 +20,8 @@ internal static class RequestDelegateGeneratorSources
 
     public static string GeneratedCodeAttribute => $@"[System.CodeDom.Compiler.GeneratedCodeAttribute(""{typeof(RequestDelegateGeneratorSources).Assembly.FullName}"", ""{typeof(RequestDelegateGeneratorSources).Assembly.GetName().Version}"")]";
 
-    public static string TryResolveBodyAsyncMethod => """
-        private static async ValueTask<(bool, T?)> TryResolveBodyAsync<T>(HttpContext httpContext, bool allowEmpty)
+    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)
         {
             var feature = httpContext.Features.Get<Microsoft.AspNetCore.Http.Features.IHttpRequestBodyDetectionFeature>();
 
@@ -28,6 +29,7 @@ internal static class RequestDelegateGeneratorSources
             {
                 if (!httpContext.Request.HasJsonContentType())
                 {
+                    logOrThrowExceptionHelper.UnexpectedJsonContentType(httpContext.Request.ContentType);
                     httpContext.Response.StatusCode = StatusCodes.Status415UnsupportedMediaType;
                     return (false, default);
                 }
@@ -36,17 +38,34 @@ internal static class RequestDelegateGeneratorSources
                     var bodyValue = await httpContext.Request.ReadFromJsonAsync<T>();
                     if (!allowEmpty && bodyValue == null)
                     {
+                        if (!isInferred)
+                        {
+                            logOrThrowExceptionHelper.RequiredParameterNotProvided(parameterTypeName, parameterName, "body");
+                        }
+                        else
+                        {
+                            logOrThrowExceptionHelper.ImplicitBodyNotProvided(parameterName);
+                        }
                         httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
                         return (false, bodyValue);
                     }
                     return (true, bodyValue);
                 }
-                catch (IOException)
+                catch (BadHttpRequestException badHttpRequestException)
                 {
+                    logOrThrowExceptionHelper.RequestBodyIOException(badHttpRequestException);
+                    httpContext.Response.StatusCode = badHttpRequestException.StatusCode;
                     return (false, default);
                 }
-                catch (System.Text.Json.JsonException)
+                catch (IOException ioException)
                 {
+                    logOrThrowExceptionHelper.RequestBodyIOException(ioException);
+                    httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
+                    return (false, default);
+                }
+                catch (System.Text.Json.JsonException jsonException)
+                {
+                    logOrThrowExceptionHelper.InvalidJsonRequestBody(parameterTypeName, parameterName, jsonException);
                     httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
                     return (false, default);
                 }
@@ -96,7 +115,7 @@ internal static class RequestDelegateGeneratorSources
 """;
 
     public static string ResolveJsonBodyOrServiceMethod => """
-        private static Func<HttpContext, bool, ValueTask<(bool, T?)>> ResolveJsonBodyOrService<T>(IServiceProviderIsService? serviceProviderIsService = null)
+        private static Func<HttpContext, bool, ValueTask<(bool, T?)>> ResolveJsonBodyOrService<T>(LogOrThrowExceptionHelper logOrThrowExceptionHelper, string parameterTypeName, string parameterName, IServiceProviderIsService? serviceProviderIsService = null)
         {
             if (serviceProviderIsService is not null)
             {
@@ -105,10 +124,155 @@ internal static class RequestDelegateGeneratorSources
                     return static (httpContext, isOptional) => new ValueTask<(bool, T?)>((true, httpContext.RequestServices.GetService<T>()));
                 }
             }
-            return static (httpContext, isOptional) => TryResolveBodyAsync<T>(httpContext, isOptional);
+            return (httpContext, isOptional) => TryResolveBodyAsync<T>(httpContext, logOrThrowExceptionHelper, isOptional, parameterTypeName, parameterName, isInferred: true);
         }
 """;
 
+    public static string LogOrThrowExceptionHelperClass => $$"""
+    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("{{typeof(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({{RequestDelegateCreationLogging.RequestBodyIOExceptionEventId}}, {{SymbolDisplay.FormatLiteral(RequestDelegateCreationLogging.RequestBodyIOExceptionEventName, true)}}), {{SymbolDisplay.FormatLiteral(RequestDelegateCreationLogging.RequestBodyIOExceptionMessage, true)}});
+
+        public void InvalidJsonRequestBody(string parameterTypeName, string parameterName, Exception exception)
+        {
+            if (_shouldThrow)
+            {
+                var message = string.Format(CultureInfo.InvariantCulture, {{SymbolDisplay.FormatLiteral(RequestDelegateCreationLogging.InvalidJsonRequestBodyExceptionMessage, true)}}, 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({{RequestDelegateCreationLogging.InvalidJsonRequestBodyEventId}}, {{SymbolDisplay.FormatLiteral(RequestDelegateCreationLogging.InvalidJsonRequestBodyEventName, true)}}), {{SymbolDisplay.FormatLiteral(RequestDelegateCreationLogging.InvalidJsonRequestBodyLogMessage, true)}});
+
+        public void ParameterBindingFailed(string parameterTypeName, string parameterName, string sourceValue)
+        {
+            if (_shouldThrow)
+            {
+                var message = string.Format(CultureInfo.InvariantCulture, {{SymbolDisplay.FormatLiteral(RequestDelegateCreationLogging.ParameterBindingFailedExceptionMessage, true)}}, 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({{RequestDelegateCreationLogging.ParameterBindingFailedEventId}}, {{SymbolDisplay.FormatLiteral(RequestDelegateCreationLogging.ParameterBindingFailedEventName, true)}}), {{SymbolDisplay.FormatLiteral(RequestDelegateCreationLogging.ParameterBindingFailedLogMessage, true)}});
+
+        public void RequiredParameterNotProvided(string parameterTypeName, string parameterName, string source)
+        {
+            if (_shouldThrow)
+            {
+                var message = string.Format(CultureInfo.InvariantCulture, {{SymbolDisplay.FormatLiteral(RequestDelegateCreationLogging.RequiredParameterNotProvidedExceptionMessage, true)}}, 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({{RequestDelegateCreationLogging.RequiredParameterNotProvidedEventId}}, {{SymbolDisplay.FormatLiteral(RequestDelegateCreationLogging.RequiredParameterNotProvidedEventName, true)}}), {{SymbolDisplay.FormatLiteral(RequestDelegateCreationLogging.RequiredParameterNotProvidedLogMessage, true)}});
+
+        public void ImplicitBodyNotProvided(string parameterName)
+        {
+            if (_shouldThrow)
+            {
+                var message = string.Format(CultureInfo.InvariantCulture, {{SymbolDisplay.FormatLiteral(RequestDelegateCreationLogging.ImplicitBodyNotProvidedExceptionMessage, true)}}, 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({{RequestDelegateCreationLogging.ImplicitBodyNotProvidedEventId}}, {{SymbolDisplay.FormatLiteral(RequestDelegateCreationLogging.ImplicitBodyNotProvidedEventName, true)}}), {{SymbolDisplay.FormatLiteral(RequestDelegateCreationLogging.ImplicitBodyNotProvidedLogMessage, true)}});
+
+        public void UnexpectedJsonContentType(string? contentType)
+        {
+            if (_shouldThrow)
+            {
+                var message = string.Format(CultureInfo.InvariantCulture, {{SymbolDisplay.FormatLiteral(RequestDelegateCreationLogging.UnexpectedJsonContentTypeExceptionMessage, true)}}, 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({{RequestDelegateCreationLogging.UnexpectedJsonContentTypeEventId}}, {{SymbolDisplay.FormatLiteral(RequestDelegateCreationLogging.UnexpectedJsonContentTypeEventName, true)}}), {{SymbolDisplay.FormatLiteral(RequestDelegateCreationLogging.UnexpectedJsonContentTypeLogMessage, true)}});
+
+        public void UnexpectedNonFormContentType(string? contentType)
+        {
+            if (_shouldThrow)
+            {
+                var message = string.Format(CultureInfo.InvariantCulture, {{SymbolDisplay.FormatLiteral(RequestDelegateCreationLogging.UnexpectedFormContentTypeExceptionMessage, true)}}, 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({{RequestDelegateCreationLogging.UnexpectedFormContentTypeEventId}}, {{SymbolDisplay.FormatLiteral(RequestDelegateCreationLogging.UnexpectedFormContentTypeLogEventName, true)}}), {{SymbolDisplay.FormatLiteral(RequestDelegateCreationLogging.UnexpectedFormContentTypeLogMessage, true)}});
+
+        public void InvalidFormRequestBody(string parameterTypeName, string parameterName, Exception exception)
+        {
+            if (_shouldThrow)
+            {
+                var message = string.Format(CultureInfo.InvariantCulture, {{SymbolDisplay.FormatLiteral(RequestDelegateCreationLogging.InvalidFormRequestBodyExceptionMessage, true)}}, 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({{RequestDelegateCreationLogging.InvalidFormRequestBodyEventId}}, {{SymbolDisplay.FormatLiteral(RequestDelegateCreationLogging.InvalidFormRequestBodyEventName, true)}}), {{SymbolDisplay.FormatLiteral(RequestDelegateCreationLogging.InvalidFormRequestBodyLogMessage, true)}});
+    }
+""";
+
     public static string GetGeneratedRouteBuilderExtensionsSource(string genericThunks, string thunks, string endpoints, string helperMethods) => $$"""
 {{SourceHeader}}
 
@@ -153,6 +317,7 @@ namespace Microsoft.AspNetCore.Http.Generated
     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;
 
@@ -199,6 +364,8 @@ namespace Microsoft.AspNetCore.Http.Generated
 
 {{helperMethods}}
     }
+
+{{LogOrThrowExceptionHelperClass}}
 }
 """;
     private static string GetGenericThunks(string genericThunks) => genericThunks != string.Empty ? $$"""

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

@@ -10,4 +10,5 @@ internal sealed class EmitterContext
     public bool HasBindAsync { get; set; }
     public bool HasParsable { get; set; }
     public bool HasJsonResponse { get; set; }
+    public bool RequiresLoggingHelper { get; set; }
 }

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

@@ -0,0 +1,32 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.AspNetCore.Analyzers.Infrastructure;
+using Microsoft.CodeAnalysis;
+
+namespace Microsoft.AspNetCore.Http.RequestDelegateGenerator.StaticRouteHandlerModel;
+
+internal static class EmitterExtensions
+{
+    public static string ToMessageString(this EndpointParameter endpointParameter) => endpointParameter.Source switch
+    {
+        EndpointParameterSource.Header => "header",
+        EndpointParameterSource.Query => "query string",
+        EndpointParameterSource.RouteOrQuery => "route or query string",
+        EndpointParameterSource.BindAsync => endpointParameter.BindMethod == BindabilityMethod.BindAsync
+            ? $"{endpointParameter.Type.ToDisplayString(SymbolDisplayFormat.CSharpShortErrorMessageFormat)}.BindAsync(HttpContext)"
+            : $"{endpointParameter.Type.ToDisplayString(SymbolDisplayFormat.CSharpShortErrorMessageFormat)}.BindAsync(HttpContext, ParameterInfo)",
+        _ => "unknown"
+    };
+
+    public static bool IsSerializableJsonResponse(this EndpointResponse endpointResponse, [NotNullWhen(true)] out ITypeSymbol? responseTypeSymbol)
+    {
+        responseTypeSymbol = null;
+        if (endpointResponse is { IsSerializable: true, ResponseType: { } responseType })
+        {
+            responseTypeSymbol = responseType;
+            return true;
+        }
+        return false;
+    }
+}

+ 13 - 2
src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Emitters/EndpointEmitter.cs

@@ -1,6 +1,8 @@
 // Licensed to the .NET Foundation under one or more agreements.
 // The .NET Foundation licenses this file to you under the MIT license.
 
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
 using System.Globalization;
 using System.IO;
 using System.Linq;
@@ -70,14 +72,23 @@ internal static class EndpointEmitter
             {
                 if (!serviceProviderEmitted)
                 {
-                    codeWriter.WriteLine("var serviceProviderIsService = options?.ServiceProvider?.GetService<IServiceProviderIsService>();");
+                    codeWriter.WriteLine("var serviceProviderIsService = serviceProvider?.GetService<IServiceProviderIsService>();");
                     serviceProviderEmitted = true;
                 }
                 codeWriter.Write($@"var {parameter.SymbolName}_JsonBodyOrServiceResolver = ");
-                codeWriter.WriteLine($"ResolveJsonBodyOrService<{parameter.Type.ToDisplayString(EmitterConstants.DisplayFormat)}>(serviceProviderIsService);");
+                var shortParameterTypeName = parameter.Type.ToDisplayString(SymbolDisplayFormat.CSharpShortErrorMessageFormat);
+                codeWriter.WriteLine($"ResolveJsonBodyOrService<{parameter.Type.ToDisplayString(EmitterConstants.DisplayFormat)}>(logOrThrowExceptionHelper, {SymbolDisplay.FormatLiteral(shortParameterTypeName, true)}, {SymbolDisplay.FormatLiteral(parameter.SymbolName, true)}, serviceProviderIsService);");
             }
         }
     }
 
+    public static void EmitLoggingPreamble(this Endpoint endpoint, CodeWriter codeWriter)
+    {
+        if (endpoint.EmitterContext.RequiresLoggingHelper)
+        {
+            codeWriter.WriteLine("var logOrThrowExceptionHelper = new LogOrThrowExceptionHelper(serviceProvider, options);");
+        }
+    }
+
     public static string EmitArgumentList(this Endpoint endpoint) => string.Join(", ", endpoint.Parameters.Select(p => p.EmitArgument()));
 }

+ 1 - 2
src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Emitters/EndpointJsonResponseEmitter.cs

@@ -7,11 +7,10 @@ internal static class EndpointJsonResponseEmitter
 {
     internal static void EmitJsonPreparation(this EndpointResponse endpointResponse, CodeWriter codeWriter)
     {
-        if (endpointResponse is { IsSerializable: true, ResponseType: {} responseType })
+        if (endpointResponse.IsSerializableJsonResponse(out var responseType))
         {
             var typeName = responseType.ToDisplayString(EmitterConstants.DisplayFormat);
 
-            codeWriter.WriteLine("var serviceProvider = options?.ServiceProvider ?? options?.EndpointBuilder?.ApplicationServices;");
             codeWriter.WriteLine("var serializerOptions = serviceProvider?.GetService<IOptions<JsonOptions>>()?.Value.SerializerOptions ?? new JsonOptions().SerializerOptions;");
             codeWriter.WriteLine($"var jsonTypeInfo =  (JsonTypeInfo<{typeName}>)serializerOptions.GetTypeInfo(typeof({typeName}));");
         }

+ 13 - 5
src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Emitters/EndpointParameterEmitter.cs

@@ -2,10 +2,11 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 
 using System;
-using System.Reflection.Metadata;
+using System.Globalization;
 using Microsoft.AspNetCore.Analyzers.Infrastructure;
 using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure;
 using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
 
 namespace Microsoft.AspNetCore.Http.RequestDelegateGenerator.StaticRouteHandlerModel.Emitters;
 
@@ -33,7 +34,7 @@ internal static class EndpointParameterEmitter
         }
         else if (endpointParameter.IsOptional)
         {
-            codeWriter.WriteLine($"var {endpointParameter.EmitTempArgument()} = {endpointParameter.EmitAssigningCodeResult()}.Count > 0 ? (string?){endpointParameter.EmitAssigningCodeResult()} : null;");
+            codeWriter.WriteLine($"var {endpointParameter.EmitTempArgument()} = {endpointParameter.EmitAssigningCodeResult()}.Count > 0 ? (string?){endpointParameter.EmitAssigningCodeResult()} : {endpointParameter.DefaultValue};");
         }
         else if (endpointParameter.IsStringValues)
         {
@@ -44,6 +45,7 @@ internal static class EndpointParameterEmitter
             codeWriter.WriteLine($"if (StringValues.IsNullOrEmpty({endpointParameter.EmitAssigningCodeResult()}))");
             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()} = (string?){endpointParameter.EmitAssigningCodeResult()};");
         }
@@ -107,6 +109,7 @@ internal static class EndpointParameterEmitter
             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();
         }
 
@@ -127,13 +130,17 @@ internal static class EndpointParameterEmitter
         }
         else if (endpointParameter.IsOptional)
         {
-            codeWriter.WriteLine($"var {endpointParameter.EmitTempArgument()} = {endpointParameter.EmitAssigningCodeResult()}.Count > 0 ? (string?){endpointParameter.EmitAssigningCodeResult()} : null;");
+            // For non-string parameters, the TryParse logic takes care of setting the default value fallback.
+            // Strings don't undergo the TryParse treatment so we set the default value here.
+            var fallback = endpointParameter.Type.SpecialType == SpecialType.System_String ? endpointParameter.DefaultValue : "null";
+            codeWriter.WriteLine($"var {endpointParameter.EmitTempArgument()} = {endpointParameter.EmitAssigningCodeResult()}.Count > 0 ? (string?){endpointParameter.EmitAssigningCodeResult()} : {fallback};");
         }
         else
         {
             codeWriter.WriteLine($"if ({endpointParameter.EmitAssigningCodeResult()} is StringValues {{ Count: 0 }})");
             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()} = (string?){endpointParameter.EmitAssigningCodeResult()};");
         }
@@ -148,7 +155,8 @@ internal static class EndpointParameterEmitter
 
         // Invoke TryResolveBody method to parse JSON and set
         // status codes on exceptions.
-        var assigningCode = $"await GeneratedRouteBuilderExtensionsCore.TryResolveBodyAsync<{endpointParameter.Type.ToDisplayString(EmitterConstants.DisplayFormat)}>(httpContext, {(endpointParameter.IsOptional ? "true" : "false")})";
+        var shortParameterTypeName = endpointParameter.Type.ToDisplayString(SymbolDisplayFormat.CSharpShortErrorMessageFormat);
+        var assigningCode = $"await GeneratedRouteBuilderExtensionsCore.TryResolveBodyAsync<{endpointParameter.Type.ToDisplayString(EmitterConstants.DisplayFormat)}>(httpContext, logOrThrowExceptionHelper, {(endpointParameter.IsOptional ? "true" : "false")}, {SymbolDisplay.FormatLiteral(shortParameterTypeName, true)}, {SymbolDisplay.FormatLiteral(endpointParameter.SymbolName, true)})";
         var resolveBodyResult = $"{endpointParameter.SymbolName}_resolveBodyResult";
         codeWriter.WriteLine($"var {resolveBodyResult} = {assigningCode};");
         codeWriter.WriteLine($"var {endpointParameter.EmitHandlerArgument()} = {resolveBodyResult}.Item2;");
@@ -211,9 +219,9 @@ internal static class EndpointParameterEmitter
         else
         {
             codeWriter.WriteLine($"{unwrappedTypeString} {endpointParameter.EmitHandlerArgument()};");
-
             codeWriter.WriteLine($"if ((object?){endpointParameter.EmitTempArgument()} == null)");
             codeWriter.StartBlock();
+            codeWriter.WriteLine($@"logOrThrowExceptionHelper.RequiredParameterNotProvided({SymbolDisplay.FormatLiteral(endpointParameter.Type.ToDisplayString(SymbolDisplayFormat.CSharpShortErrorMessageFormat), true)}, {SymbolDisplay.FormatLiteral(endpointParameter.SymbolName, true)}, {SymbolDisplay.FormatLiteral(endpointParameter.ToMessageString(), true)});");
             codeWriter.WriteLine("wasParamCheckFailure = true;");
             codeWriter.WriteLine($"{endpointParameter.EmitHandlerArgument()} = default!;");
             codeWriter.EndBlock();

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

@@ -48,6 +48,7 @@ internal class Endpoint
 
         if (method.Parameters.Length == 0)
         {
+            EmitterContext.RequiresLoggingHelper = false;
             return;
         }
 
@@ -91,6 +92,9 @@ internal class Endpoint
         EmitterContext.HasRouteOrQuery = Parameters.Any(parameter => parameter.Source == EndpointParameterSource.RouteOrQuery);
         EmitterContext.HasBindAsync = Parameters.Any(parameter => parameter.Source == EndpointParameterSource.BindAsync);
         EmitterContext.HasParsable = Parameters.Any(parameter => parameter.IsParsable);
+        EmitterContext.RequiresLoggingHelper = !Parameters.All(parameter =>
+            parameter.Source == EndpointParameterSource.SpecialType ||
+            parameter is { IsArray: true, ElementType.SpecialType: SpecialType.System_String, Source: EndpointParameterSource.Query });
     }
 
     public string HttpMethod { get; }

+ 10 - 1
src/Http/Http.Extensions/gen/StaticRouteHandlerModel/EndpointParameter.cs

@@ -3,11 +3,13 @@
 
 using System;
 using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
 using System.Text;
 using Microsoft.AspNetCore.Analyzers.Infrastructure;
 using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure;
 using Microsoft.AspNetCore.App.Analyzers.Infrastructure;
 using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
 using WellKnownType = Microsoft.AspNetCore.App.Analyzers.Infrastructure.WellKnownTypeData.WellKnownType;
 
 namespace Microsoft.AspNetCore.Http.RequestDelegateGenerator.StaticRouteHandlerModel;
@@ -22,6 +24,7 @@ internal class EndpointParameter
         Ordinal = parameter.Ordinal;
         Source = EndpointParameterSource.Unknown;
         IsOptional = parameter.IsOptional();
+        DefaultValue = parameter.GetDefaultValueString();
         IsArray = TryGetArrayElementType(parameter, out var elementType);
         ElementType = elementType;
 
@@ -137,6 +140,7 @@ internal class EndpointParameter
     public int Ordinal { get; }
     public bool IsOptional { get; }
     public bool IsArray { get; set; }
+    public string DefaultValue { get; set; }
 
     public EndpointParameterSource Source { get; }
 
@@ -236,7 +240,7 @@ internal class EndpointParameter
                 writer.EndBlock();
                 writer.WriteLine($$"""else if (string.IsNullOrEmpty({{inputArgument}}))""");
                 writer.StartBlock();
-                writer.WriteLine($$"""{{outputArgument}} = null;""");
+                writer.WriteLine($$"""{{outputArgument}} = {{DefaultValue}};""");
                 writer.EndBlock();
                 writer.WriteLine("else");
                 writer.StartBlock();
@@ -255,6 +259,7 @@ internal class EndpointParameter
                     writer.WriteLine($$"""if (!string.IsNullOrEmpty({{inputArgument}}))""");
                     writer.StartBlock();
                     writer.WriteLine("wasParamCheckFailure = true;");
+                    writer.WriteLine($@"logOrThrowExceptionHelper.RequiredParameterNotProvided({SymbolDisplay.FormatLiteral(Type.ToDisplayString(SymbolDisplayFormat.CSharpShortErrorMessageFormat), true)}, {SymbolDisplay.FormatLiteral(SymbolName, true)}, {SymbolDisplay.FormatLiteral(this.ToMessageString(), true)});");
                     writer.EndBlock();
                     writer.EndBlock();
                 }
@@ -262,8 +267,12 @@ internal class EndpointParameter
                 {
                     writer.WriteLine($$"""if (!{{preferredTryParseInvocation(inputArgument, outputArgument)}})""");
                     writer.StartBlock();
+                    writer.WriteLine($"if (!string.IsNullOrEmpty({inputArgument}))");
+                    writer.StartBlock();
+                    writer.WriteLine($@"logOrThrowExceptionHelper.ParameterBindingFailed({SymbolDisplay.FormatLiteral(Type.ToDisplayString(SymbolDisplayFormat.CSharpShortErrorMessageFormat), true)}, {SymbolDisplay.FormatLiteral(SymbolName, true)}, {inputArgument});");
                     writer.WriteLine("wasParamCheckFailure = true;");
                     writer.EndBlock();
+                    writer.EndBlock();
                 }
             };
         }

+ 3 - 2
src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj

@@ -14,6 +14,7 @@
   <ItemGroup>
     <Compile Include="$(SharedSourceRoot)ObjectMethodExecutor\**\*.cs" LinkBase="Shared" />
     <Compile Include="$(SharedSourceRoot)ParameterBindingMethodCache.cs" LinkBase="Shared" />
+    <Compile Include="$(SharedSourceRoot)RequestDelegateCreationMessages.cs" LinkBase="Shared" />
     <Compile Include="$(SharedSourceRoot)EndpointMetadataPopulator.cs" LinkBase="Shared" />
     <Compile Include="$(SharedSourceRoot)PropertyAsParameterInfo.cs" LinkBase="Shared" />
     <Compile Include="..\..\Shared\StreamCopyOperationInternal.cs" LinkBase="Shared" />
@@ -25,8 +26,8 @@
     <Compile Include="$(SharedSourceRoot)Json\JsonSerializerExtensions.cs" LinkBase="Shared" />
     <Compile Include="$(SharedSourceRoot)TrimmingAppContextSwitches.cs" LinkBase="Shared" />
     <Compile Include="$(SharedSourceRoot)RouteHandlers\ExecuteHandlerHelper.cs" LinkBase="Shared"/>
-  </ItemGroup>  
-  
+  </ItemGroup>
+
   <ItemGroup>
     <EmbeddedResource Include="Properties\ILLink.Substitutions.xml" LogicalName="ILLink.Substitutions.xml" />
   </ItemGroup>

+ 15 - 36
src/Http/Http.Extensions/src/RequestDelegateFactory.cs

@@ -2359,131 +2359,110 @@ public static partial class RequestDelegateFactory
 
     private static partial class Log
     {
-        private const string InvalidJsonRequestBodyMessage = @"Failed to read parameter ""{ParameterType} {ParameterName}"" from the request body as JSON.";
-        private const string InvalidJsonRequestBodyExceptionMessage = @"Failed to read parameter ""{0} {1}"" from the request body as JSON.";
-
-        private const string ParameterBindingFailedLogMessage = @"Failed to bind parameter ""{ParameterType} {ParameterName}"" from ""{SourceValue}"".";
-        private const string ParameterBindingFailedExceptionMessage = @"Failed to bind parameter ""{0} {1}"" from ""{2}"".";
-
-        private const string RequiredParameterNotProvidedLogMessage = @"Required parameter ""{ParameterType} {ParameterName}"" was not provided from {Source}.";
-        private const string RequiredParameterNotProvidedExceptionMessage = @"Required parameter ""{0} {1}"" was not provided from {2}.";
-
-        private const string UnexpectedJsonContentTypeLogMessage = @"Expected a supported JSON media type but got ""{ContentType}"".";
-        private const string UnexpectedJsonContentTypeExceptionMessage = @"Expected a supported JSON media type but got ""{0}"".";
-
-        private const string ImplicitBodyNotProvidedLogMessage = @"Implicit body inferred for parameter ""{ParameterName}"" but no body was provided. Did you mean to use a Service instead?";
-        private const string ImplicitBodyNotProvidedExceptionMessage = @"Implicit body inferred for parameter ""{0}"" but no body was provided. Did you mean to use a Service instead?";
-
-        private const string InvalidFormRequestBodyMessage = @"Failed to read parameter ""{ParameterType} {ParameterName}"" from the request body as form.";
-        private const string InvalidFormRequestBodyExceptionMessage = @"Failed to read parameter ""{0} {1}"" from the request body as form.";
-
-        private const string UnexpectedFormContentTypeLogMessage = @"Expected a supported form media type but got ""{ContentType}"".";
-        private const string UnexpectedFormContentTypeExceptionMessage = @"Expected a supported form media type but got ""{0}"".";
-
         // This doesn't take a shouldThrow parameter because an IOException indicates an aborted request rather than a "bad" request so
         // a BadHttpRequestException feels wrong. The client shouldn't be able to read the Developer Exception Page at any rate.
         public static void RequestBodyIOException(HttpContext httpContext, IOException exception)
             => RequestBodyIOException(GetLogger(httpContext), exception);
 
-        [LoggerMessage(1, LogLevel.Debug, "Reading the request body failed with an IOException.", EventName = "RequestBodyIOException")]
+        [LoggerMessage(RequestDelegateCreationLogging.RequestBodyIOExceptionEventId, LogLevel.Debug, RequestDelegateCreationLogging.RequestBodyIOExceptionMessage, EventName = RequestDelegateCreationLogging.RequestBodyIOExceptionEventName)]
         private static partial void RequestBodyIOException(ILogger logger, IOException exception);
 
         public static void InvalidJsonRequestBody(HttpContext httpContext, string parameterTypeName, string parameterName, Exception exception, bool shouldThrow)
         {
             if (shouldThrow)
             {
-                var message = string.Format(CultureInfo.InvariantCulture, InvalidJsonRequestBodyExceptionMessage, parameterTypeName, parameterName);
+                var message = string.Format(CultureInfo.InvariantCulture, RequestDelegateCreationLogging.InvalidJsonRequestBodyExceptionMessage, parameterTypeName, parameterName);
                 throw new BadHttpRequestException(message, exception);
             }
 
             InvalidJsonRequestBody(GetLogger(httpContext), parameterTypeName, parameterName, exception);
         }
 
-        [LoggerMessage(2, LogLevel.Debug, InvalidJsonRequestBodyMessage, EventName = "InvalidJsonRequestBody")]
+        [LoggerMessage(RequestDelegateCreationLogging.InvalidJsonRequestBodyEventId, LogLevel.Debug, RequestDelegateCreationLogging.InvalidJsonRequestBodyLogMessage, EventName = RequestDelegateCreationLogging.InvalidJsonRequestBodyEventName)]
         private static partial void InvalidJsonRequestBody(ILogger logger, string parameterType, string parameterName, Exception exception);
 
         public static void ParameterBindingFailed(HttpContext httpContext, string parameterTypeName, string parameterName, string sourceValue, bool shouldThrow)
         {
             if (shouldThrow)
             {
-                var message = string.Format(CultureInfo.InvariantCulture, ParameterBindingFailedExceptionMessage, parameterTypeName, parameterName, sourceValue);
+                var message = string.Format(CultureInfo.InvariantCulture, RequestDelegateCreationLogging.ParameterBindingFailedExceptionMessage, parameterTypeName, parameterName, sourceValue);
                 throw new BadHttpRequestException(message);
             }
 
             ParameterBindingFailed(GetLogger(httpContext), parameterTypeName, parameterName, sourceValue);
         }
 
-        [LoggerMessage(3, LogLevel.Debug, ParameterBindingFailedLogMessage, EventName = "ParameterBindingFailed")]
+        [LoggerMessage(RequestDelegateCreationLogging.ParameterBindingFailedEventId, LogLevel.Debug, RequestDelegateCreationLogging.ParameterBindingFailedLogMessage, EventName = RequestDelegateCreationLogging.ParameterBindingFailedEventName)]
         private static partial void ParameterBindingFailed(ILogger logger, string parameterType, string parameterName, string sourceValue);
 
         public static void RequiredParameterNotProvided(HttpContext httpContext, string parameterTypeName, string parameterName, string source, bool shouldThrow)
         {
             if (shouldThrow)
             {
-                var message = string.Format(CultureInfo.InvariantCulture, RequiredParameterNotProvidedExceptionMessage, parameterTypeName, parameterName, source);
+                var message = string.Format(CultureInfo.InvariantCulture, RequestDelegateCreationLogging.RequiredParameterNotProvidedExceptionMessage, parameterTypeName, parameterName, source);
                 throw new BadHttpRequestException(message);
             }
 
             RequiredParameterNotProvided(GetLogger(httpContext), parameterTypeName, parameterName, source);
         }
 
-        [LoggerMessage(4, LogLevel.Debug, RequiredParameterNotProvidedLogMessage, EventName = "RequiredParameterNotProvided")]
+        [LoggerMessage(RequestDelegateCreationLogging.RequiredParameterNotProvidedEventId, LogLevel.Debug, RequestDelegateCreationLogging.RequiredParameterNotProvidedLogMessage, EventName = RequestDelegateCreationLogging.RequiredParameterNotProvidedEventName)]
         private static partial void RequiredParameterNotProvided(ILogger logger, string parameterType, string parameterName, string source);
 
         public static void ImplicitBodyNotProvided(HttpContext httpContext, string parameterName, bool shouldThrow)
         {
             if (shouldThrow)
             {
-                var message = string.Format(CultureInfo.InvariantCulture, ImplicitBodyNotProvidedExceptionMessage, parameterName);
+                var message = string.Format(CultureInfo.InvariantCulture, RequestDelegateCreationLogging.ImplicitBodyNotProvidedExceptionMessage, parameterName);
                 throw new BadHttpRequestException(message);
             }
 
             ImplicitBodyNotProvided(GetLogger(httpContext), parameterName);
         }
 
-        [LoggerMessage(5, LogLevel.Debug, ImplicitBodyNotProvidedLogMessage, EventName = "ImplicitBodyNotProvided")]
+        [LoggerMessage(RequestDelegateCreationLogging.ImplicitBodyNotProvidedEventId, LogLevel.Debug, RequestDelegateCreationLogging.ImplicitBodyNotProvidedLogMessage, EventName = RequestDelegateCreationLogging.ImplicitBodyNotProvidedEventName)]
         private static partial void ImplicitBodyNotProvided(ILogger logger, string parameterName);
 
         public static void UnexpectedJsonContentType(HttpContext httpContext, string? contentType, bool shouldThrow)
         {
             if (shouldThrow)
             {
-                var message = string.Format(CultureInfo.InvariantCulture, UnexpectedJsonContentTypeExceptionMessage, contentType);
+                var message = string.Format(CultureInfo.InvariantCulture, RequestDelegateCreationLogging.UnexpectedJsonContentTypeExceptionMessage, contentType);
                 throw new BadHttpRequestException(message, StatusCodes.Status415UnsupportedMediaType);
             }
 
             UnexpectedJsonContentType(GetLogger(httpContext), contentType ?? "(none)");
         }
 
-        [LoggerMessage(6, LogLevel.Debug, UnexpectedJsonContentTypeLogMessage, EventName = "UnexpectedContentType")]
+        [LoggerMessage(RequestDelegateCreationLogging.UnexpectedJsonContentTypeEventId, LogLevel.Debug, RequestDelegateCreationLogging.UnexpectedJsonContentTypeLogMessage, EventName = RequestDelegateCreationLogging.UnexpectedJsonContentTypeEventName)]
         private static partial void UnexpectedJsonContentType(ILogger logger, string contentType);
 
         public static void UnexpectedNonFormContentType(HttpContext httpContext, string? contentType, bool shouldThrow)
         {
             if (shouldThrow)
             {
-                var message = string.Format(CultureInfo.InvariantCulture, UnexpectedFormContentTypeExceptionMessage, contentType);
+                var message = string.Format(CultureInfo.InvariantCulture, RequestDelegateCreationLogging.UnexpectedFormContentTypeExceptionMessage, contentType);
                 throw new BadHttpRequestException(message, StatusCodes.Status415UnsupportedMediaType);
             }
 
             UnexpectedNonFormContentType(GetLogger(httpContext), contentType ?? "(none)");
         }
 
-        [LoggerMessage(7, LogLevel.Debug, UnexpectedFormContentTypeLogMessage, EventName = "UnexpectedNonFormContentType")]
+        [LoggerMessage(RequestDelegateCreationLogging.UnexpectedFormContentTypeEventId, LogLevel.Debug, RequestDelegateCreationLogging.UnexpectedFormContentTypeLogMessage, EventName = RequestDelegateCreationLogging.UnexpectedFormContentTypeLogEventName)]
         private static partial void UnexpectedNonFormContentType(ILogger logger, string contentType);
 
         public static void InvalidFormRequestBody(HttpContext httpContext, string parameterTypeName, string parameterName, Exception exception, bool shouldThrow)
         {
             if (shouldThrow)
             {
-                var message = string.Format(CultureInfo.InvariantCulture, InvalidFormRequestBodyExceptionMessage, parameterTypeName, parameterName);
+                var message = string.Format(CultureInfo.InvariantCulture, RequestDelegateCreationLogging.InvalidFormRequestBodyExceptionMessage, parameterTypeName, parameterName);
                 throw new BadHttpRequestException(message, exception);
             }
 
             InvalidFormRequestBody(GetLogger(httpContext), parameterTypeName, parameterName, exception);
         }
 
-        [LoggerMessage(8, LogLevel.Debug, InvalidFormRequestBodyMessage, EventName = "InvalidFormRequestBody")]
+        [LoggerMessage(RequestDelegateCreationLogging.InvalidFormRequestBodyEventId, LogLevel.Debug, RequestDelegateCreationLogging.InvalidFormRequestBodyLogMessage, EventName = RequestDelegateCreationLogging.InvalidFormRequestBodyEventName)]
         private static partial void InvalidFormRequestBody(ILogger logger, string parameterType, string parameterName, Exception exception);
 
         private static ILogger GetLogger(HttpContext httpContext)

+ 11 - 840
src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs

@@ -768,48 +768,6 @@ public partial class RequestDelegateFactoryTests : LoggedTest
         Assert.Equal(new StringValues(new[] { "7", "8", "9" }), httpContext.Items["form"]!);
     }
 
-    [Fact]
-    public async Task RequestDelegateLogsStringValuesFromExplicitQueryStringSourceForUnpresentedValuesFailuresAsDebugAndSets400Response()
-    {
-        var invoked = false;
-
-        var httpContext = CreateHttpContext();
-        httpContext.Request.Form = new FormCollection(null);
-
-        var factoryResult = RequestDelegateFactory.Create((HttpContext context,
-                [FromHeader(Name = "foo")] StringValues headerValues,
-                [FromQuery(Name = "bar")] StringValues queryValues,
-                [FromForm(Name = "form")] StringValues formValues) =>
-        {
-            invoked = true;
-        });
-
-        var requestDelegate = factoryResult.RequestDelegate;
-
-        await requestDelegate(httpContext);
-
-        Assert.False(invoked);
-        Assert.False(httpContext.RequestAborted.IsCancellationRequested);
-        Assert.Equal(400, httpContext.Response.StatusCode);
-        Assert.False(httpContext.Response.HasStarted);
-
-        var logs = TestSink.Writes.ToArray();
-
-        Assert.Equal(3, logs.Length);
-
-        Assert.Equal(new EventId(4, "RequiredParameterNotProvided"), logs[0].EventId);
-        Assert.Equal(LogLevel.Debug, logs[0].LogLevel);
-        Assert.Equal(@"Required parameter ""StringValues headerValues"" was not provided from header.", logs[0].Message);
-
-        Assert.Equal(new EventId(4, "RequiredParameterNotProvided"), logs[1].EventId);
-        Assert.Equal(LogLevel.Debug, logs[1].LogLevel);
-        Assert.Equal(@"Required parameter ""StringValues queryValues"" was not provided from query string.", logs[1].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);
-    }
-
     [Fact]
     public async Task RequestDelegateHandlesNullableStringValuesFromExplicitQueryStringSourceForUnpresentedValues()
     {
@@ -932,111 +890,6 @@ public partial class RequestDelegateFactoryTests : LoggedTest
         Assert.Equal("Encountered a parameter of type 'System.Runtime.CompilerServices.Closure' without a name. Parameters must have a name.", ex.Message);
     }
 
-    [Fact]
-    public async Task RequestDelegateLogsTryParsableFailuresAsDebugAndSets400Response()
-    {
-        var invoked = false;
-
-        void TestAction([FromRoute] int tryParsable, [FromRoute] int tryParsable2)
-        {
-            invoked = true;
-        }
-
-        var httpContext = CreateHttpContext();
-        httpContext.Request.RouteValues["tryParsable"] = "invalid!";
-        httpContext.Request.RouteValues["tryParsable2"] = "invalid again!";
-
-        var factoryResult = RequestDelegateFactory.Create(TestAction);
-        var requestDelegate = factoryResult.RequestDelegate;
-
-        await requestDelegate(httpContext);
-
-        Assert.False(invoked);
-        Assert.False(httpContext.RequestAborted.IsCancellationRequested);
-        Assert.Equal(400, httpContext.Response.StatusCode);
-        Assert.False(httpContext.Response.HasStarted);
-
-        var logs = TestSink.Writes.ToArray();
-
-        Assert.Equal(2, logs.Length);
-
-        Assert.Equal(new EventId(3, "ParameterBindingFailed"), logs[0].EventId);
-        Assert.Equal(LogLevel.Debug, logs[0].LogLevel);
-        Assert.Equal(@"Failed to bind parameter ""int tryParsable"" from ""invalid!"".", logs[0].Message);
-
-        Assert.Equal(new EventId(3, "ParameterBindingFailed"), logs[1].EventId);
-        Assert.Equal(LogLevel.Debug, logs[1].LogLevel);
-        Assert.Equal(@"Failed to bind parameter ""int tryParsable2"" from ""invalid again!"".", logs[1].Message);
-    }
-
-    [Fact]
-    public async Task RequestDelegateThrowsForTryParsableFailuresIfThrowOnBadRequest()
-    {
-        var invoked = false;
-
-        void TestAction([FromRoute] int tryParsable, [FromRoute] int tryParsable2)
-        {
-            invoked = true;
-        }
-
-        var httpContext = CreateHttpContext();
-        httpContext.Request.RouteValues["tryParsable"] = "invalid!";
-        httpContext.Request.RouteValues["tryParsable2"] = "invalid again!";
-
-        var factoryResult = RequestDelegateFactory.Create(TestAction, new() { ThrowOnBadRequest = true });
-        var requestDelegate = factoryResult.RequestDelegate;
-
-        var badHttpRequestException = await Assert.ThrowsAsync<BadHttpRequestException>(() => requestDelegate(httpContext));
-
-        Assert.False(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 bind parameter ""int tryParsable"" from ""invalid!"".", badHttpRequestException.Message);
-        Assert.Equal(400, badHttpRequestException.StatusCode);
-    }
-
-    [Fact]
-    public async Task RequestDelegateThrowsForTryParsableFailuresIfThrowOnBadRequestWithArrays()
-    {
-        var invoked = false;
-
-        void TestAction([FromQuery] int[] values)
-        {
-            invoked = true;
-        }
-
-        var httpContext = CreateHttpContext();
-        httpContext.Request.Query = new QueryCollection(new Dictionary<string, StringValues>()
-        {
-            ["values"] = new(new[] { "1", "NAN", "3" })
-        });
-
-        var factoryResult = RequestDelegateFactory.Create(TestAction, new() { ThrowOnBadRequest = true, DisableInferBodyFromParameters = true });
-        var requestDelegate = factoryResult.RequestDelegate;
-
-        var badHttpRequestException = await Assert.ThrowsAsync<BadHttpRequestException>(() => requestDelegate(httpContext));
-
-        Assert.False(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 bind parameter ""int[] values"" from ""NAN"".", badHttpRequestException.Message);
-        Assert.Equal(400, badHttpRequestException.StatusCode);
-    }
-
     [Fact]
     public async Task RequestDelegateThrowsForTryParsableFailuresIfThrowOnBadRequestWithNonOptionalArrays()
     {
@@ -1072,130 +925,6 @@ public partial class RequestDelegateFactoryTests : LoggedTest
         Assert.Equal(400, badHttpRequestException.StatusCode);
     }
 
-    [Fact]
-    public async Task RequestDelegateLogsBindAsyncFailuresAndSets400Response()
-    {
-        // Not supplying any headers will cause the HttpContext BindAsync overload to return null.
-        var httpContext = CreateHttpContext();
-        var invoked = false;
-
-        var factoryResult = RequestDelegateFactory.Create((MyBindAsyncRecord myBindAsyncRecord1, MyBindAsyncRecord myBindAsyncRecord2) =>
-        {
-            invoked = true;
-        });
-
-        var requestDelegate = factoryResult.RequestDelegate;
-        await requestDelegate(httpContext);
-
-        Assert.False(invoked);
-        Assert.False(httpContext.RequestAborted.IsCancellationRequested);
-        Assert.Equal(400, httpContext.Response.StatusCode);
-
-        var logs = TestSink.Writes.ToArray();
-
-        Assert.Equal(2, logs.Length);
-
-        Assert.Equal(new EventId(4, "RequiredParameterNotProvided"), logs[0].EventId);
-        Assert.Equal(LogLevel.Debug, logs[0].LogLevel);
-        Assert.Equal(@"Required parameter ""MyBindAsyncRecord myBindAsyncRecord1"" was not provided from MyBindAsyncRecord.BindAsync(HttpContext, ParameterInfo).", logs[0].Message);
-
-        Assert.Equal(new EventId(4, "RequiredParameterNotProvided"), logs[1].EventId);
-        Assert.Equal(LogLevel.Debug, logs[1].LogLevel);
-        Assert.Equal(@"Required parameter ""MyBindAsyncRecord myBindAsyncRecord2"" was not provided from MyBindAsyncRecord.BindAsync(HttpContext, ParameterInfo).", logs[1].Message);
-    }
-
-    [Fact]
-    public async Task RequestDelegateThrowsForBindAsyncFailuresIfThrowOnBadRequest()
-    {
-        // Not supplying any headers will cause the HttpContext BindAsync overload to return null.
-        var httpContext = CreateHttpContext();
-        var invoked = false;
-
-        var factoryResult = RequestDelegateFactory.Create((MyBindAsyncRecord myBindAsyncRecord1, MyBindAsyncRecord myBindAsyncRecord2) =>
-        {
-            invoked = true;
-        }, new() { ThrowOnBadRequest = true });
-
-        var requestDelegate = factoryResult.RequestDelegate;
-        var badHttpRequestException = await Assert.ThrowsAsync<BadHttpRequestException>(() => requestDelegate(httpContext));
-
-        Assert.False(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(@"Required parameter ""MyBindAsyncRecord myBindAsyncRecord1"" was not provided from MyBindAsyncRecord.BindAsync(HttpContext, ParameterInfo).", badHttpRequestException.Message);
-        Assert.Equal(400, badHttpRequestException.StatusCode);
-    }
-
-    [Fact]
-    public async Task RequestDelegateLogsSingleArgBindAsyncFailuresAndSets400Response()
-    {
-        // Not supplying any headers will cause the HttpContext BindAsync overload to return null.
-        var httpContext = CreateHttpContext();
-        var invoked = false;
-
-        var factoryResult = RequestDelegateFactory.Create((MySimpleBindAsyncRecord mySimpleBindAsyncRecord1,
-            MySimpleBindAsyncRecord mySimpleBindAsyncRecord2) =>
-        {
-            invoked = true;
-        });
-
-        var requestDelegate = factoryResult.RequestDelegate;
-        await requestDelegate(httpContext);
-
-        Assert.False(invoked);
-        Assert.False(httpContext.RequestAborted.IsCancellationRequested);
-        Assert.Equal(400, httpContext.Response.StatusCode);
-
-        var logs = TestSink.Writes.ToArray();
-
-        Assert.Equal(2, logs.Length);
-
-        Assert.Equal(new EventId(4, "RequiredParameterNotProvided"), logs[0].EventId);
-        Assert.Equal(LogLevel.Debug, logs[0].LogLevel);
-        Assert.Equal(@"Required parameter ""MySimpleBindAsyncRecord mySimpleBindAsyncRecord1"" was not provided from MySimpleBindAsyncRecord.BindAsync(HttpContext).", logs[0].Message);
-
-        Assert.Equal(new EventId(4, "RequiredParameterNotProvided"), logs[1].EventId);
-        Assert.Equal(LogLevel.Debug, logs[1].LogLevel);
-        Assert.Equal(@"Required parameter ""MySimpleBindAsyncRecord mySimpleBindAsyncRecord2"" was not provided from MySimpleBindAsyncRecord.BindAsync(HttpContext).", logs[1].Message);
-    }
-
-    [Fact]
-    public async Task RequestDelegateThrowsForSingleArgBindAsyncFailuresIfThrowOnBadRequest()
-    {
-        // Not supplying any headers will cause the HttpContext BindAsync overload to return null.
-        var httpContext = CreateHttpContext();
-        var invoked = false;
-
-        var factoryResult = RequestDelegateFactory.Create((MySimpleBindAsyncRecord mySimpleBindAsyncRecord1,
-            MySimpleBindAsyncRecord mySimpleBindAsyncRecord2) =>
-        {
-            invoked = true;
-        }, new() { ThrowOnBadRequest = true });
-
-        var requestDelegate = factoryResult.RequestDelegate;
-        var badHttpRequestException = await Assert.ThrowsAsync<BadHttpRequestException>(() => requestDelegate(httpContext));
-
-        Assert.False(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(@"Required parameter ""MySimpleBindAsyncRecord mySimpleBindAsyncRecord1"" was not provided from MySimpleBindAsyncRecord.BindAsync(HttpContext).", badHttpRequestException.Message);
-        Assert.Equal(400, badHttpRequestException.StatusCode);
-    }
-
     [Fact]
     public async Task BindAsyncWithBodyArgument()
     {
@@ -1537,200 +1266,23 @@ public partial class RequestDelegateFactoryTests : LoggedTest
         }
     }
 
-    [Theory]
-    [InlineData(true)]
-    [InlineData(false)]
-    public async Task RequestDelegateLogsIOExceptionsAsDebugDoesNotAbortAndNeverThrows(bool throwOnBadRequests)
+    [Fact]
+    public void BuildRequestDelegateThrowsInvalidOperationExceptionGivenFromBodyOnMultipleParameters()
     {
-        var invoked = false;
-
-        void TestAction([FromBody] Todo todo)
-        {
-            invoked = true;
-        }
-
-        var ioException = new IOException();
-
-        var httpContext = CreateHttpContext();
-        httpContext.Request.Headers["Content-Type"] = "application/json";
-        httpContext.Request.Headers["Content-Length"] = "1";
-        httpContext.Request.Body = new ExceptionThrowingRequestBodyStream(ioException);
-        httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
-
-        var factoryResult = RequestDelegateFactory.Create(TestAction, new() { ThrowOnBadRequest = throwOnBadRequests });
-        var requestDelegate = factoryResult.RequestDelegate;
-
-        await requestDelegate(httpContext);
-
-        Assert.False(invoked);
-        Assert.False(httpContext.RequestAborted.IsCancellationRequested);
+        void TestAttributedInvalidAction([FromBody] int value1, [FromBody] int value2) { }
+        void TestInferredInvalidAction(Todo value1, Todo value2) { }
+        void TestBothInvalidAction(Todo value1, [FromBody] int value2) { }
 
-        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);
+        Assert.Throws<InvalidOperationException>(() => RequestDelegateFactory.Create(TestAttributedInvalidAction));
+        Assert.Throws<InvalidOperationException>(() => RequestDelegateFactory.Create(TestInferredInvalidAction));
+        Assert.Throws<InvalidOperationException>(() => RequestDelegateFactory.Create(TestBothInvalidAction));
     }
 
     [Fact]
-    public async Task RequestDelegateLogsJsonExceptionsAsDebugAndSets400Response()
+    public void BuildRequestDelegateThrowsInvalidOperationExceptionForInvalidTryParse()
     {
-        var invoked = false;
-
-        void TestAction([FromBody] Todo todo)
-        {
-            invoked = true;
-        }
-
-        var jsonException = new JsonException();
-
-        var httpContext = CreateHttpContext();
-        httpContext.Request.Headers["Content-Type"] = "application/json";
-        httpContext.Request.Headers["Content-Length"] = "1";
-        httpContext.Request.Body = new ExceptionThrowingRequestBodyStream(jsonException);
-        httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
-
-        var factoryResult = RequestDelegateFactory.Create(TestAction);
-        var requestDelegate = factoryResult.RequestDelegate;
-
-        await requestDelegate(httpContext);
-
-        Assert.False(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(2, "InvalidJsonRequestBody"), logMessage.EventId);
-        Assert.Equal(LogLevel.Debug, logMessage.LogLevel);
-        Assert.Equal(@"Failed to read parameter ""Todo todo"" from the request body as JSON.", logMessage.Message);
-        Assert.Same(jsonException, logMessage.Exception);
-    }
-
-    [Fact]
-    public async Task RequestDelegateThrowsForJsonExceptionsIfThrowOnBadRequest()
-    {
-        var invoked = false;
-
-        void TestAction([FromBody] Todo todo)
-        {
-            invoked = true;
-        }
-
-        var jsonException = new JsonException();
-
-        var httpContext = CreateHttpContext();
-        httpContext.Request.Headers["Content-Type"] = "application/json";
-        httpContext.Request.Headers["Content-Length"] = "1";
-        httpContext.Request.Body = new ExceptionThrowingRequestBodyStream(jsonException);
-        httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
-
-        var factoryResult = RequestDelegateFactory.Create(TestAction, new() { ThrowOnBadRequest = true });
-        var requestDelegate = factoryResult.RequestDelegate;
-
-        var badHttpRequestException = await Assert.ThrowsAsync<BadHttpRequestException>(() => requestDelegate(httpContext));
-
-        Assert.False(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 ""Todo todo"" from the request body as JSON.", badHttpRequestException.Message);
-        Assert.Equal(400, badHttpRequestException.StatusCode);
-        Assert.Same(jsonException, badHttpRequestException.InnerException);
-    }
-
-    [Fact]
-    public async Task RequestDelegateLogsMalformedJsonAsDebugAndSets400Response()
-    {
-        var invoked = false;
-
-        void TestAction([FromBody] Todo todo)
-        {
-            invoked = true;
-        }
-
-        var httpContext = CreateHttpContext();
-        httpContext.Request.Headers["Content-Type"] = "application/json";
-        httpContext.Request.Headers["Content-Length"] = "1";
-        httpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("{"));
-        httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
-
-        var factoryResult = RequestDelegateFactory.Create(TestAction);
-        var requestDelegate = factoryResult.RequestDelegate;
-
-        await requestDelegate(httpContext);
-
-        Assert.False(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(2, "InvalidJsonRequestBody"), logMessage.EventId);
-        Assert.Equal(LogLevel.Debug, logMessage.LogLevel);
-        Assert.Equal(@"Failed to read parameter ""Todo todo"" from the request body as JSON.", logMessage.Message);
-        Assert.IsType<JsonException>(logMessage.Exception);
-    }
-
-    [Fact]
-    public async Task RequestDelegateThrowsForMalformedJsonIfThrowOnBadRequest()
-    {
-        var invoked = false;
-
-        void TestAction([FromBody] Todo todo)
-        {
-            invoked = true;
-        }
-
-        var httpContext = CreateHttpContext();
-        httpContext.Request.Headers["Content-Type"] = "application/json";
-        httpContext.Request.Headers["Content-Length"] = "1";
-        httpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("{"));
-        httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
-
-        var factoryResult = RequestDelegateFactory.Create(TestAction, new() { ThrowOnBadRequest = true });
-        var requestDelegate = factoryResult.RequestDelegate;
-
-        var badHttpRequestException = await Assert.ThrowsAsync<BadHttpRequestException>(() => requestDelegate(httpContext));
-
-        Assert.False(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 ""Todo todo"" from the request body as JSON.", badHttpRequestException.Message);
-        Assert.Equal(400, badHttpRequestException.StatusCode);
-        Assert.IsType<JsonException>(badHttpRequestException.InnerException);
-    }
-
-    [Fact]
-    public void BuildRequestDelegateThrowsInvalidOperationExceptionGivenFromBodyOnMultipleParameters()
-    {
-        void TestAttributedInvalidAction([FromBody] int value1, [FromBody] int value2) { }
-        void TestInferredInvalidAction(Todo value1, Todo value2) { }
-        void TestBothInvalidAction(Todo value1, [FromBody] int value2) { }
-
-        Assert.Throws<InvalidOperationException>(() => RequestDelegateFactory.Create(TestAttributedInvalidAction));
-        Assert.Throws<InvalidOperationException>(() => RequestDelegateFactory.Create(TestInferredInvalidAction));
-        Assert.Throws<InvalidOperationException>(() => RequestDelegateFactory.Create(TestBothInvalidAction));
-    }
-
-    [Fact]
-    public void BuildRequestDelegateThrowsInvalidOperationExceptionForInvalidTryParse()
-    {
-        void TestTryParseStruct(BadTryParseStruct value1) { }
-        void TestTryParseClass(BadTryParseClass value1) { }
+        void TestTryParseStruct(BadTryParseStruct value1) { }
+        void TestTryParseClass(BadTryParseClass value1) { }
 
         Assert.Throws<InvalidOperationException>(() => RequestDelegateFactory.Create(TestTryParseStruct));
         Assert.Throws<InvalidOperationException>(() => RequestDelegateFactory.Create(TestTryParseClass));
@@ -2848,77 +2400,6 @@ public partial class RequestDelegateFactoryTests : LoggedTest
         Assert.Equal("null", responseBody);
     }
 
-    public static IEnumerable<object?[]> QueryParamOptionalityData
-    {
-        get
-        {
-            string requiredQueryParam(string name) => $"Hello {name}!";
-            string defaultValueQueryParam(string name = "DefaultName") => $"Hello {name}!";
-            string nullableQueryParam(string? name) => $"Hello {name}!";
-            string requiredParseableQueryParam(int age) => $"Age: {age}";
-            string defaultValueParseableQueryParam(int age = 12) => $"Age: {age}";
-            string nullableQueryParseableParam(int? age) => $"Age: {age}";
-
-            return new List<object?[]>
-                {
-                    new object?[] { (Func<string, string>)requiredQueryParam, "name", null, true, null},
-                    new object?[] { (Func<string, string>)requiredQueryParam, "name", "TestName", false, "Hello TestName!" },
-                    new object?[] { (Func<string, string>)defaultValueQueryParam, "name", null, false, "Hello DefaultName!" },
-                    new object?[] { (Func<string, string>)defaultValueQueryParam, "name", "TestName", false, "Hello TestName!" },
-                    new object?[] { (Func<string?, string>)nullableQueryParam, "name", null, false, "Hello !" },
-                    new object?[] { (Func<string?, string>)nullableQueryParam, "name", "TestName", false, "Hello TestName!"},
-
-                    new object?[] { (Func<int, string>)requiredParseableQueryParam, "age", null, true, null},
-                    new object?[] { (Func<int, string>)requiredParseableQueryParam, "age", "42", false, "Age: 42" },
-                    new object?[] { (Func<int, string>)defaultValueParseableQueryParam, "age", null, false, "Age: 12" },
-                    new object?[] { (Func<int, string>)defaultValueParseableQueryParam, "age", "42", false, "Age: 42" },
-                    new object?[] { (Func<int?, string>)nullableQueryParseableParam, "age", null, false, "Age: " },
-                    new object?[] { (Func<int?, string>)nullableQueryParseableParam, "age", "42", false, "Age: 42"},
-                };
-        }
-    }
-
-    [Theory]
-    [MemberData(nameof(QueryParamOptionalityData))]
-    public async Task RequestDelegateHandlesQueryParamOptionality(Delegate @delegate, string paramName, string? queryParam, bool isInvalid, string? expectedResponse)
-    {
-        var httpContext = CreateHttpContext();
-        var responseBodyStream = new MemoryStream();
-        httpContext.Response.Body = responseBodyStream;
-
-        if (queryParam is not null)
-        {
-            httpContext.Request.Query = new QueryCollection(new Dictionary<string, StringValues>
-            {
-                [paramName] = queryParam
-            });
-        }
-
-        var factoryResult = RequestDelegateFactory.Create(@delegate);
-        var requestDelegate = factoryResult.RequestDelegate;
-
-        await requestDelegate(httpContext);
-
-        var logs = TestSink.Writes.ToArray();
-
-        if (isInvalid)
-        {
-            Assert.Equal(400, httpContext.Response.StatusCode);
-            var log = Assert.Single(logs);
-            Assert.Equal(LogLevel.Debug, log.LogLevel);
-            Assert.Equal(new EventId(4, "RequiredParameterNotProvided"), log.EventId);
-            var expectedType = paramName == "age" ? "int age" : $"string name";
-            Assert.Equal($@"Required parameter ""{expectedType}"" was not provided from route or query string.", log.Message);
-        }
-        else
-        {
-            Assert.Equal(200, httpContext.Response.StatusCode);
-            Assert.False(httpContext.RequestAborted.IsCancellationRequested);
-            var decodedResponseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray());
-            Assert.Equal(expectedResponse, decodedResponseBody);
-        }
-    }
-
     public static IEnumerable<object?[]> RouteParamOptionalityData
     {
         get
@@ -2991,77 +2472,6 @@ public partial class RequestDelegateFactoryTests : LoggedTest
         }
     }
 
-    public static IEnumerable<object?[]> BodyParamOptionalityData
-    {
-        get
-        {
-            string requiredBodyParam(Todo todo) => $"Todo: {todo.Name}";
-            string defaultValueBodyParam(Todo? todo = null) => $"Todo: {todo?.Name}";
-            string nullableBodyParam(Todo? todo) => $"Todo: {todo?.Name}";
-
-            return new List<object?[]>
-                {
-                    new object?[] { (Func<Todo, string>)requiredBodyParam, false, true, null },
-                    new object?[] { (Func<Todo, string>)requiredBodyParam, true, false, "Todo: Default Todo"},
-                    new object?[] { (Func<Todo, string>)defaultValueBodyParam, false, false, "Todo: "},
-                    new object?[] { (Func<Todo, string>)defaultValueBodyParam, true, false, "Todo: Default Todo"},
-                    new object?[] { (Func<Todo?, string>)nullableBodyParam, false, false, "Todo: " },
-                    new object?[] { (Func<Todo?, string>)nullableBodyParam, true, false, "Todo: Default Todo" },
-                };
-        }
-    }
-
-    [Theory]
-    [MemberData(nameof(BodyParamOptionalityData))]
-    public async Task RequestDelegateHandlesBodyParamOptionality(Delegate @delegate, bool hasBody, bool isInvalid, string? expectedResponse)
-    {
-        var httpContext = CreateHttpContext();
-        var responseBodyStream = new MemoryStream();
-        httpContext.Response.Body = responseBodyStream;
-
-        if (hasBody)
-        {
-            var todo = new Todo() { Name = "Default Todo" };
-            var requestBodyBytes = JsonSerializer.SerializeToUtf8Bytes(todo);
-            var stream = new MemoryStream(requestBodyBytes);
-            httpContext.Request.Body = stream;
-            httpContext.Request.Headers["Content-Type"] = "application/json";
-            httpContext.Request.ContentLength = stream.Length;
-            httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
-        }
-
-        var jsonOptions = new JsonOptions();
-        jsonOptions.SerializerOptions.Converters.Add(new TodoJsonConverter());
-
-        var serviceCollection = new ServiceCollection();
-        serviceCollection.AddSingleton(LoggerFactory);
-        serviceCollection.AddSingleton(Options.Create(jsonOptions));
-        httpContext.RequestServices = serviceCollection.BuildServiceProvider();
-
-        var factoryResult = RequestDelegateFactory.Create(@delegate);
-        var requestDelegate = factoryResult.RequestDelegate;
-
-        var request = requestDelegate(httpContext);
-
-        if (isInvalid)
-        {
-            var logs = TestSink.Writes.ToArray();
-            Assert.Equal(400, httpContext.Response.StatusCode);
-            var log = Assert.Single(logs);
-            Assert.Equal(LogLevel.Debug, log.LogLevel);
-            Assert.Equal(new EventId(5, "ImplicitBodyNotProvided"), log.EventId);
-            Assert.Equal(@"Implicit body inferred for parameter ""todo"" but no body was provided. Did you mean to use a Service instead?", log.Message);
-        }
-        else
-        {
-            await request;
-            Assert.Equal(200, httpContext.Response.StatusCode);
-            Assert.False(httpContext.RequestAborted.IsCancellationRequested);
-            var decodedResponseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray());
-            Assert.Equal(expectedResponse, decodedResponseBody);
-        }
-    }
-
     public static IEnumerable<object?[]> BindAsyncParamOptionalityData
     {
         get
@@ -3228,54 +2638,6 @@ public partial class RequestDelegateFactoryTests : LoggedTest
             Assert.False(httpContext.RequestAborted.IsCancellationRequested);
         }
     }
-
-    public static IEnumerable<object?[]> AllowEmptyData
-    {
-        get
-        {
-            string disallowEmptyAndNonOptional([FromBody(AllowEmpty = false)] Todo todo) => $"{todo}";
-            string allowEmptyAndNonOptional([FromBody(AllowEmpty = true)] Todo todo) => $"{todo}";
-            string allowEmptyAndOptional([FromBody(AllowEmpty = true)] Todo? todo = null) => $"{todo}";
-            string disallowEmptyAndOptional([FromBody(AllowEmpty = false)] Todo? todo = null) => $"{todo}";
-
-            return new List<object?[]>
-                {
-                    new object?[] { (Func<Todo, string>)disallowEmptyAndNonOptional, false },
-                    new object?[] { (Func<Todo, string>)allowEmptyAndNonOptional, true },
-                    new object?[] { (Func<Todo, string>)allowEmptyAndOptional, true },
-                    new object?[] { (Func<Todo, string>)disallowEmptyAndOptional, true }
-                };
-        }
-    }
-
-    [Theory]
-    [MemberData(nameof(AllowEmptyData))]
-    public async Task AllowEmptyOverridesOptionality(Delegate @delegate, bool allowsEmptyRequest)
-    {
-        var httpContext = CreateHttpContext();
-
-        var factoryResult = RequestDelegateFactory.Create(@delegate);
-        var requestDelegate = factoryResult.RequestDelegate;
-
-        await requestDelegate(httpContext);
-
-        var logs = TestSink.Writes.ToArray();
-
-        if (!allowsEmptyRequest)
-        {
-            Assert.Equal(400, httpContext.Response.StatusCode);
-            var log = Assert.Single(logs);
-            Assert.Equal(LogLevel.Debug, log.LogLevel);
-            Assert.Equal(new EventId(4, "RequiredParameterNotProvided"), log.EventId);
-            Assert.Equal(@"Required parameter ""Todo todo"" was not provided from body.", log.Message);
-        }
-        else
-        {
-            Assert.Equal(200, httpContext.Response.StatusCode);
-            Assert.False(httpContext.RequestAborted.IsCancellationRequested);
-        }
-    }
-
 #nullable disable
 
     [Theory]
@@ -3395,93 +2757,6 @@ public partial class RequestDelegateFactoryTests : LoggedTest
         Assert.Equal(@"""Hello Tester. This is from an extension method.""", decodedResponseBody);
     }
 
-    [Theory]
-    [InlineData(true)]
-    [InlineData(false)]
-    public async Task RequestDelegateRejectsNonJsonContent(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, Todo todo) =>
-        {
-        }, 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 JSON 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(6, "UnexpectedContentType"), logMessage.EventId);
-            Assert.Equal(LogLevel.Debug, logMessage.LogLevel);
-            Assert.Equal("Expected a supported JSON media type but got \"application/xml\".", logMessage.Message);
-        }
-    }
-
-    [Theory]
-    [InlineData(true)]
-    [InlineData(false)]
-    public async Task RequestDelegateWithBindAndImplicitBodyRejectsNonJsonContent(bool shouldThrow)
-    {
-        Todo originalTodo = new()
-        {
-            Name = "Write more tests!"
-        };
-
-        var httpContext = new DefaultHttpContext();
-
-        var requestBodyBytes = JsonSerializer.SerializeToUtf8Bytes(originalTodo);
-        var stream = new MemoryStream(requestBodyBytes);
-        httpContext.Request.Body = stream;
-        httpContext.Request.Headers["Content-Type"] = "application/xml";
-        httpContext.Request.Headers["Content-Length"] = stream.Length.ToString(CultureInfo.InvariantCulture);
-        httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
-
-        var serviceCollection = new ServiceCollection();
-        serviceCollection.AddSingleton(LoggerFactory);
-        httpContext.RequestServices = serviceCollection.BuildServiceProvider();
-
-        var factoryResult = RequestDelegateFactory.Create((HttpContext context, JsonTodo customTodo, Todo todo) =>
-        {
-        }, 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 JSON 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(6, "UnexpectedContentType"), logMessage.EventId);
-            Assert.Equal(LogLevel.Debug, logMessage.LogLevel);
-            Assert.Equal("Expected a supported JSON media type but got \"application/xml\".", logMessage.Message);
-        }
-    }
-
     public static IEnumerable<object?[]> UriDelegates
     {
         get
@@ -3522,41 +2797,6 @@ public partial class RequestDelegateFactoryTests : LoggedTest
         Assert.Equal(expectedResponse, decodedResponseBody);
     }
 
-    [Theory]
-    [InlineData(true)]
-    [InlineData(false)]
-    public async Task RequestDelegateLogsIOExceptionsForFormAsDebugDoesNotAbortAndNeverThrows(bool throwOnBadRequests)
-    {
-        var invoked = false;
-
-        void TestAction(IFormFile file)
-        {
-            invoked = true;
-        }
-
-        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));
-
-        var factoryResult = RequestDelegateFactory.Create(TestAction, new() { ThrowOnBadRequest = throwOnBadRequests });
-        var requestDelegate = factoryResult.RequestDelegate;
-
-        await requestDelegate(httpContext);
-
-        Assert.False(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 RequestDelegateThrowsBadHttpRequestExceptionWhenReadingOversizeFormResultsIn413BadRequest()
     {
@@ -3623,75 +2863,6 @@ public partial class RequestDelegateFactoryTests : LoggedTest
         Assert.Equal(413, httpContext.Response.StatusCode);
     }
 
-    [Fact]
-    public async Task RequestDelegateLogsMalformedFormAsDebugAndSets400Response()
-    {
-        var invoked = false;
-
-        void TestAction(IFormFile file)
-        {
-            invoked = true;
-        }
-
-        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 factoryResult = RequestDelegateFactory.Create(TestAction);
-        var requestDelegate = factoryResult.RequestDelegate;
-
-        await requestDelegate(httpContext);
-
-        Assert.False(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 invoked = false;
-
-        void TestAction(IFormFile file)
-        {
-            invoked = true;
-        }
-
-        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 factoryResult = RequestDelegateFactory.Create(TestAction, new() { ThrowOnBadRequest = true });
-        var requestDelegate = factoryResult.RequestDelegate;
-
-        var badHttpRequestException = await Assert.ThrowsAsync<BadHttpRequestException>(() => requestDelegate(httpContext));
-
-        Assert.False(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 void BuildRequestDelegateThrowsInvalidOperationExceptionBodyAndFormParameters()
     {

+ 198 - 0
src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_BindAsync_Snapshot.generated.txt

@@ -334,6 +334,7 @@ namespace Microsoft.AspNetCore.Http.Generated
     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;
 
@@ -356,6 +357,8 @@ namespace Microsoft.AspNetCore.Http.Generated
                 {
                     var handler = (System.Func<global::Microsoft.AspNetCore.Http.HttpContext, global::Microsoft.AspNetCore.Http.Generators.Tests.MyBindAsyncRecord, global::System.String>)del;
                     EndpointFilterDelegate? filteredInvocation = null;
+                    var serviceProvider = options?.ServiceProvider ?? options?.EndpointBuilder?.ApplicationServices;
+                    var logOrThrowExceptionHelper = new LogOrThrowExceptionHelper(serviceProvider, options);
                     var parameters = del.Method.GetParameters();
 
                     if (options?.EndpointBuilder?.FilterFactories.Count > 0)
@@ -380,6 +383,7 @@ namespace Microsoft.AspNetCore.Http.Generated
                         global::Microsoft.AspNetCore.Http.Generators.Tests.MyBindAsyncRecord myBindAsyncParam_local;
                         if ((object?)myBindAsyncParam_temp == null)
                         {
+                            logOrThrowExceptionHelper.RequiredParameterNotProvided("MyBindAsyncRecord", "myBindAsyncParam", "MyBindAsyncRecord.BindAsync(HttpContext, ParameterInfo)");
                             wasParamCheckFailure = true;
                             myBindAsyncParam_local = default!;
                         }
@@ -406,6 +410,7 @@ namespace Microsoft.AspNetCore.Http.Generated
                         global::Microsoft.AspNetCore.Http.Generators.Tests.MyBindAsyncRecord myBindAsyncParam_local;
                         if ((object?)myBindAsyncParam_temp == null)
                         {
+                            logOrThrowExceptionHelper.RequiredParameterNotProvided("MyBindAsyncRecord", "myBindAsyncParam", "MyBindAsyncRecord.BindAsync(HttpContext, ParameterInfo)");
                             wasParamCheckFailure = true;
                             myBindAsyncParam_local = default!;
                         }
@@ -437,6 +442,8 @@ namespace Microsoft.AspNetCore.Http.Generated
                 {
                     var handler = (System.Func<global::Microsoft.AspNetCore.Http.Generators.Tests.MyBindAsyncRecord?, global::System.String>)del;
                     EndpointFilterDelegate? filteredInvocation = null;
+                    var serviceProvider = options?.ServiceProvider ?? options?.EndpointBuilder?.ApplicationServices;
+                    var logOrThrowExceptionHelper = new LogOrThrowExceptionHelper(serviceProvider, options);
                     var parameters = del.Method.GetParameters();
 
                     if (options?.EndpointBuilder?.FilterFactories.Count > 0)
@@ -498,6 +505,8 @@ namespace Microsoft.AspNetCore.Http.Generated
                 {
                     var handler = (System.Func<global::Microsoft.AspNetCore.Http.HttpContext, global::Microsoft.AspNetCore.Http.Generators.Tests.MyBindAsyncStruct, global::System.String>)del;
                     EndpointFilterDelegate? filteredInvocation = null;
+                    var serviceProvider = options?.ServiceProvider ?? options?.EndpointBuilder?.ApplicationServices;
+                    var logOrThrowExceptionHelper = new LogOrThrowExceptionHelper(serviceProvider, options);
                     var parameters = del.Method.GetParameters();
 
                     if (options?.EndpointBuilder?.FilterFactories.Count > 0)
@@ -522,6 +531,7 @@ namespace Microsoft.AspNetCore.Http.Generated
                         global::Microsoft.AspNetCore.Http.Generators.Tests.MyBindAsyncStruct myBindAsyncParam_local;
                         if ((object?)myBindAsyncParam_temp == null)
                         {
+                            logOrThrowExceptionHelper.RequiredParameterNotProvided("MyBindAsyncStruct", "myBindAsyncParam", "MyBindAsyncStruct.BindAsync(HttpContext, ParameterInfo)");
                             wasParamCheckFailure = true;
                             myBindAsyncParam_local = default!;
                         }
@@ -548,6 +558,7 @@ namespace Microsoft.AspNetCore.Http.Generated
                         global::Microsoft.AspNetCore.Http.Generators.Tests.MyBindAsyncStruct myBindAsyncParam_local;
                         if ((object?)myBindAsyncParam_temp == null)
                         {
+                            logOrThrowExceptionHelper.RequiredParameterNotProvided("MyBindAsyncStruct", "myBindAsyncParam", "MyBindAsyncStruct.BindAsync(HttpContext, ParameterInfo)");
                             wasParamCheckFailure = true;
                             myBindAsyncParam_local = default!;
                         }
@@ -579,6 +590,8 @@ namespace Microsoft.AspNetCore.Http.Generated
                 {
                     var handler = (System.Func<global::Microsoft.AspNetCore.Http.Generators.Tests.MyBindAsyncStruct?, global::System.String>)del;
                     EndpointFilterDelegate? filteredInvocation = null;
+                    var serviceProvider = options?.ServiceProvider ?? options?.EndpointBuilder?.ApplicationServices;
+                    var logOrThrowExceptionHelper = new LogOrThrowExceptionHelper(serviceProvider, options);
                     var parameters = del.Method.GetParameters();
 
                     if (options?.EndpointBuilder?.FilterFactories.Count > 0)
@@ -640,6 +653,8 @@ namespace Microsoft.AspNetCore.Http.Generated
                 {
                     var handler = (System.Func<global::Microsoft.AspNetCore.Http.HttpContext, global::Microsoft.AspNetCore.Http.Generators.Tests.MyNullableBindAsyncStruct, global::System.String>)del;
                     EndpointFilterDelegate? filteredInvocation = null;
+                    var serviceProvider = options?.ServiceProvider ?? options?.EndpointBuilder?.ApplicationServices;
+                    var logOrThrowExceptionHelper = new LogOrThrowExceptionHelper(serviceProvider, options);
                     var parameters = del.Method.GetParameters();
 
                     if (options?.EndpointBuilder?.FilterFactories.Count > 0)
@@ -664,6 +679,7 @@ namespace Microsoft.AspNetCore.Http.Generated
                         global::Microsoft.AspNetCore.Http.Generators.Tests.MyNullableBindAsyncStruct myBindAsyncParam_local;
                         if ((object?)myBindAsyncParam_temp == null)
                         {
+                            logOrThrowExceptionHelper.RequiredParameterNotProvided("MyNullableBindAsyncStruct", "myBindAsyncParam", "MyNullableBindAsyncStruct.BindAsync(HttpContext, ParameterInfo)");
                             wasParamCheckFailure = true;
                             myBindAsyncParam_local = default!;
                         }
@@ -690,6 +706,7 @@ namespace Microsoft.AspNetCore.Http.Generated
                         global::Microsoft.AspNetCore.Http.Generators.Tests.MyNullableBindAsyncStruct myBindAsyncParam_local;
                         if ((object?)myBindAsyncParam_temp == null)
                         {
+                            logOrThrowExceptionHelper.RequiredParameterNotProvided("MyNullableBindAsyncStruct", "myBindAsyncParam", "MyNullableBindAsyncStruct.BindAsync(HttpContext, ParameterInfo)");
                             wasParamCheckFailure = true;
                             myBindAsyncParam_local = default!;
                         }
@@ -721,6 +738,8 @@ namespace Microsoft.AspNetCore.Http.Generated
                 {
                     var handler = (System.Func<global::Microsoft.AspNetCore.Http.Generators.Tests.MyNullableBindAsyncStruct?, global::System.String>)del;
                     EndpointFilterDelegate? filteredInvocation = null;
+                    var serviceProvider = options?.ServiceProvider ?? options?.EndpointBuilder?.ApplicationServices;
+                    var logOrThrowExceptionHelper = new LogOrThrowExceptionHelper(serviceProvider, options);
                     var parameters = del.Method.GetParameters();
 
                     if (options?.EndpointBuilder?.FilterFactories.Count > 0)
@@ -782,6 +801,8 @@ namespace Microsoft.AspNetCore.Http.Generated
                 {
                     var handler = (System.Func<global::Microsoft.AspNetCore.Http.HttpContext, global::Microsoft.AspNetCore.Http.Generators.Tests.MyBothBindAsyncStruct, global::System.String>)del;
                     EndpointFilterDelegate? filteredInvocation = null;
+                    var serviceProvider = options?.ServiceProvider ?? options?.EndpointBuilder?.ApplicationServices;
+                    var logOrThrowExceptionHelper = new LogOrThrowExceptionHelper(serviceProvider, options);
                     var parameters = del.Method.GetParameters();
 
                     if (options?.EndpointBuilder?.FilterFactories.Count > 0)
@@ -806,6 +827,7 @@ namespace Microsoft.AspNetCore.Http.Generated
                         global::Microsoft.AspNetCore.Http.Generators.Tests.MyBothBindAsyncStruct myBindAsyncParam_local;
                         if ((object?)myBindAsyncParam_temp == null)
                         {
+                            logOrThrowExceptionHelper.RequiredParameterNotProvided("MyBothBindAsyncStruct", "myBindAsyncParam", "MyBothBindAsyncStruct.BindAsync(HttpContext, ParameterInfo)");
                             wasParamCheckFailure = true;
                             myBindAsyncParam_local = default!;
                         }
@@ -832,6 +854,7 @@ namespace Microsoft.AspNetCore.Http.Generated
                         global::Microsoft.AspNetCore.Http.Generators.Tests.MyBothBindAsyncStruct myBindAsyncParam_local;
                         if ((object?)myBindAsyncParam_temp == null)
                         {
+                            logOrThrowExceptionHelper.RequiredParameterNotProvided("MyBothBindAsyncStruct", "myBindAsyncParam", "MyBothBindAsyncStruct.BindAsync(HttpContext, ParameterInfo)");
                             wasParamCheckFailure = true;
                             myBindAsyncParam_local = default!;
                         }
@@ -863,6 +886,8 @@ namespace Microsoft.AspNetCore.Http.Generated
                 {
                     var handler = (System.Func<global::Microsoft.AspNetCore.Http.Generators.Tests.MyBothBindAsyncStruct?, global::System.String>)del;
                     EndpointFilterDelegate? filteredInvocation = null;
+                    var serviceProvider = options?.ServiceProvider ?? options?.EndpointBuilder?.ApplicationServices;
+                    var logOrThrowExceptionHelper = new LogOrThrowExceptionHelper(serviceProvider, options);
                     var parameters = del.Method.GetParameters();
 
                     if (options?.EndpointBuilder?.FilterFactories.Count > 0)
@@ -924,6 +949,8 @@ namespace Microsoft.AspNetCore.Http.Generated
                 {
                     var handler = (System.Func<global::Microsoft.AspNetCore.Http.HttpContext, global::Microsoft.AspNetCore.Http.Generators.Tests.MySimpleBindAsyncRecord, global::System.String>)del;
                     EndpointFilterDelegate? filteredInvocation = null;
+                    var serviceProvider = options?.ServiceProvider ?? options?.EndpointBuilder?.ApplicationServices;
+                    var logOrThrowExceptionHelper = new LogOrThrowExceptionHelper(serviceProvider, options);
 
                     if (options?.EndpointBuilder?.FilterFactories.Count > 0)
                     {
@@ -947,6 +974,7 @@ namespace Microsoft.AspNetCore.Http.Generated
                         global::Microsoft.AspNetCore.Http.Generators.Tests.MySimpleBindAsyncRecord myBindAsyncParam_local;
                         if ((object?)myBindAsyncParam_temp == null)
                         {
+                            logOrThrowExceptionHelper.RequiredParameterNotProvided("MySimpleBindAsyncRecord", "myBindAsyncParam", "MySimpleBindAsyncRecord.BindAsync(HttpContext)");
                             wasParamCheckFailure = true;
                             myBindAsyncParam_local = default!;
                         }
@@ -973,6 +1001,7 @@ namespace Microsoft.AspNetCore.Http.Generated
                         global::Microsoft.AspNetCore.Http.Generators.Tests.MySimpleBindAsyncRecord myBindAsyncParam_local;
                         if ((object?)myBindAsyncParam_temp == null)
                         {
+                            logOrThrowExceptionHelper.RequiredParameterNotProvided("MySimpleBindAsyncRecord", "myBindAsyncParam", "MySimpleBindAsyncRecord.BindAsync(HttpContext)");
                             wasParamCheckFailure = true;
                             myBindAsyncParam_local = default!;
                         }
@@ -1004,6 +1033,8 @@ namespace Microsoft.AspNetCore.Http.Generated
                 {
                     var handler = (System.Func<global::Microsoft.AspNetCore.Http.Generators.Tests.MySimpleBindAsyncRecord?, global::System.String>)del;
                     EndpointFilterDelegate? filteredInvocation = null;
+                    var serviceProvider = options?.ServiceProvider ?? options?.EndpointBuilder?.ApplicationServices;
+                    var logOrThrowExceptionHelper = new LogOrThrowExceptionHelper(serviceProvider, options);
 
                     if (options?.EndpointBuilder?.FilterFactories.Count > 0)
                     {
@@ -1064,6 +1095,8 @@ namespace Microsoft.AspNetCore.Http.Generated
                 {
                     var handler = (System.Func<global::Microsoft.AspNetCore.Http.HttpContext, global::Microsoft.AspNetCore.Http.Generators.Tests.MySimpleBindAsyncStruct, global::System.String>)del;
                     EndpointFilterDelegate? filteredInvocation = null;
+                    var serviceProvider = options?.ServiceProvider ?? options?.EndpointBuilder?.ApplicationServices;
+                    var logOrThrowExceptionHelper = new LogOrThrowExceptionHelper(serviceProvider, options);
 
                     if (options?.EndpointBuilder?.FilterFactories.Count > 0)
                     {
@@ -1087,6 +1120,7 @@ namespace Microsoft.AspNetCore.Http.Generated
                         global::Microsoft.AspNetCore.Http.Generators.Tests.MySimpleBindAsyncStruct myBindAsyncParam_local;
                         if ((object?)myBindAsyncParam_temp == null)
                         {
+                            logOrThrowExceptionHelper.RequiredParameterNotProvided("MySimpleBindAsyncStruct", "myBindAsyncParam", "MySimpleBindAsyncStruct.BindAsync(HttpContext)");
                             wasParamCheckFailure = true;
                             myBindAsyncParam_local = default!;
                         }
@@ -1113,6 +1147,7 @@ namespace Microsoft.AspNetCore.Http.Generated
                         global::Microsoft.AspNetCore.Http.Generators.Tests.MySimpleBindAsyncStruct myBindAsyncParam_local;
                         if ((object?)myBindAsyncParam_temp == null)
                         {
+                            logOrThrowExceptionHelper.RequiredParameterNotProvided("MySimpleBindAsyncStruct", "myBindAsyncParam", "MySimpleBindAsyncStruct.BindAsync(HttpContext)");
                             wasParamCheckFailure = true;
                             myBindAsyncParam_local = default!;
                         }
@@ -1144,6 +1179,8 @@ namespace Microsoft.AspNetCore.Http.Generated
                 {
                     var handler = (System.Func<global::Microsoft.AspNetCore.Http.Generators.Tests.MySimpleBindAsyncStruct?, global::System.String>)del;
                     EndpointFilterDelegate? filteredInvocation = null;
+                    var serviceProvider = options?.ServiceProvider ?? options?.EndpointBuilder?.ApplicationServices;
+                    var logOrThrowExceptionHelper = new LogOrThrowExceptionHelper(serviceProvider, options);
 
                     if (options?.EndpointBuilder?.FilterFactories.Count > 0)
                     {
@@ -1204,6 +1241,8 @@ namespace Microsoft.AspNetCore.Http.Generated
                 {
                     var handler = (System.Func<global::Microsoft.AspNetCore.Http.HttpContext, global::Microsoft.AspNetCore.Http.Generators.Tests.BindAsyncFromImplicitStaticAbstractInterface, global::System.String>)del;
                     EndpointFilterDelegate? filteredInvocation = null;
+                    var serviceProvider = options?.ServiceProvider ?? options?.EndpointBuilder?.ApplicationServices;
+                    var logOrThrowExceptionHelper = new LogOrThrowExceptionHelper(serviceProvider, options);
                     var parameters = del.Method.GetParameters();
 
                     if (options?.EndpointBuilder?.FilterFactories.Count > 0)
@@ -1228,6 +1267,7 @@ namespace Microsoft.AspNetCore.Http.Generated
                         global::Microsoft.AspNetCore.Http.Generators.Tests.BindAsyncFromImplicitStaticAbstractInterface myBindAsyncParam_local;
                         if ((object?)myBindAsyncParam_temp == null)
                         {
+                            logOrThrowExceptionHelper.RequiredParameterNotProvided("BindAsyncFromImplicitStaticAbstractInterface", "myBindAsyncParam", "BindAsyncFromImplicitStaticAbstractInterface.BindAsync(HttpContext, ParameterInfo)");
                             wasParamCheckFailure = true;
                             myBindAsyncParam_local = default!;
                         }
@@ -1254,6 +1294,7 @@ namespace Microsoft.AspNetCore.Http.Generated
                         global::Microsoft.AspNetCore.Http.Generators.Tests.BindAsyncFromImplicitStaticAbstractInterface myBindAsyncParam_local;
                         if ((object?)myBindAsyncParam_temp == null)
                         {
+                            logOrThrowExceptionHelper.RequiredParameterNotProvided("BindAsyncFromImplicitStaticAbstractInterface", "myBindAsyncParam", "BindAsyncFromImplicitStaticAbstractInterface.BindAsync(HttpContext, ParameterInfo)");
                             wasParamCheckFailure = true;
                             myBindAsyncParam_local = default!;
                         }
@@ -1285,6 +1326,8 @@ namespace Microsoft.AspNetCore.Http.Generated
                 {
                     var handler = (System.Func<global::Microsoft.AspNetCore.Http.Generators.Tests.BindAsyncFromImplicitStaticAbstractInterface?, global::System.String>)del;
                     EndpointFilterDelegate? filteredInvocation = null;
+                    var serviceProvider = options?.ServiceProvider ?? options?.EndpointBuilder?.ApplicationServices;
+                    var logOrThrowExceptionHelper = new LogOrThrowExceptionHelper(serviceProvider, options);
                     var parameters = del.Method.GetParameters();
 
                     if (options?.EndpointBuilder?.FilterFactories.Count > 0)
@@ -1346,6 +1389,8 @@ namespace Microsoft.AspNetCore.Http.Generated
                 {
                     var handler = (System.Func<global::Microsoft.AspNetCore.Http.HttpContext, global::Microsoft.AspNetCore.Http.Generators.Tests.InheritBindAsync, global::System.String>)del;
                     EndpointFilterDelegate? filteredInvocation = null;
+                    var serviceProvider = options?.ServiceProvider ?? options?.EndpointBuilder?.ApplicationServices;
+                    var logOrThrowExceptionHelper = new LogOrThrowExceptionHelper(serviceProvider, options);
 
                     if (options?.EndpointBuilder?.FilterFactories.Count > 0)
                     {
@@ -1369,6 +1414,7 @@ namespace Microsoft.AspNetCore.Http.Generated
                         global::Microsoft.AspNetCore.Http.Generators.Tests.InheritBindAsync myBindAsyncParam_local;
                         if ((object?)myBindAsyncParam_temp == null)
                         {
+                            logOrThrowExceptionHelper.RequiredParameterNotProvided("InheritBindAsync", "myBindAsyncParam", "InheritBindAsync.BindAsync(HttpContext)");
                             wasParamCheckFailure = true;
                             myBindAsyncParam_local = default!;
                         }
@@ -1395,6 +1441,7 @@ namespace Microsoft.AspNetCore.Http.Generated
                         global::Microsoft.AspNetCore.Http.Generators.Tests.InheritBindAsync myBindAsyncParam_local;
                         if ((object?)myBindAsyncParam_temp == null)
                         {
+                            logOrThrowExceptionHelper.RequiredParameterNotProvided("InheritBindAsync", "myBindAsyncParam", "InheritBindAsync.BindAsync(HttpContext)");
                             wasParamCheckFailure = true;
                             myBindAsyncParam_local = default!;
                         }
@@ -1426,6 +1473,8 @@ namespace Microsoft.AspNetCore.Http.Generated
                 {
                     var handler = (System.Func<global::Microsoft.AspNetCore.Http.Generators.Tests.InheritBindAsync?, global::System.String>)del;
                     EndpointFilterDelegate? filteredInvocation = null;
+                    var serviceProvider = options?.ServiceProvider ?? options?.EndpointBuilder?.ApplicationServices;
+                    var logOrThrowExceptionHelper = new LogOrThrowExceptionHelper(serviceProvider, options);
 
                     if (options?.EndpointBuilder?.FilterFactories.Count > 0)
                     {
@@ -1486,6 +1535,8 @@ namespace Microsoft.AspNetCore.Http.Generated
                 {
                     var handler = (System.Func<global::Microsoft.AspNetCore.Http.HttpContext, global::Microsoft.AspNetCore.Http.Generators.Tests.BindAsyncFromExplicitStaticAbstractInterface, global::System.String>)del;
                     EndpointFilterDelegate? filteredInvocation = null;
+                    var serviceProvider = options?.ServiceProvider ?? options?.EndpointBuilder?.ApplicationServices;
+                    var logOrThrowExceptionHelper = new LogOrThrowExceptionHelper(serviceProvider, options);
                     var parameters = del.Method.GetParameters();
 
                     if (options?.EndpointBuilder?.FilterFactories.Count > 0)
@@ -1510,6 +1561,7 @@ namespace Microsoft.AspNetCore.Http.Generated
                         global::Microsoft.AspNetCore.Http.Generators.Tests.BindAsyncFromExplicitStaticAbstractInterface myBindAsyncParam_local;
                         if ((object?)myBindAsyncParam_temp == null)
                         {
+                            logOrThrowExceptionHelper.RequiredParameterNotProvided("BindAsyncFromExplicitStaticAbstractInterface", "myBindAsyncParam", "BindAsyncFromExplicitStaticAbstractInterface.BindAsync(HttpContext, ParameterInfo)");
                             wasParamCheckFailure = true;
                             myBindAsyncParam_local = default!;
                         }
@@ -1536,6 +1588,7 @@ namespace Microsoft.AspNetCore.Http.Generated
                         global::Microsoft.AspNetCore.Http.Generators.Tests.BindAsyncFromExplicitStaticAbstractInterface myBindAsyncParam_local;
                         if ((object?)myBindAsyncParam_temp == null)
                         {
+                            logOrThrowExceptionHelper.RequiredParameterNotProvided("BindAsyncFromExplicitStaticAbstractInterface", "myBindAsyncParam", "BindAsyncFromExplicitStaticAbstractInterface.BindAsync(HttpContext, ParameterInfo)");
                             wasParamCheckFailure = true;
                             myBindAsyncParam_local = default!;
                         }
@@ -1567,6 +1620,8 @@ namespace Microsoft.AspNetCore.Http.Generated
                 {
                     var handler = (System.Func<global::Microsoft.AspNetCore.Http.Generators.Tests.BindAsyncFromExplicitStaticAbstractInterface?, global::System.String>)del;
                     EndpointFilterDelegate? filteredInvocation = null;
+                    var serviceProvider = options?.ServiceProvider ?? options?.EndpointBuilder?.ApplicationServices;
+                    var logOrThrowExceptionHelper = new LogOrThrowExceptionHelper(serviceProvider, options);
                     var parameters = del.Method.GetParameters();
 
                     if (options?.EndpointBuilder?.FilterFactories.Count > 0)
@@ -1672,4 +1727,147 @@ namespace Microsoft.AspNetCore.Http.Generated
         }
 
     }
+
+    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.");
+    }
 }

+ 173 - 7
src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitBodyParam_ComplexReturn_Snapshot.generated.txt

@@ -79,6 +79,7 @@ namespace Microsoft.AspNetCore.Http.Generated
     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;
 
@@ -101,6 +102,8 @@ namespace Microsoft.AspNetCore.Http.Generated
                 {
                     var handler = (System.Func<global::Microsoft.AspNetCore.Http.Generators.Tests.Todo, global::Microsoft.AspNetCore.Http.HttpResults.Ok<global::Microsoft.AspNetCore.Http.Generators.Tests.Todo>>)del;
                     EndpointFilterDelegate? filteredInvocation = null;
+                    var serviceProvider = options?.ServiceProvider ?? options?.EndpointBuilder?.ApplicationServices;
+                    var logOrThrowExceptionHelper = new LogOrThrowExceptionHelper(serviceProvider, options);
 
                     if (options?.EndpointBuilder?.FilterFactories.Count > 0)
                     {
@@ -120,7 +123,7 @@ namespace Microsoft.AspNetCore.Http.Generated
                     {
                         var wasParamCheckFailure = false;
                         // Endpoint Parameter: todo (Type = Microsoft.AspNetCore.Http.Generators.Tests.Todo, IsOptional = False, IsParsable = False, IsArray = False, Source = JsonBody)
-                        var todo_resolveBodyResult = await GeneratedRouteBuilderExtensionsCore.TryResolveBodyAsync<global::Microsoft.AspNetCore.Http.Generators.Tests.Todo>(httpContext, false);
+                        var todo_resolveBodyResult = await GeneratedRouteBuilderExtensionsCore.TryResolveBodyAsync<global::Microsoft.AspNetCore.Http.Generators.Tests.Todo>(httpContext, logOrThrowExceptionHelper, false, "Todo", "todo");
                         var todo_local = todo_resolveBodyResult.Item2;
                         if (!todo_resolveBodyResult.Item1)
                         {
@@ -140,7 +143,7 @@ namespace Microsoft.AspNetCore.Http.Generated
                     {
                         var wasParamCheckFailure = false;
                         // Endpoint Parameter: todo (Type = Microsoft.AspNetCore.Http.Generators.Tests.Todo, IsOptional = False, IsParsable = False, IsArray = False, Source = JsonBody)
-                        var todo_resolveBodyResult = await GeneratedRouteBuilderExtensionsCore.TryResolveBodyAsync<global::Microsoft.AspNetCore.Http.Generators.Tests.Todo>(httpContext, false);
+                        var todo_resolveBodyResult = await GeneratedRouteBuilderExtensionsCore.TryResolveBodyAsync<global::Microsoft.AspNetCore.Http.Generators.Tests.Todo>(httpContext, logOrThrowExceptionHelper, false, "Todo", "todo");
                         var todo_local = todo_resolveBodyResult.Item2;
                         if (!todo_resolveBodyResult.Item1)
                         {
@@ -170,6 +173,8 @@ namespace Microsoft.AspNetCore.Http.Generated
                 {
                     var handler = (System.Func<global::Microsoft.AspNetCore.Http.Generators.Tests.Todo?, global::Microsoft.AspNetCore.Http.HttpResults.Ok<global::Microsoft.AspNetCore.Http.Generators.Tests.Todo>>)del;
                     EndpointFilterDelegate? filteredInvocation = null;
+                    var serviceProvider = options?.ServiceProvider ?? options?.EndpointBuilder?.ApplicationServices;
+                    var logOrThrowExceptionHelper = new LogOrThrowExceptionHelper(serviceProvider, options);
 
                     if (options?.EndpointBuilder?.FilterFactories.Count > 0)
                     {
@@ -189,7 +194,7 @@ namespace Microsoft.AspNetCore.Http.Generated
                     {
                         var wasParamCheckFailure = false;
                         // Endpoint Parameter: todo (Type = Microsoft.AspNetCore.Http.Generators.Tests.Todo?, IsOptional = True, IsParsable = False, IsArray = False, Source = JsonBody)
-                        var todo_resolveBodyResult = await GeneratedRouteBuilderExtensionsCore.TryResolveBodyAsync<global::Microsoft.AspNetCore.Http.Generators.Tests.Todo?>(httpContext, true);
+                        var todo_resolveBodyResult = await GeneratedRouteBuilderExtensionsCore.TryResolveBodyAsync<global::Microsoft.AspNetCore.Http.Generators.Tests.Todo?>(httpContext, logOrThrowExceptionHelper, true, "Todo?", "todo");
                         var todo_local = todo_resolveBodyResult.Item2;
                         if (!todo_resolveBodyResult.Item1)
                         {
@@ -209,7 +214,7 @@ namespace Microsoft.AspNetCore.Http.Generated
                     {
                         var wasParamCheckFailure = false;
                         // Endpoint Parameter: todo (Type = Microsoft.AspNetCore.Http.Generators.Tests.Todo?, IsOptional = True, IsParsable = False, IsArray = False, Source = JsonBody)
-                        var todo_resolveBodyResult = await GeneratedRouteBuilderExtensionsCore.TryResolveBodyAsync<global::Microsoft.AspNetCore.Http.Generators.Tests.Todo?>(httpContext, true);
+                        var todo_resolveBodyResult = await GeneratedRouteBuilderExtensionsCore.TryResolveBodyAsync<global::Microsoft.AspNetCore.Http.Generators.Tests.Todo?>(httpContext, logOrThrowExceptionHelper, true, "Todo?", "todo");
                         var todo_local = todo_resolveBodyResult.Item2;
                         if (!todo_resolveBodyResult.Item1)
                         {
@@ -276,7 +281,7 @@ namespace Microsoft.AspNetCore.Http.Generated
             }
         }
 
-        private static async ValueTask<(bool, T?)> TryResolveBodyAsync<T>(HttpContext httpContext, bool allowEmpty)
+        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>();
 
@@ -284,6 +289,7 @@ namespace Microsoft.AspNetCore.Http.Generated
             {
                 if (!httpContext.Request.HasJsonContentType())
                 {
+                    logOrThrowExceptionHelper.UnexpectedJsonContentType(httpContext.Request.ContentType);
                     httpContext.Response.StatusCode = StatusCodes.Status415UnsupportedMediaType;
                     return (false, default);
                 }
@@ -292,17 +298,34 @@ namespace Microsoft.AspNetCore.Http.Generated
                     var bodyValue = await httpContext.Request.ReadFromJsonAsync<T>();
                     if (!allowEmpty && bodyValue == null)
                     {
+                        if (!isInferred)
+                        {
+                            logOrThrowExceptionHelper.RequiredParameterNotProvided(parameterTypeName, parameterName, "body");
+                        }
+                        else
+                        {
+                            logOrThrowExceptionHelper.ImplicitBodyNotProvided(parameterName);
+                        }
                         httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
                         return (false, bodyValue);
                     }
                     return (true, bodyValue);
                 }
-                catch (IOException)
+                catch (BadHttpRequestException badHttpRequestException)
                 {
+                    logOrThrowExceptionHelper.RequestBodyIOException(badHttpRequestException);
+                    httpContext.Response.StatusCode = badHttpRequestException.StatusCode;
                     return (false, default);
                 }
-                catch (System.Text.Json.JsonException)
+                catch (IOException ioException)
                 {
+                    logOrThrowExceptionHelper.RequestBodyIOException(ioException);
+                    httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
+                    return (false, default);
+                }
+                catch (System.Text.Json.JsonException jsonException)
+                {
+                    logOrThrowExceptionHelper.InvalidJsonRequestBody(parameterTypeName, parameterName, jsonException);
                     httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
                     return (false, default);
                 }
@@ -316,4 +339,147 @@ namespace Microsoft.AspNetCore.Http.Generated
         }
 
     }
+
+    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.");
+    }
 }

+ 155 - 2
src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitHeader_ComplexTypeArrayParam.generated.txt

@@ -79,6 +79,7 @@ namespace Microsoft.AspNetCore.Http.Generated
     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;
 
@@ -102,6 +103,7 @@ namespace Microsoft.AspNetCore.Http.Generated
                     var handler = (System.Func<global::Microsoft.AspNetCore.Http.Generators.Tests.ParsableTodo[], global::System.Int32>)del;
                     EndpointFilterDelegate? filteredInvocation = null;
                     var serviceProvider = options?.ServiceProvider ?? options?.EndpointBuilder?.ApplicationServices;
+                    var logOrThrowExceptionHelper = new LogOrThrowExceptionHelper(serviceProvider, options);
                     var serializerOptions = serviceProvider?.GetService<IOptions<JsonOptions>>()?.Value.SerializerOptions ?? new JsonOptions().SerializerOptions;
                     var jsonTypeInfo =  (JsonTypeInfo<global::System.Int32>)serializerOptions.GetTypeInfo(typeof(global::System.Int32));
 
@@ -131,7 +133,11 @@ namespace Microsoft.AspNetCore.Http.Generated
                             var element = p_temp[i];
                             if (!GeneratedRouteBuilderExtensionsCore.TryParseExplicit<global::Microsoft.AspNetCore.Http.Generators.Tests.ParsableTodo>(element!, CultureInfo.InvariantCulture, out var parsed_element))
                             {
-                                wasParamCheckFailure = true;
+                                if (!string.IsNullOrEmpty(element))
+                                {
+                                    logOrThrowExceptionHelper.ParameterBindingFailed("ParsableTodo[]", "p", element);
+                                    wasParamCheckFailure = true;
+                                }
                             }
                             p_local[i] = parsed_element!;
                         }
@@ -158,7 +164,11 @@ namespace Microsoft.AspNetCore.Http.Generated
                             var element = p_temp[i];
                             if (!GeneratedRouteBuilderExtensionsCore.TryParseExplicit<global::Microsoft.AspNetCore.Http.Generators.Tests.ParsableTodo>(element!, CultureInfo.InvariantCulture, out var parsed_element))
                             {
-                                wasParamCheckFailure = true;
+                                if (!string.IsNullOrEmpty(element))
+                                {
+                                    logOrThrowExceptionHelper.ParameterBindingFailed("ParsableTodo[]", "p", element);
+                                    wasParamCheckFailure = true;
+                                }
                             }
                             p_local[i] = parsed_element!;
                         }
@@ -227,4 +237,147 @@ namespace Microsoft.AspNetCore.Http.Generated
             => 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.");
+    }
 }

+ 145 - 0
src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitHeader_NullableStringArrayParam.generated.txt

@@ -79,6 +79,7 @@ namespace Microsoft.AspNetCore.Http.Generated
     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;
 
@@ -102,6 +103,7 @@ namespace Microsoft.AspNetCore.Http.Generated
                     var handler = (System.Func<global::System.String?[], global::System.Int32>)del;
                     EndpointFilterDelegate? filteredInvocation = null;
                     var serviceProvider = options?.ServiceProvider ?? options?.EndpointBuilder?.ApplicationServices;
+                    var logOrThrowExceptionHelper = new LogOrThrowExceptionHelper(serviceProvider, options);
                     var serializerOptions = serviceProvider?.GetService<IOptions<JsonOptions>>()?.Value.SerializerOptions ?? new JsonOptions().SerializerOptions;
                     var jsonTypeInfo =  (JsonTypeInfo<global::System.Int32>)serializerOptions.GetTypeInfo(typeof(global::System.Int32));
 
@@ -207,4 +209,147 @@ namespace Microsoft.AspNetCore.Http.Generated
 
 
     }
+
+    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.");
+    }
 }

+ 145 - 0
src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitHeader_StringArrayParam.generated.txt

@@ -79,6 +79,7 @@ namespace Microsoft.AspNetCore.Http.Generated
     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;
 
@@ -102,6 +103,7 @@ namespace Microsoft.AspNetCore.Http.Generated
                     var handler = (System.Func<global::System.String[], global::System.Int32>)del;
                     EndpointFilterDelegate? filteredInvocation = null;
                     var serviceProvider = options?.ServiceProvider ?? options?.EndpointBuilder?.ApplicationServices;
+                    var logOrThrowExceptionHelper = new LogOrThrowExceptionHelper(serviceProvider, options);
                     var serializerOptions = serviceProvider?.GetService<IOptions<JsonOptions>>()?.Value.SerializerOptions ?? new JsonOptions().SerializerOptions;
                     var jsonTypeInfo =  (JsonTypeInfo<global::System.Int32>)serializerOptions.GetTypeInfo(typeof(global::System.Int32));
 
@@ -207,4 +209,147 @@ namespace Microsoft.AspNetCore.Http.Generated
 
 
     }
+
+    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.");
+    }
 }

+ 155 - 2
src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_ComplexTypeArrayParam.generated.txt

@@ -79,6 +79,7 @@ namespace Microsoft.AspNetCore.Http.Generated
     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;
 
@@ -102,6 +103,7 @@ namespace Microsoft.AspNetCore.Http.Generated
                     var handler = (System.Func<global::Microsoft.AspNetCore.Http.Generators.Tests.ParsableTodo[], global::System.Int32>)del;
                     EndpointFilterDelegate? filteredInvocation = null;
                     var serviceProvider = options?.ServiceProvider ?? options?.EndpointBuilder?.ApplicationServices;
+                    var logOrThrowExceptionHelper = new LogOrThrowExceptionHelper(serviceProvider, options);
                     var serializerOptions = serviceProvider?.GetService<IOptions<JsonOptions>>()?.Value.SerializerOptions ?? new JsonOptions().SerializerOptions;
                     var jsonTypeInfo =  (JsonTypeInfo<global::System.Int32>)serializerOptions.GetTypeInfo(typeof(global::System.Int32));
 
@@ -131,7 +133,11 @@ namespace Microsoft.AspNetCore.Http.Generated
                             var element = p_temp[i];
                             if (!GeneratedRouteBuilderExtensionsCore.TryParseExplicit<global::Microsoft.AspNetCore.Http.Generators.Tests.ParsableTodo>(element!, CultureInfo.InvariantCulture, out var parsed_element))
                             {
-                                wasParamCheckFailure = true;
+                                if (!string.IsNullOrEmpty(element))
+                                {
+                                    logOrThrowExceptionHelper.ParameterBindingFailed("ParsableTodo[]", "p", element);
+                                    wasParamCheckFailure = true;
+                                }
                             }
                             p_local[i] = parsed_element!;
                         }
@@ -158,7 +164,11 @@ namespace Microsoft.AspNetCore.Http.Generated
                             var element = p_temp[i];
                             if (!GeneratedRouteBuilderExtensionsCore.TryParseExplicit<global::Microsoft.AspNetCore.Http.Generators.Tests.ParsableTodo>(element!, CultureInfo.InvariantCulture, out var parsed_element))
                             {
-                                wasParamCheckFailure = true;
+                                if (!string.IsNullOrEmpty(element))
+                                {
+                                    logOrThrowExceptionHelper.ParameterBindingFailed("ParsableTodo[]", "p", element);
+                                    wasParamCheckFailure = true;
+                                }
                             }
                             p_local[i] = parsed_element!;
                         }
@@ -227,4 +237,147 @@ namespace Microsoft.AspNetCore.Http.Generated
             => 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.");
+    }
 }

+ 144 - 0
src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_NullableStringArrayParam.generated.txt

@@ -79,6 +79,7 @@ namespace Microsoft.AspNetCore.Http.Generated
     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;
 
@@ -207,4 +208,147 @@ namespace Microsoft.AspNetCore.Http.Generated
 
 
     }
+
+    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.");
+    }
 }

+ 144 - 0
src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_StringArrayParam.generated.txt

@@ -79,6 +79,7 @@ namespace Microsoft.AspNetCore.Http.Generated
     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;
 
@@ -207,4 +208,147 @@ namespace Microsoft.AspNetCore.Http.Generated
 
 
     }
+
+    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.");
+    }
 }

+ 150 - 0
src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitServiceParam_SimpleReturn_Snapshot.generated.txt

@@ -109,6 +109,7 @@ namespace Microsoft.AspNetCore.Http.Generated
     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;
 
@@ -131,6 +132,8 @@ namespace Microsoft.AspNetCore.Http.Generated
                 {
                     var handler = (System.Func<global::Microsoft.AspNetCore.Http.Generators.Tests.TestService, global::System.String>)del;
                     EndpointFilterDelegate? filteredInvocation = null;
+                    var serviceProvider = options?.ServiceProvider ?? options?.EndpointBuilder?.ApplicationServices;
+                    var logOrThrowExceptionHelper = new LogOrThrowExceptionHelper(serviceProvider, options);
 
                     if (options?.EndpointBuilder?.FilterFactories.Count > 0)
                     {
@@ -191,6 +194,8 @@ namespace Microsoft.AspNetCore.Http.Generated
                 {
                     var handler = (System.Func<global::System.Collections.Generic.IEnumerable<global::Microsoft.AspNetCore.Http.Generators.Tests.TestService>, global::System.String>)del;
                     EndpointFilterDelegate? filteredInvocation = null;
+                    var serviceProvider = options?.ServiceProvider ?? options?.EndpointBuilder?.ApplicationServices;
+                    var logOrThrowExceptionHelper = new LogOrThrowExceptionHelper(serviceProvider, options);
 
                     if (options?.EndpointBuilder?.FilterFactories.Count > 0)
                     {
@@ -251,6 +256,8 @@ namespace Microsoft.AspNetCore.Http.Generated
                 {
                     var handler = (System.Func<global::Microsoft.AspNetCore.Http.Generators.Tests.TestService?, global::System.Collections.Generic.IEnumerable<global::Microsoft.AspNetCore.Http.Generators.Tests.TestService>, global::System.String>)del;
                     EndpointFilterDelegate? filteredInvocation = null;
+                    var serviceProvider = options?.ServiceProvider ?? options?.EndpointBuilder?.ApplicationServices;
+                    var logOrThrowExceptionHelper = new LogOrThrowExceptionHelper(serviceProvider, options);
 
                     if (options?.EndpointBuilder?.FilterFactories.Count > 0)
                     {
@@ -354,4 +361,147 @@ namespace Microsoft.AspNetCore.Http.Generated
 
 
     }
+
+    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.");
+    }
 }

+ 164 - 0
src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitSource_SimpleReturn_Snapshot.generated.txt

@@ -79,6 +79,7 @@ namespace Microsoft.AspNetCore.Http.Generated
     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;
 
@@ -101,6 +102,8 @@ namespace Microsoft.AspNetCore.Http.Generated
                 {
                     var handler = (System.Func<global::System.String, global::System.String>)del;
                     EndpointFilterDelegate? filteredInvocation = null;
+                    var serviceProvider = options?.ServiceProvider ?? options?.EndpointBuilder?.ApplicationServices;
+                    var logOrThrowExceptionHelper = new LogOrThrowExceptionHelper(serviceProvider, options);
 
                     if (options?.EndpointBuilder?.FilterFactories.Count > 0)
                     {
@@ -124,6 +127,7 @@ namespace Microsoft.AspNetCore.Http.Generated
                         if (StringValues.IsNullOrEmpty(queryValue_raw))
                         {
                             wasParamCheckFailure = true;
+                            logOrThrowExceptionHelper.RequiredParameterNotProvided("string", "queryValue", "query string");
                         }
                         var queryValue_temp = (string?)queryValue_raw;
                         string queryValue_local = queryValue_temp!;
@@ -146,6 +150,7 @@ namespace Microsoft.AspNetCore.Http.Generated
                         if (StringValues.IsNullOrEmpty(queryValue_raw))
                         {
                             wasParamCheckFailure = true;
+                            logOrThrowExceptionHelper.RequiredParameterNotProvided("string", "queryValue", "query string");
                         }
                         var queryValue_temp = (string?)queryValue_raw;
                         string queryValue_local = queryValue_temp!;
@@ -173,6 +178,8 @@ namespace Microsoft.AspNetCore.Http.Generated
                 {
                     var handler = (System.Func<global::System.String, global::System.String>)del;
                     EndpointFilterDelegate? filteredInvocation = null;
+                    var serviceProvider = options?.ServiceProvider ?? options?.EndpointBuilder?.ApplicationServices;
+                    var logOrThrowExceptionHelper = new LogOrThrowExceptionHelper(serviceProvider, options);
 
                     if (options?.EndpointBuilder?.FilterFactories.Count > 0)
                     {
@@ -196,6 +203,7 @@ namespace Microsoft.AspNetCore.Http.Generated
                         if (StringValues.IsNullOrEmpty(headerValue_raw))
                         {
                             wasParamCheckFailure = true;
+                            logOrThrowExceptionHelper.RequiredParameterNotProvided("string", "headerValue", "header");
                         }
                         var headerValue_temp = (string?)headerValue_raw;
                         string headerValue_local = headerValue_temp!;
@@ -218,6 +226,7 @@ namespace Microsoft.AspNetCore.Http.Generated
                         if (StringValues.IsNullOrEmpty(headerValue_raw))
                         {
                             wasParamCheckFailure = true;
+                            logOrThrowExceptionHelper.RequiredParameterNotProvided("string", "headerValue", "header");
                         }
                         var headerValue_temp = (string?)headerValue_raw;
                         string headerValue_local = headerValue_temp!;
@@ -245,6 +254,8 @@ namespace Microsoft.AspNetCore.Http.Generated
                 {
                     var handler = (System.Func<global::System.String, global::System.String>)del;
                     EndpointFilterDelegate? filteredInvocation = null;
+                    var serviceProvider = options?.ServiceProvider ?? options?.EndpointBuilder?.ApplicationServices;
+                    var logOrThrowExceptionHelper = new LogOrThrowExceptionHelper(serviceProvider, options);
 
                     if (options?.EndpointBuilder?.FilterFactories.Count > 0)
                     {
@@ -272,6 +283,7 @@ namespace Microsoft.AspNetCore.Http.Generated
                         if (routeValue_raw == null)
                         {
                             wasParamCheckFailure = true;
+                            logOrThrowExceptionHelper.RequiredParameterNotProvided("string", "routeValue", "unknown");
                         }
                         var routeValue_temp = (string?)routeValue_raw;
                         string routeValue_local = routeValue_temp!;
@@ -298,6 +310,7 @@ namespace Microsoft.AspNetCore.Http.Generated
                         if (routeValue_raw == null)
                         {
                             wasParamCheckFailure = true;
+                            logOrThrowExceptionHelper.RequiredParameterNotProvided("string", "routeValue", "unknown");
                         }
                         var routeValue_temp = (string?)routeValue_raw;
                         string routeValue_local = routeValue_temp!;
@@ -325,6 +338,8 @@ namespace Microsoft.AspNetCore.Http.Generated
                 {
                     var handler = (System.Func<global::System.String, global::System.String>)del;
                     EndpointFilterDelegate? filteredInvocation = null;
+                    var serviceProvider = options?.ServiceProvider ?? options?.EndpointBuilder?.ApplicationServices;
+                    var logOrThrowExceptionHelper = new LogOrThrowExceptionHelper(serviceProvider, options);
                     var value_RouteOrQueryResolver = GeneratedRouteBuilderExtensionsCore.ResolveFromRouteOrQuery("value", options?.RouteParameterNames);
 
                     if (options?.EndpointBuilder?.FilterFactories.Count > 0)
@@ -349,6 +364,7 @@ namespace Microsoft.AspNetCore.Http.Generated
                         if (value_raw is StringValues { Count: 0 })
                         {
                             wasParamCheckFailure = true;
+                            logOrThrowExceptionHelper.RequiredParameterNotProvided("string", "value", "route or query string");
                         }
                         var value_temp = (string?)value_raw;
                         string value_local = value_temp!;
@@ -371,6 +387,7 @@ namespace Microsoft.AspNetCore.Http.Generated
                         if (value_raw is StringValues { Count: 0 })
                         {
                             wasParamCheckFailure = true;
+                            logOrThrowExceptionHelper.RequiredParameterNotProvided("string", "value", "route or query string");
                         }
                         var value_temp = (string?)value_raw;
                         string value_local = value_temp!;
@@ -398,6 +415,8 @@ namespace Microsoft.AspNetCore.Http.Generated
                 {
                     var handler = (System.Func<global::System.String, global::System.String>)del;
                     EndpointFilterDelegate? filteredInvocation = null;
+                    var serviceProvider = options?.ServiceProvider ?? options?.EndpointBuilder?.ApplicationServices;
+                    var logOrThrowExceptionHelper = new LogOrThrowExceptionHelper(serviceProvider, options);
                     var value_RouteOrQueryResolver = GeneratedRouteBuilderExtensionsCore.ResolveFromRouteOrQuery("value", options?.RouteParameterNames);
 
                     if (options?.EndpointBuilder?.FilterFactories.Count > 0)
@@ -422,6 +441,7 @@ namespace Microsoft.AspNetCore.Http.Generated
                         if (value_raw is StringValues { Count: 0 })
                         {
                             wasParamCheckFailure = true;
+                            logOrThrowExceptionHelper.RequiredParameterNotProvided("string", "value", "route or query string");
                         }
                         var value_temp = (string?)value_raw;
                         string value_local = value_temp!;
@@ -444,6 +464,7 @@ namespace Microsoft.AspNetCore.Http.Generated
                         if (value_raw is StringValues { Count: 0 })
                         {
                             wasParamCheckFailure = true;
+                            logOrThrowExceptionHelper.RequiredParameterNotProvided("string", "value", "route or query string");
                         }
                         var value_temp = (string?)value_raw;
                         string value_local = value_temp!;
@@ -516,4 +537,147 @@ namespace Microsoft.AspNetCore.Http.Generated
         }
 
     }
+
+    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.");
+    }
 }

+ 156 - 3
src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_ComplexTypeArrayParam.generated.txt

@@ -79,6 +79,7 @@ namespace Microsoft.AspNetCore.Http.Generated
     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;
 
@@ -101,8 +102,9 @@ namespace Microsoft.AspNetCore.Http.Generated
                 {
                     var handler = (System.Func<global::Microsoft.AspNetCore.Http.Generators.Tests.ParsableTodo[], global::System.Int32>)del;
                     EndpointFilterDelegate? filteredInvocation = null;
-                    var p_RouteOrQueryResolver = GeneratedRouteBuilderExtensionsCore.ResolveFromRouteOrQuery("p", options?.RouteParameterNames);
                     var serviceProvider = options?.ServiceProvider ?? options?.EndpointBuilder?.ApplicationServices;
+                    var logOrThrowExceptionHelper = new LogOrThrowExceptionHelper(serviceProvider, options);
+                    var p_RouteOrQueryResolver = GeneratedRouteBuilderExtensionsCore.ResolveFromRouteOrQuery("p", options?.RouteParameterNames);
                     var serializerOptions = serviceProvider?.GetService<IOptions<JsonOptions>>()?.Value.SerializerOptions ?? new JsonOptions().SerializerOptions;
                     var jsonTypeInfo =  (JsonTypeInfo<global::System.Int32>)serializerOptions.GetTypeInfo(typeof(global::System.Int32));
 
@@ -132,7 +134,11 @@ namespace Microsoft.AspNetCore.Http.Generated
                             var element = p_temp[i];
                             if (!GeneratedRouteBuilderExtensionsCore.TryParseExplicit<global::Microsoft.AspNetCore.Http.Generators.Tests.ParsableTodo>(element!, CultureInfo.InvariantCulture, out var parsed_element))
                             {
-                                wasParamCheckFailure = true;
+                                if (!string.IsNullOrEmpty(element))
+                                {
+                                    logOrThrowExceptionHelper.ParameterBindingFailed("ParsableTodo[]", "p", element);
+                                    wasParamCheckFailure = true;
+                                }
                             }
                             p_local[i] = parsed_element!;
                         }
@@ -159,7 +165,11 @@ namespace Microsoft.AspNetCore.Http.Generated
                             var element = p_temp[i];
                             if (!GeneratedRouteBuilderExtensionsCore.TryParseExplicit<global::Microsoft.AspNetCore.Http.Generators.Tests.ParsableTodo>(element!, CultureInfo.InvariantCulture, out var parsed_element))
                             {
-                                wasParamCheckFailure = true;
+                                if (!string.IsNullOrEmpty(element))
+                                {
+                                    logOrThrowExceptionHelper.ParameterBindingFailed("ParsableTodo[]", "p", element);
+                                    wasParamCheckFailure = true;
+                                }
                             }
                             p_local[i] = parsed_element!;
                         }
@@ -234,4 +244,147 @@ namespace Microsoft.AspNetCore.Http.Generated
             => 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.");
+    }
 }

+ 144 - 0
src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_NullableStringArrayParam.generated.txt

@@ -79,6 +79,7 @@ namespace Microsoft.AspNetCore.Http.Generated
     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;
 
@@ -207,4 +208,147 @@ namespace Microsoft.AspNetCore.Http.Generated
 
 
     }
+
+    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.");
+    }
 }

+ 144 - 0
src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_NullableStringArrayParam_EmptyQueryValues.generated.txt

@@ -79,6 +79,7 @@ namespace Microsoft.AspNetCore.Http.Generated
     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;
 
@@ -207,4 +208,147 @@ namespace Microsoft.AspNetCore.Http.Generated
 
 
     }
+
+    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.");
+    }
 }

+ 144 - 0
src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_NullableStringArrayParam_QueryNotPresent.generated.txt

@@ -79,6 +79,7 @@ namespace Microsoft.AspNetCore.Http.Generated
     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;
 
@@ -207,4 +208,147 @@ namespace Microsoft.AspNetCore.Http.Generated
 
 
     }
+
+    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.");
+    }
 }

+ 144 - 0
src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_StringArrayParam.generated.txt

@@ -79,6 +79,7 @@ namespace Microsoft.AspNetCore.Http.Generated
     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;
 
@@ -207,4 +208,147 @@ namespace Microsoft.AspNetCore.Http.Generated
 
 
     }
+
+    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.");
+    }
 }

+ 172 - 8
src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_JsonBodyOrService_HandlesBothJsonAndService.generated.txt

@@ -79,6 +79,7 @@ namespace Microsoft.AspNetCore.Http.Generated
     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;
 
@@ -101,9 +102,11 @@ namespace Microsoft.AspNetCore.Http.Generated
                 {
                     var handler = (System.Func<global::Microsoft.AspNetCore.Http.Generators.Tests.Todo, global::Microsoft.AspNetCore.Http.Generators.Tests.TestService, global::System.String>)del;
                     EndpointFilterDelegate? filteredInvocation = null;
-                    var serviceProviderIsService = options?.ServiceProvider?.GetService<IServiceProviderIsService>();
-                    var todo_JsonBodyOrServiceResolver = ResolveJsonBodyOrService<global::Microsoft.AspNetCore.Http.Generators.Tests.Todo>(serviceProviderIsService);
-                    var svc_JsonBodyOrServiceResolver = ResolveJsonBodyOrService<global::Microsoft.AspNetCore.Http.Generators.Tests.TestService>(serviceProviderIsService);
+                    var serviceProvider = options?.ServiceProvider ?? options?.EndpointBuilder?.ApplicationServices;
+                    var logOrThrowExceptionHelper = new LogOrThrowExceptionHelper(serviceProvider, options);
+                    var serviceProviderIsService = serviceProvider?.GetService<IServiceProviderIsService>();
+                    var todo_JsonBodyOrServiceResolver = ResolveJsonBodyOrService<global::Microsoft.AspNetCore.Http.Generators.Tests.Todo>(logOrThrowExceptionHelper, "Todo", "todo", serviceProviderIsService);
+                    var svc_JsonBodyOrServiceResolver = ResolveJsonBodyOrService<global::Microsoft.AspNetCore.Http.Generators.Tests.TestService>(logOrThrowExceptionHelper, "TestService", "svc", serviceProviderIsService);
 
                     if (options?.EndpointBuilder?.FilterFactories.Count > 0)
                     {
@@ -225,7 +228,7 @@ namespace Microsoft.AspNetCore.Http.Generated
             }
         }
 
-        private static async ValueTask<(bool, T?)> TryResolveBodyAsync<T>(HttpContext httpContext, bool allowEmpty)
+        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>();
 
@@ -233,6 +236,7 @@ namespace Microsoft.AspNetCore.Http.Generated
             {
                 if (!httpContext.Request.HasJsonContentType())
                 {
+                    logOrThrowExceptionHelper.UnexpectedJsonContentType(httpContext.Request.ContentType);
                     httpContext.Response.StatusCode = StatusCodes.Status415UnsupportedMediaType;
                     return (false, default);
                 }
@@ -241,17 +245,34 @@ namespace Microsoft.AspNetCore.Http.Generated
                     var bodyValue = await httpContext.Request.ReadFromJsonAsync<T>();
                     if (!allowEmpty && bodyValue == null)
                     {
+                        if (!isInferred)
+                        {
+                            logOrThrowExceptionHelper.RequiredParameterNotProvided(parameterTypeName, parameterName, "body");
+                        }
+                        else
+                        {
+                            logOrThrowExceptionHelper.ImplicitBodyNotProvided(parameterName);
+                        }
                         httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
                         return (false, bodyValue);
                     }
                     return (true, bodyValue);
                 }
-                catch (IOException)
+                catch (BadHttpRequestException badHttpRequestException)
                 {
+                    logOrThrowExceptionHelper.RequestBodyIOException(badHttpRequestException);
+                    httpContext.Response.StatusCode = badHttpRequestException.StatusCode;
                     return (false, default);
                 }
-                catch (System.Text.Json.JsonException)
+                catch (IOException ioException)
                 {
+                    logOrThrowExceptionHelper.RequestBodyIOException(ioException);
+                    httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
+                    return (false, default);
+                }
+                catch (System.Text.Json.JsonException jsonException)
+                {
+                    logOrThrowExceptionHelper.InvalidJsonRequestBody(parameterTypeName, parameterName, jsonException);
                     httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
                     return (false, default);
                 }
@@ -263,7 +284,7 @@ namespace Microsoft.AspNetCore.Http.Generated
 
             return (allowEmpty, default);
         }
-        private static Func<HttpContext, bool, ValueTask<(bool, T?)>> ResolveJsonBodyOrService<T>(IServiceProviderIsService? serviceProviderIsService = null)
+        private static Func<HttpContext, bool, ValueTask<(bool, T?)>> ResolveJsonBodyOrService<T>(LogOrThrowExceptionHelper logOrThrowExceptionHelper, string parameterTypeName, string parameterName, IServiceProviderIsService? serviceProviderIsService = null)
         {
             if (serviceProviderIsService is not null)
             {
@@ -272,8 +293,151 @@ namespace Microsoft.AspNetCore.Http.Generated
                     return static (httpContext, isOptional) => new ValueTask<(bool, T?)>((true, httpContext.RequestServices.GetService<T>()));
                 }
             }
-            return static (httpContext, isOptional) => TryResolveBodyAsync<T>(httpContext, isOptional);
+            return (httpContext, isOptional) => TryResolveBodyAsync<T>(httpContext, logOrThrowExceptionHelper, isOptional, parameterTypeName, parameterName, isInferred: true);
+        }
+
+    }
+
+    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.");
     }
 }

+ 144 - 0
src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_MultipleSpecialTypeParam_StringReturn.generated.txt

@@ -79,6 +79,7 @@ namespace Microsoft.AspNetCore.Http.Generated
     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;
 
@@ -200,4 +201,147 @@ namespace Microsoft.AspNetCore.Http.Generated
 
 
     }
+
+    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.");
+    }
 }

+ 150 - 0
src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_MultipleStringParam_StringReturn.generated.txt

@@ -79,6 +79,7 @@ namespace Microsoft.AspNetCore.Http.Generated
     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;
 
@@ -101,6 +102,8 @@ namespace Microsoft.AspNetCore.Http.Generated
                 {
                     var handler = (System.Func<global::System.String, global::System.String, global::System.String>)del;
                     EndpointFilterDelegate? filteredInvocation = null;
+                    var serviceProvider = options?.ServiceProvider ?? options?.EndpointBuilder?.ApplicationServices;
+                    var logOrThrowExceptionHelper = new LogOrThrowExceptionHelper(serviceProvider, options);
 
                     if (options?.EndpointBuilder?.FilterFactories.Count > 0)
                     {
@@ -124,6 +127,7 @@ namespace Microsoft.AspNetCore.Http.Generated
                         if (StringValues.IsNullOrEmpty(p1_raw))
                         {
                             wasParamCheckFailure = true;
+                            logOrThrowExceptionHelper.RequiredParameterNotProvided("string", "p1", "query string");
                         }
                         var p1_temp = (string?)p1_raw;
                         string p1_local = p1_temp!;
@@ -132,6 +136,7 @@ namespace Microsoft.AspNetCore.Http.Generated
                         if (StringValues.IsNullOrEmpty(p2_raw))
                         {
                             wasParamCheckFailure = true;
+                            logOrThrowExceptionHelper.RequiredParameterNotProvided("string", "p2", "query string");
                         }
                         var p2_temp = (string?)p2_raw;
                         string p2_local = p2_temp!;
@@ -154,6 +159,7 @@ namespace Microsoft.AspNetCore.Http.Generated
                         if (StringValues.IsNullOrEmpty(p1_raw))
                         {
                             wasParamCheckFailure = true;
+                            logOrThrowExceptionHelper.RequiredParameterNotProvided("string", "p1", "query string");
                         }
                         var p1_temp = (string?)p1_raw;
                         string p1_local = p1_temp!;
@@ -162,6 +168,7 @@ namespace Microsoft.AspNetCore.Http.Generated
                         if (StringValues.IsNullOrEmpty(p2_raw))
                         {
                             wasParamCheckFailure = true;
+                            logOrThrowExceptionHelper.RequiredParameterNotProvided("string", "p2", "query string");
                         }
                         var p2_temp = (string?)p2_raw;
                         string p2_local = p2_temp!;
@@ -228,4 +235,147 @@ namespace Microsoft.AspNetCore.Http.Generated
 
 
     }
+
+    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.");
+    }
 }

+ 144 - 0
src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_NoParam_StringReturn_WithFilter.generated.txt

@@ -79,6 +79,7 @@ namespace Microsoft.AspNetCore.Http.Generated
     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;
 
@@ -194,4 +195,147 @@ namespace Microsoft.AspNetCore.Http.Generated
 
 
     }
+
+    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.");
+    }
 }

+ 158 - 2
src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_SingleComplexTypeParam_StringReturn.generated.txt

@@ -79,6 +79,7 @@ namespace Microsoft.AspNetCore.Http.Generated
     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;
 
@@ -101,6 +102,8 @@ namespace Microsoft.AspNetCore.Http.Generated
                 {
                     var handler = (System.Func<global::Microsoft.AspNetCore.Http.Generators.Tests.TryParseTodo, global::System.String>)del;
                     EndpointFilterDelegate? filteredInvocation = null;
+                    var serviceProvider = options?.ServiceProvider ?? options?.EndpointBuilder?.ApplicationServices;
+                    var logOrThrowExceptionHelper = new LogOrThrowExceptionHelper(serviceProvider, options);
 
                     if (options?.EndpointBuilder?.FilterFactories.Count > 0)
                     {
@@ -124,11 +127,16 @@ namespace Microsoft.AspNetCore.Http.Generated
                         if (StringValues.IsNullOrEmpty(p_raw))
                         {
                             wasParamCheckFailure = true;
+                            logOrThrowExceptionHelper.RequiredParameterNotProvided("TryParseTodo", "p", "query string");
                         }
                         var p_temp = (string?)p_raw;
                         if (!global::Microsoft.AspNetCore.Http.Generators.Tests.TryParseTodo.TryParse(p_temp!, out var p_parsed_temp))
                         {
-                            wasParamCheckFailure = true;
+                            if (!string.IsNullOrEmpty(p_temp))
+                            {
+                                logOrThrowExceptionHelper.ParameterBindingFailed("TryParseTodo", "p", p_temp);
+                                wasParamCheckFailure = true;
+                            }
                         }
                         global::Microsoft.AspNetCore.Http.Generators.Tests.TryParseTodo p_local = p_parsed_temp!;
 
@@ -150,11 +158,16 @@ namespace Microsoft.AspNetCore.Http.Generated
                         if (StringValues.IsNullOrEmpty(p_raw))
                         {
                             wasParamCheckFailure = true;
+                            logOrThrowExceptionHelper.RequiredParameterNotProvided("TryParseTodo", "p", "query string");
                         }
                         var p_temp = (string?)p_raw;
                         if (!global::Microsoft.AspNetCore.Http.Generators.Tests.TryParseTodo.TryParse(p_temp!, out var p_parsed_temp))
                         {
-                            wasParamCheckFailure = true;
+                            if (!string.IsNullOrEmpty(p_temp))
+                            {
+                                logOrThrowExceptionHelper.ParameterBindingFailed("TryParseTodo", "p", p_temp);
+                                wasParamCheckFailure = true;
+                            }
                         }
                         global::Microsoft.AspNetCore.Http.Generators.Tests.TryParseTodo p_local = p_parsed_temp!;
 
@@ -222,4 +235,147 @@ namespace Microsoft.AspNetCore.Http.Generated
             => 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.");
+    }
 }

+ 158 - 2
src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_SingleEnumParam_StringReturn.generated.txt

@@ -79,6 +79,7 @@ namespace Microsoft.AspNetCore.Http.Generated
     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;
 
@@ -101,6 +102,8 @@ namespace Microsoft.AspNetCore.Http.Generated
                 {
                     var handler = (System.Func<global::Microsoft.AspNetCore.Http.Generators.Tests.TodoStatus, global::System.String>)del;
                     EndpointFilterDelegate? filteredInvocation = null;
+                    var serviceProvider = options?.ServiceProvider ?? options?.EndpointBuilder?.ApplicationServices;
+                    var logOrThrowExceptionHelper = new LogOrThrowExceptionHelper(serviceProvider, options);
 
                     if (options?.EndpointBuilder?.FilterFactories.Count > 0)
                     {
@@ -124,11 +127,16 @@ namespace Microsoft.AspNetCore.Http.Generated
                         if (StringValues.IsNullOrEmpty(p_raw))
                         {
                             wasParamCheckFailure = true;
+                            logOrThrowExceptionHelper.RequiredParameterNotProvided("TodoStatus", "p", "query string");
                         }
                         var p_temp = (string?)p_raw;
                         if (!Enum.TryParse<global::Microsoft.AspNetCore.Http.Generators.Tests.TodoStatus>(p_temp!, out var p_parsed_temp))
                         {
-                            wasParamCheckFailure = true;
+                            if (!string.IsNullOrEmpty(p_temp))
+                            {
+                                logOrThrowExceptionHelper.ParameterBindingFailed("TodoStatus", "p", p_temp);
+                                wasParamCheckFailure = true;
+                            }
                         }
                         global::Microsoft.AspNetCore.Http.Generators.Tests.TodoStatus p_local = p_parsed_temp!;
 
@@ -150,11 +158,16 @@ namespace Microsoft.AspNetCore.Http.Generated
                         if (StringValues.IsNullOrEmpty(p_raw))
                         {
                             wasParamCheckFailure = true;
+                            logOrThrowExceptionHelper.RequiredParameterNotProvided("TodoStatus", "p", "query string");
                         }
                         var p_temp = (string?)p_raw;
                         if (!Enum.TryParse<global::Microsoft.AspNetCore.Http.Generators.Tests.TodoStatus>(p_temp!, out var p_parsed_temp))
                         {
-                            wasParamCheckFailure = true;
+                            if (!string.IsNullOrEmpty(p_temp))
+                            {
+                                logOrThrowExceptionHelper.ParameterBindingFailed("TodoStatus", "p", p_temp);
+                                wasParamCheckFailure = true;
+                            }
                         }
                         global::Microsoft.AspNetCore.Http.Generators.Tests.TodoStatus p_local = p_parsed_temp!;
 
@@ -222,4 +235,147 @@ namespace Microsoft.AspNetCore.Http.Generated
             => 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.");
+    }
 }

+ 146 - 0
src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_SingleNullableStringParam_WithEmptyQueryStringValueProvided_StringReturn.generated.txt

@@ -79,6 +79,7 @@ namespace Microsoft.AspNetCore.Http.Generated
     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;
 
@@ -101,6 +102,8 @@ namespace Microsoft.AspNetCore.Http.Generated
                 {
                     var handler = (System.Func<global::System.String?, global::System.String>)del;
                     EndpointFilterDelegate? filteredInvocation = null;
+                    var serviceProvider = options?.ServiceProvider ?? options?.EndpointBuilder?.ApplicationServices;
+                    var logOrThrowExceptionHelper = new LogOrThrowExceptionHelper(serviceProvider, options);
 
                     if (options?.EndpointBuilder?.FilterFactories.Count > 0)
                     {
@@ -204,4 +207,147 @@ namespace Microsoft.AspNetCore.Http.Generated
 
 
     }
+
+    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.");
+    }
 }

+ 171 - 7
src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapPost_WithArrayQueryString_AndBody_ShouldUseBody.generated.txt

@@ -79,6 +79,7 @@ namespace Microsoft.AspNetCore.Http.Generated
     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;
 
@@ -101,8 +102,10 @@ namespace Microsoft.AspNetCore.Http.Generated
                 {
                     var handler = (System.Func<global::System.String[], global::System.String>)del;
                     EndpointFilterDelegate? filteredInvocation = null;
-                    var serviceProviderIsService = options?.ServiceProvider?.GetService<IServiceProviderIsService>();
-                    var p_JsonBodyOrServiceResolver = ResolveJsonBodyOrService<global::System.String[]>(serviceProviderIsService);
+                    var serviceProvider = options?.ServiceProvider ?? options?.EndpointBuilder?.ApplicationServices;
+                    var logOrThrowExceptionHelper = new LogOrThrowExceptionHelper(serviceProvider, options);
+                    var serviceProviderIsService = serviceProvider?.GetService<IServiceProviderIsService>();
+                    var p_JsonBodyOrServiceResolver = ResolveJsonBodyOrService<global::System.String[]>(logOrThrowExceptionHelper, "string[]", "p", serviceProviderIsService);
 
                     if (options?.EndpointBuilder?.FilterFactories.Count > 0)
                     {
@@ -210,7 +213,7 @@ namespace Microsoft.AspNetCore.Http.Generated
             }
         }
 
-        private static async ValueTask<(bool, T?)> TryResolveBodyAsync<T>(HttpContext httpContext, bool allowEmpty)
+        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>();
 
@@ -218,6 +221,7 @@ namespace Microsoft.AspNetCore.Http.Generated
             {
                 if (!httpContext.Request.HasJsonContentType())
                 {
+                    logOrThrowExceptionHelper.UnexpectedJsonContentType(httpContext.Request.ContentType);
                     httpContext.Response.StatusCode = StatusCodes.Status415UnsupportedMediaType;
                     return (false, default);
                 }
@@ -226,17 +230,34 @@ namespace Microsoft.AspNetCore.Http.Generated
                     var bodyValue = await httpContext.Request.ReadFromJsonAsync<T>();
                     if (!allowEmpty && bodyValue == null)
                     {
+                        if (!isInferred)
+                        {
+                            logOrThrowExceptionHelper.RequiredParameterNotProvided(parameterTypeName, parameterName, "body");
+                        }
+                        else
+                        {
+                            logOrThrowExceptionHelper.ImplicitBodyNotProvided(parameterName);
+                        }
                         httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
                         return (false, bodyValue);
                     }
                     return (true, bodyValue);
                 }
-                catch (IOException)
+                catch (BadHttpRequestException badHttpRequestException)
                 {
+                    logOrThrowExceptionHelper.RequestBodyIOException(badHttpRequestException);
+                    httpContext.Response.StatusCode = badHttpRequestException.StatusCode;
                     return (false, default);
                 }
-                catch (System.Text.Json.JsonException)
+                catch (IOException ioException)
                 {
+                    logOrThrowExceptionHelper.RequestBodyIOException(ioException);
+                    httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
+                    return (false, default);
+                }
+                catch (System.Text.Json.JsonException jsonException)
+                {
+                    logOrThrowExceptionHelper.InvalidJsonRequestBody(parameterTypeName, parameterName, jsonException);
                     httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
                     return (false, default);
                 }
@@ -248,7 +269,7 @@ namespace Microsoft.AspNetCore.Http.Generated
 
             return (allowEmpty, default);
         }
-        private static Func<HttpContext, bool, ValueTask<(bool, T?)>> ResolveJsonBodyOrService<T>(IServiceProviderIsService? serviceProviderIsService = null)
+        private static Func<HttpContext, bool, ValueTask<(bool, T?)>> ResolveJsonBodyOrService<T>(LogOrThrowExceptionHelper logOrThrowExceptionHelper, string parameterTypeName, string parameterName, IServiceProviderIsService? serviceProviderIsService = null)
         {
             if (serviceProviderIsService is not null)
             {
@@ -257,8 +278,151 @@ namespace Microsoft.AspNetCore.Http.Generated
                     return static (httpContext, isOptional) => new ValueTask<(bool, T?)>((true, httpContext.RequestServices.GetService<T>()));
                 }
             }
-            return static (httpContext, isOptional) => TryResolveBodyAsync<T>(httpContext, isOptional);
+            return (httpContext, isOptional) => TryResolveBodyAsync<T>(httpContext, logOrThrowExceptionHelper, isOptional, parameterTypeName, parameterName, isInferred: true);
+        }
+
+    }
+
+    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.");
     }
 }

+ 170 - 7
src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapPost_WithArrayQueryString_ShouldFail.generated.txt

@@ -79,6 +79,7 @@ namespace Microsoft.AspNetCore.Http.Generated
     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;
 
@@ -101,9 +102,10 @@ namespace Microsoft.AspNetCore.Http.Generated
                 {
                     var handler = (System.Func<global::System.String[], global::System.Int32>)del;
                     EndpointFilterDelegate? filteredInvocation = null;
-                    var serviceProviderIsService = options?.ServiceProvider?.GetService<IServiceProviderIsService>();
-                    var p_JsonBodyOrServiceResolver = ResolveJsonBodyOrService<global::System.String[]>(serviceProviderIsService);
                     var serviceProvider = options?.ServiceProvider ?? options?.EndpointBuilder?.ApplicationServices;
+                    var logOrThrowExceptionHelper = new LogOrThrowExceptionHelper(serviceProvider, options);
+                    var serviceProviderIsService = serviceProvider?.GetService<IServiceProviderIsService>();
+                    var p_JsonBodyOrServiceResolver = ResolveJsonBodyOrService<global::System.String[]>(logOrThrowExceptionHelper, "string[]", "p", serviceProviderIsService);
                     var serializerOptions = serviceProvider?.GetService<IOptions<JsonOptions>>()?.Value.SerializerOptions ?? new JsonOptions().SerializerOptions;
                     var jsonTypeInfo =  (JsonTypeInfo<global::System.Int32>)serializerOptions.GetTypeInfo(typeof(global::System.Int32));
 
@@ -213,7 +215,7 @@ namespace Microsoft.AspNetCore.Http.Generated
             }
         }
 
-        private static async ValueTask<(bool, T?)> TryResolveBodyAsync<T>(HttpContext httpContext, bool allowEmpty)
+        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>();
 
@@ -221,6 +223,7 @@ namespace Microsoft.AspNetCore.Http.Generated
             {
                 if (!httpContext.Request.HasJsonContentType())
                 {
+                    logOrThrowExceptionHelper.UnexpectedJsonContentType(httpContext.Request.ContentType);
                     httpContext.Response.StatusCode = StatusCodes.Status415UnsupportedMediaType;
                     return (false, default);
                 }
@@ -229,17 +232,34 @@ namespace Microsoft.AspNetCore.Http.Generated
                     var bodyValue = await httpContext.Request.ReadFromJsonAsync<T>();
                     if (!allowEmpty && bodyValue == null)
                     {
+                        if (!isInferred)
+                        {
+                            logOrThrowExceptionHelper.RequiredParameterNotProvided(parameterTypeName, parameterName, "body");
+                        }
+                        else
+                        {
+                            logOrThrowExceptionHelper.ImplicitBodyNotProvided(parameterName);
+                        }
                         httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
                         return (false, bodyValue);
                     }
                     return (true, bodyValue);
                 }
-                catch (IOException)
+                catch (BadHttpRequestException badHttpRequestException)
                 {
+                    logOrThrowExceptionHelper.RequestBodyIOException(badHttpRequestException);
+                    httpContext.Response.StatusCode = badHttpRequestException.StatusCode;
                     return (false, default);
                 }
-                catch (System.Text.Json.JsonException)
+                catch (IOException ioException)
                 {
+                    logOrThrowExceptionHelper.RequestBodyIOException(ioException);
+                    httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
+                    return (false, default);
+                }
+                catch (System.Text.Json.JsonException jsonException)
+                {
+                    logOrThrowExceptionHelper.InvalidJsonRequestBody(parameterTypeName, parameterName, jsonException);
                     httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
                     return (false, default);
                 }
@@ -251,7 +271,7 @@ namespace Microsoft.AspNetCore.Http.Generated
 
             return (allowEmpty, default);
         }
-        private static Func<HttpContext, bool, ValueTask<(bool, T?)>> ResolveJsonBodyOrService<T>(IServiceProviderIsService? serviceProviderIsService = null)
+        private static Func<HttpContext, bool, ValueTask<(bool, T?)>> ResolveJsonBodyOrService<T>(LogOrThrowExceptionHelper logOrThrowExceptionHelper, string parameterTypeName, string parameterName, IServiceProviderIsService? serviceProviderIsService = null)
         {
             if (serviceProviderIsService is not null)
             {
@@ -260,8 +280,151 @@ namespace Microsoft.AspNetCore.Http.Generated
                     return static (httpContext, isOptional) => new ValueTask<(bool, T?)>((true, httpContext.RequestServices.GetService<T>()));
                 }
             }
-            return static (httpContext, isOptional) => TryResolveBodyAsync<T>(httpContext, isOptional);
+            return (httpContext, isOptional) => TryResolveBodyAsync<T>(httpContext, logOrThrowExceptionHelper, isOptional, parameterTypeName, parameterName, isInferred: true);
+        }
+
+    }
+
+    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.");
     }
 }

+ 144 - 0
src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/Multiple_MapAction_NoParam_StringReturn.generated.txt

@@ -109,6 +109,7 @@ namespace Microsoft.AspNetCore.Http.Generated
     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;
 
@@ -388,4 +389,147 @@ namespace Microsoft.AspNetCore.Http.Generated
 
 
     }
+
+    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.");
+    }
 }

+ 144 - 0
src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/Multiple_MapAction_WithParams_StringReturn.generated.txt

@@ -109,6 +109,7 @@ namespace Microsoft.AspNetCore.Http.Generated
     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;
 
@@ -346,4 +347,147 @@ namespace Microsoft.AspNetCore.Http.Generated
 
 
     }
+
+    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.");
+    }
 }

+ 1 - 1
src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTestBase.cs

@@ -152,7 +152,7 @@ public abstract class RequestDelegateCreationTestBase : LoggedTest
 
         Assert.NotNull(handler);
 
-        var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(serviceProvider ?? new EmptyServiceProvider()));
+        var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(serviceProvider ?? CreateServiceProvider()));
         _ = handler(builder);
 
         var dataSource = Assert.Single(builder.DataSources);

+ 9 - 0
src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.BindAsync.cs

@@ -13,6 +13,7 @@ using System.Text.Json;
 using Microsoft.AspNetCore.Http.Features;
 using Microsoft.AspNetCore.Http.RequestDelegateGenerator.StaticRouteHandlerModel;
 using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Primitives;
 
 namespace Microsoft.AspNetCore.Http.Generators.Tests;
@@ -135,6 +136,14 @@ app.MapGet("/", (HttpContext httpContext, {{bindAsyncType}} myBindAsyncParam) =>
 
             Assert.Null(httpContext.Items["uri"]);
             Assert.Equal(400, httpContext.Response.StatusCode);
+            Assert.Equal(400, httpContext.Response.StatusCode);
+            var log = Assert.Single(TestSink.Writes);
+            Assert.Equal(LogLevel.Debug, log.LogLevel);
+            Assert.Equal(new EventId(4, "RequiredParameterNotProvided"), log.EventId);
+            var parameters = bindAsyncType is "MySimpleBindAsyncRecord" || bindAsyncType is "InheritBindAsync"
+                ? "(HttpContext)"
+                : "(HttpContext, ParameterInfo)";
+            Assert.Equal($@"Required parameter ""{bindAsyncType} myBindAsyncParam"" was not provided from {bindAsyncType}.BindAsync{parameters}.", log.Message);
         }
     }
 

+ 49 - 0
src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.JsonBody.cs

@@ -5,6 +5,8 @@ using System.Globalization;
 using System.IO.Pipelines;
 using System.Text.Json;
 using Microsoft.AspNetCore.Http.Features;
+using Microsoft.Extensions.Logging;
+
 namespace Microsoft.AspNetCore.Http.Generators.Tests;
 
 public abstract partial class RequestDelegateCreationTests
@@ -401,4 +403,51 @@ app.MapPost("/", TestAction);
 
         Assert.Equal(default(BodyStruct), httpContext.Items[structToBeZeroedKey]);
     }
+
+    public static IEnumerable<object[]> AllowEmptyData
+    {
+        get
+        {
+            return new List<object[]>
+                {
+                    new object[] { $@"string handler([CustomFromBody(AllowEmpty = false)] Todo todo) => todo?.ToString() ?? string.Empty", false },
+                    new object[] { $@"string handler([CustomFromBody(AllowEmpty = true)] Todo todo) => todo?.ToString() ?? string.Empty", true },
+                    new object[] { $@"string handler([CustomFromBody(AllowEmpty = true)] Todo? todo = null) => todo?.ToString() ?? string.Empty", true },
+                    new object[] { $@"string handler([CustomFromBody(AllowEmpty = false)] Todo? todo = null) => todo?.ToString() ?? string.Empty", true }
+                };
+        }
+    }
+
+    [Theory]
+    [MemberData(nameof(AllowEmptyData))]
+    public async Task AllowEmptyOverridesOptionality(string innerSource, bool allowsEmptyRequest)
+    {
+        var source = $"""
+{innerSource};
+app.MapPost("/", handler);
+""";
+        var (_, compilation) = await RunGeneratorAsync(source);
+        var endpoint = GetEndpointFromCompilation(compilation);
+
+        var httpContext = CreateHttpContextWithBody(null);
+
+        await endpoint.RequestDelegate(httpContext);
+
+        var logs = TestSink.Writes.ToArray();
+
+        if (!allowsEmptyRequest)
+        {
+            Assert.Equal(400, httpContext.Response.StatusCode);
+            var log = Assert.Single(logs);
+            Assert.Equal(LogLevel.Debug, log.LogLevel);
+            Assert.Equal(new EventId(4, "RequiredParameterNotProvided"), log.EventId);
+            Assert.Equal(@"Required parameter ""Todo todo"" was not provided from body.", log.Message);
+        }
+        else
+        {
+            Assert.Equal(200, httpContext.Response.StatusCode);
+            Assert.False(httpContext.RequestAborted.IsCancellationRequested);
+        }
+    }
+
 }

+ 52 - 1
src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.JsonBodyOrService.cs

@@ -1,12 +1,15 @@
 // Licensed to the .NET Foundation under one or more agreements.
 // The .NET Foundation licenses this file to you under the MIT license.
 using System.Globalization;
+using System.Text;
 using System.Text.Json;
 using Microsoft.AspNetCore.Http.Features;
 using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
 namespace Microsoft.AspNetCore.Http.Generators.Tests;
 
-public abstract partial class RequestDelegateCreationTests
+public abstract partial class RequestDelegateCreationTests : RequestDelegateCreationTestBase
 {
     public static object[][] MapAction_JsonBodyOrService_SimpleReturn_Data
     {
@@ -78,4 +81,52 @@ app.MapPost("/", (Todo todo, TestService svc) => $"{svc.TestServiceMethod()}, {t
         await endpoint.RequestDelegate(httpContext);
         await VerifyResponseBodyAsync(httpContext, expectedBody);
     }
+
+        public static IEnumerable<object[]> BodyParamOptionalityData
+    {
+        get
+        {
+            return new List<object[]>
+            {
+                new object[] { @"(Todo todo) => $""Todo: {todo.Name}"";", false, true, null },
+                new object[] { @"(Todo todo) => $""Todo: {todo.Name}"";", true, false, "Todo: Default Todo"},
+                new object[] { @"(Todo? todo = null) => $""Todo: {todo?.Name}"";", false, false, "Todo: "},
+                new object[] { @"(Todo? todo = null) => $""Todo: {todo?.Name}"";", true, false, "Todo: Default Todo"},
+                new object[] { @"(Todo? todo) => $""Todo: {todo?.Name}"";", false, false, "Todo: " },
+                new object[] { @"(Todo? todo) => $""Todo: {todo?.Name}"";", true, false, "Todo: Default Todo" },
+            };
+        }
+    }
+
+    [Theory]
+    [MemberData(nameof(BodyParamOptionalityData))]
+    public async Task RequestDelegateHandlesBodyParamOptionality(string innerSource, bool hasBody, bool isInvalid, string expectedBody)
+    {
+        var source = $"""
+string handler{innerSource};
+app.MapPost("/", handler);
+""";
+        var (_, compilation) = await RunGeneratorAsync(source);
+        var serviceProvider = CreateServiceProvider();
+        var endpoint = GetEndpointFromCompilation(compilation, serviceProvider: serviceProvider);
+
+        var todo = new Todo() { Name = "Default Todo" };
+        var httpContext = hasBody ? CreateHttpContextWithBody(todo) : CreateHttpContextWithBody(null);
+
+        await endpoint.RequestDelegate(httpContext);
+
+        if (isInvalid)
+        {
+            var logs = TestSink.Writes.ToArray();
+            Assert.Equal(400, httpContext.Response.StatusCode);
+            var log = Assert.Single(logs);
+            Assert.Equal(LogLevel.Debug, log.LogLevel);
+            Assert.Equal(new EventId(5, "ImplicitBodyNotProvided"), log.EventId);
+            Assert.Equal(@"Implicit body inferred for parameter ""todo"" but no body was provided. Did you mean to use a Service instead?", log.Message);
+        }
+        else
+        {
+            await VerifyResponseBodyAsync(httpContext, expectedBody);
+        }
+    }
 }

+ 646 - 0
src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.Logging.cs

@@ -0,0 +1,646 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Text;
+using System.Text.Json;
+using Microsoft.AspNetCore.Http.Features;
+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 RequestDelegateLogsStringValuesFromExplicitQueryStringSourceForUnpresentedValuesFailuresAsDebugAndSets400Response()
+    {
+        var source = """
+app.MapGet("/", (
+    HttpContext httpContext,
+    [FromHeader(Name = "foo")] StringValues headerValues,
+    [FromQuery(Name = "bar")] StringValues queryValues
+    // TODO: https://github.com/dotnet/aspnetcore/issues/47200
+    // [FromForm(Name = "form")] StringValues formValues
+) =>
+{
+    httpContext.Items["invoked"] = true;
+});
+""";
+        var (_, compilation) = await RunGeneratorAsync(source);
+        var serviceProvider = CreateServiceProvider();
+        var endpoint = GetEndpointFromCompilation(compilation, serviceProvider: serviceProvider);
+
+        var httpContext = CreateHttpContext(serviceProvider);
+        httpContext.Request.Form = new FormCollection(null);
+
+        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 logs = TestSink.Writes.ToArray();
+
+        Assert.Equal(2, logs.Length);
+
+        Assert.Equal(new EventId(4, "RequiredParameterNotProvided"), logs[0].EventId);
+        Assert.Equal(LogLevel.Debug, logs[0].LogLevel);
+        Assert.Equal(@"Required parameter ""StringValues headerValues"" was not provided from header.", logs[0].Message);
+        var log1Values = Assert.IsAssignableFrom<IReadOnlyList<KeyValuePair<string, object>>>(logs[0].State);
+        Assert.Equal("StringValues", log1Values[0].Value);
+        Assert.Equal("headerValues", log1Values[1].Value);
+        Assert.Equal("header", log1Values[2].Value);
+
+        Assert.Equal(new EventId(4, "RequiredParameterNotProvided"), logs[1].EventId);
+        Assert.Equal(LogLevel.Debug, logs[1].LogLevel);
+        Assert.Equal(@"Required parameter ""StringValues queryValues"" was not provided from query string.", logs[1].Message);
+        var log2Values = Assert.IsAssignableFrom<IReadOnlyList<KeyValuePair<string, object>>>(logs[1].State);
+        Assert.Equal("StringValues", log2Values[0].Value);
+        Assert.Equal("queryValues", log2Values[1].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);
+    }
+
+    [Fact]
+    public async Task RequestDelegateLogsTryParsableFailuresAsDebugAndSets400Response()
+    {
+        var source = """
+void TestAction(HttpContext httpContext, [FromRoute] int tryParsable, [FromRoute] int tryParsable2)
+{
+    httpContext.Items["invoked"] = true;
+}
+
+app.MapGet("/{tryParsable}/{tryParsable2}", TestAction);
+""";
+        var (_, compilation) = await RunGeneratorAsync(source);
+        var serviceProvider = CreateServiceProvider();
+        var endpoint = GetEndpointFromCompilation(compilation, serviceProvider: serviceProvider);
+
+        var httpContext = CreateHttpContext();
+        httpContext.Request.RouteValues["tryParsable"] = "invalid!";
+        httpContext.Request.RouteValues["tryParsable2"] = "invalid again!";
+
+        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 logs = TestSink.Writes.ToArray();
+
+        Assert.Equal(2, logs.Length);
+
+        Assert.Equal(new EventId(3, "ParameterBindingFailed"), logs[0].EventId);
+        Assert.Equal(LogLevel.Debug, logs[0].LogLevel);
+        Assert.Equal(@"Failed to bind parameter ""int tryParsable"" from ""invalid!"".", logs[0].Message);
+        var log1Values = Assert.IsAssignableFrom<IReadOnlyList<KeyValuePair<string, object>>>(logs[0].State);
+        Assert.Equal("int", log1Values[0].Value);
+        Assert.Equal("tryParsable", log1Values[1].Value);
+        Assert.Equal("invalid!", log1Values[2].Value);
+
+        Assert.Equal(new EventId(3, "ParameterBindingFailed"), logs[1].EventId);
+        Assert.Equal(LogLevel.Debug, logs[1].LogLevel);
+        Assert.Equal(@"Failed to bind parameter ""int tryParsable2"" from ""invalid again!"".", logs[1].Message);
+        var log2Values = Assert.IsAssignableFrom<IReadOnlyList<KeyValuePair<string, object>>>(logs[1].State);
+        Assert.Equal("int", log2Values[0].Value);
+        Assert.Equal("tryParsable2", log2Values[1].Value);
+        Assert.Equal("invalid again!", log2Values[2].Value);
+
+    }
+
+    [Fact]
+    public async Task RequestDelegateThrowsForTryParsableFailuresIfThrowOnBadRequest()
+    {
+        var source = """
+void TestAction(HttpContext httpContext, [FromRoute] int tryParsable, [FromRoute] int tryParsable2)
+{
+    httpContext.Items["invoked"] = true;
+}
+
+app.MapGet("/{tryParsable}/{tryParsable2}", 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.RouteValues["tryParsable"] = "invalid!";
+        httpContext.Request.RouteValues["tryParsable2"] = "invalid again!";
+
+        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 bind parameter ""int tryParsable"" from ""invalid!"".", badHttpRequestException.Message);
+        Assert.Equal(400, badHttpRequestException.StatusCode);
+    }
+
+    [Fact]
+    public async Task RequestDelegateThrowsForTryParsableFailuresIfThrowOnBadRequestWithArrays()
+    {
+        var source = """
+void TestAction(HttpContext httpContext, [FromQuery] int[] values)
+{
+    httpContext.Items["invoked"] = true;
+}
+app.MapGet("/", 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.Query = new QueryCollection(new Dictionary<string, StringValues>()
+        {
+            ["values"] = new(new[] { "1", "NAN", "3" })
+        });
+
+        var badHttpRequestException = await Assert.ThrowsAsync<BadHttpRequestException>(() => endpoint.RequestDelegate(httpContext));
+
+        // 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 bind parameter ""int[] values"" from ""NAN"".", badHttpRequestException.Message);
+        Assert.Equal(400, badHttpRequestException.StatusCode);
+    }
+
+    [Fact]
+    public async Task RequestDelegateLogsBindAsyncFailuresAndSets400Response()
+    {
+        var source = """
+void TestAction(HttpContext httpContext, MyBindAsyncRecord myBindAsyncParam1, MyBindAsyncRecord myBindAsyncParam2)
+{
+    httpContext.Items["invoked"] = true;
+}
+app.MapGet("/", TestAction);
+""";
+        var (_, compilation) = await RunGeneratorAsync(source);
+        var serviceProvider = CreateServiceProvider();
+        var endpoint = GetEndpointFromCompilation(compilation, serviceProvider: serviceProvider);
+
+        // Not supplying any headers will cause the HttpContext BindAsync overload to return null.
+        var httpContext = CreateHttpContext();
+
+        await endpoint.RequestDelegate(httpContext);
+
+        Assert.Null(httpContext.Items["invoked"]);
+        Assert.False(httpContext.RequestAborted.IsCancellationRequested);
+        Assert.Equal(400, httpContext.Response.StatusCode);
+
+        var logs = TestSink.Writes.ToArray();
+
+        Assert.Equal(2, logs.Length);
+
+        Assert.Equal(new EventId(4, "RequiredParameterNotProvided"), logs[0].EventId);
+        Assert.Equal(LogLevel.Debug, logs[0].LogLevel);
+        Assert.Equal(@"Required parameter ""MyBindAsyncRecord myBindAsyncParam1"" was not provided from MyBindAsyncRecord.BindAsync(HttpContext, ParameterInfo).", logs[0].Message);
+        var log1Values = Assert.IsAssignableFrom<IReadOnlyList<KeyValuePair<string, object>>>(logs[0].State);
+        Assert.Equal("MyBindAsyncRecord", log1Values[0].Value);
+        Assert.Equal("myBindAsyncParam1", log1Values[1].Value);
+        Assert.Equal("MyBindAsyncRecord.BindAsync(HttpContext, ParameterInfo)", log1Values[2].Value);
+
+        Assert.Equal(new EventId(4, "RequiredParameterNotProvided"), logs[1].EventId);
+        Assert.Equal(LogLevel.Debug, logs[1].LogLevel);
+        Assert.Equal(@"Required parameter ""MyBindAsyncRecord myBindAsyncParam2"" was not provided from MyBindAsyncRecord.BindAsync(HttpContext, ParameterInfo).", logs[1].Message);
+        var log2Values = Assert.IsAssignableFrom<IReadOnlyList<KeyValuePair<string, object>>>(logs[1].State);
+        Assert.Equal("MyBindAsyncRecord", log2Values[0].Value);
+        Assert.Equal("myBindAsyncParam2", log2Values[1].Value);
+        Assert.Equal("MyBindAsyncRecord.BindAsync(HttpContext, ParameterInfo)", log2Values[2].Value);
+    }
+
+    [Fact]
+    public async Task RequestDelegateThrowsForBindAsyncFailuresIfThrowOnBadRequest()
+    {
+        var source = """
+void TestAction(HttpContext httpContext, MyBindAsyncRecord myBindAsyncParam1, MyBindAsyncRecord myBindAsyncParam2)
+{
+    httpContext.Items["invoked"] = true;
+}
+app.MapGet("/", TestAction);
+""";
+        var (_, compilation) = await RunGeneratorAsync(source);
+        var serviceProvider = CreateServiceProvider(serviceCollection =>
+        {
+            serviceCollection.Configure<RouteHandlerOptions>(options => options.ThrowOnBadRequest = true);
+        });
+        var endpoint = GetEndpointFromCompilation(compilation, serviceProvider: serviceProvider);
+
+        // Not supplying any headers will cause the HttpContext BindAsync overload to return null.
+        var httpContext = CreateHttpContext();
+
+        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(@"Required parameter ""MyBindAsyncRecord myBindAsyncParam1"" was not provided from MyBindAsyncRecord.BindAsync(HttpContext, ParameterInfo).", badHttpRequestException.Message);
+        Assert.Equal(400, badHttpRequestException.StatusCode);
+    }
+
+    [Fact]
+    public async Task RequestDelegateLogsSingleArgBindAsyncFailuresAndSets400Response()
+    {
+        var source = """
+void TestAction(HttpContext httpContext, MySimpleBindAsyncRecord mySimpleBindAsyncRecord1, MySimpleBindAsyncRecord mySimpleBindAsyncRecord2)
+{
+    httpContext.Items["invoked"] = true;
+}
+app.MapGet("/", TestAction);
+""";
+        var (_, compilation) = await RunGeneratorAsync(source);
+        var serviceProvider = CreateServiceProvider();
+        var endpoint = GetEndpointFromCompilation(compilation, serviceProvider: serviceProvider);
+
+        // Not supplying any headers will cause the HttpContext BindAsync overload to return null.
+        var httpContext = CreateHttpContext();
+
+        await endpoint.RequestDelegate(httpContext);
+
+        Assert.Null(httpContext.Items["invoked"]);
+        Assert.False(httpContext.RequestAborted.IsCancellationRequested);
+        Assert.Equal(400, httpContext.Response.StatusCode);
+
+        var logs = TestSink.Writes.ToArray();
+
+        Assert.Equal(2, logs.Length);
+
+        Assert.Equal(new EventId(4, "RequiredParameterNotProvided"), logs[0].EventId);
+        Assert.Equal(LogLevel.Debug, logs[0].LogLevel);
+        Assert.Equal(@"Required parameter ""MySimpleBindAsyncRecord mySimpleBindAsyncRecord1"" was not provided from MySimpleBindAsyncRecord.BindAsync(HttpContext).", logs[0].Message);
+        var log1Values = Assert.IsAssignableFrom<IReadOnlyList<KeyValuePair<string, object>>>(logs[0].State);
+        Assert.Equal("MySimpleBindAsyncRecord", log1Values[0].Value);
+        Assert.Equal("mySimpleBindAsyncRecord1", log1Values[1].Value);
+        Assert.Equal("MySimpleBindAsyncRecord.BindAsync(HttpContext)", log1Values[2].Value);
+
+        Assert.Equal(new EventId(4, "RequiredParameterNotProvided"), logs[1].EventId);
+        Assert.Equal(LogLevel.Debug, logs[1].LogLevel);
+        Assert.Equal(@"Required parameter ""MySimpleBindAsyncRecord mySimpleBindAsyncRecord2"" was not provided from MySimpleBindAsyncRecord.BindAsync(HttpContext).", logs[1].Message);
+        var log2Values = Assert.IsAssignableFrom<IReadOnlyList<KeyValuePair<string, object>>>(logs[1].State);
+        Assert.Equal("MySimpleBindAsyncRecord", log2Values[0].Value);
+        Assert.Equal("mySimpleBindAsyncRecord2", log2Values[1].Value);
+        Assert.Equal("MySimpleBindAsyncRecord.BindAsync(HttpContext)", log2Values[2].Value);
+    }
+
+    [Fact]
+    public async Task RequestDelegateThrowsForSingleArgBindAsyncFailuresIfThrowOnBadRequest()
+    {
+        var source = """
+void TestAction(HttpContext httpContext, MySimpleBindAsyncRecord mySimpleBindAsyncRecord1, MySimpleBindAsyncRecord mySimpleBindAsyncRecord2)
+{
+    httpContext.Items["invoked"] = true;
+}
+app.MapGet("/", TestAction);
+""";
+        var (_, compilation) = await RunGeneratorAsync(source);
+        var serviceProvider = CreateServiceProvider(serviceCollection =>
+        {
+            serviceCollection.Configure<RouteHandlerOptions>(options => options.ThrowOnBadRequest = true);
+        });
+        var endpoint = GetEndpointFromCompilation(compilation, serviceProvider: serviceProvider);
+
+        // Not supplying any headers will cause the HttpContext BindAsync overload to return null.
+        var httpContext = CreateHttpContext();
+        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(@"Required parameter ""MySimpleBindAsyncRecord mySimpleBindAsyncRecord1"" was not provided from MySimpleBindAsyncRecord.BindAsync(HttpContext).", badHttpRequestException.Message);
+        Assert.Equal(400, badHttpRequestException.StatusCode);
+    }
+
+    [Theory]
+    [InlineData(true)]
+    [InlineData(false)]
+    public async Task RequestDelegateRejectsNonJsonContent(bool shouldThrow)
+    {
+        var source = """
+void TestAction(HttpContext httpContext, Todo todo)
+{
+    httpContext.Items["invoked"] = true;
+}
+app.MapPost("/", TestAction);
+""";
+        var (_, compilation) = await RunGeneratorAsync(source);
+        var serviceProvider = CreateServiceProvider(serviceCollection =>
+        {
+            serviceCollection.Configure<RouteHandlerOptions>(options => options.ThrowOnBadRequest = shouldThrow);
+        });
+        var endpoint = GetEndpointFromCompilation(compilation, serviceProvider: serviceProvider);
+
+        var httpContext = CreateHttpContext();
+        httpContext.Request.Headers["Content-Type"] = "application/xml";
+        httpContext.Request.Headers["Content-Length"] = "1";
+        httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
+
+        var request = endpoint.RequestDelegate(httpContext);
+
+        if (shouldThrow)
+        {
+            var ex = await Assert.ThrowsAsync<BadHttpRequestException>(() => request);
+            Assert.Equal("Expected a supported JSON 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(6, "UnexpectedContentType"), logMessage.EventId);
+            Assert.Equal(LogLevel.Debug, logMessage.LogLevel);
+            Assert.Equal("Expected a supported JSON media type but got \"application/xml\".", logMessage.Message);
+            var logValues = Assert.IsAssignableFrom<IReadOnlyList<KeyValuePair<string, object>>>(logMessage.State);
+            Assert.Equal("application/xml", logValues[0].Value);
+        }
+    }
+
+    [Theory]
+    [InlineData(true)]
+    [InlineData(false)]
+    public async Task RequestDelegateWithBindAndImplicitBodyRejectsNonJsonContent(bool shouldThrow)
+    {
+        var source = """
+void TestAction(HttpContext httpContext, Todo todo)
+{
+    httpContext.Items["invoked"] = true;
+}
+app.MapPost("/", TestAction);
+""";
+        var (_, compilation) = await RunGeneratorAsync(source);
+        var serviceProvider = CreateServiceProvider(serviceCollection =>
+        {
+            serviceCollection.Configure<RouteHandlerOptions>(options => options.ThrowOnBadRequest = shouldThrow);
+        });
+        var endpoint = GetEndpointFromCompilation(compilation, serviceProvider: serviceProvider);
+
+        Todo originalTodo = new()
+        {
+            Name = "Write more tests!"
+        };
+
+        var httpContext = CreateHttpContextWithBody(originalTodo);
+        httpContext.Request.Headers["Content-Type"] = "application/xml";
+
+        var request = endpoint.RequestDelegate(httpContext);
+
+        if (shouldThrow)
+        {
+            var ex = await Assert.ThrowsAsync<BadHttpRequestException>(() => request);
+            Assert.Equal("Expected a supported JSON 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(6, "UnexpectedContentType"), logMessage.EventId);
+            Assert.Equal(LogLevel.Debug, logMessage.LogLevel);
+            Assert.Equal("Expected a supported JSON media type but got \"application/xml\".", logMessage.Message);
+            var logValues = Assert.IsAssignableFrom<IReadOnlyList<KeyValuePair<string, object>>>(logMessage.State);
+            Assert.Equal("application/xml", logValues[0].Value);
+        }
+    }
+
+    [Theory]
+    [InlineData(true)]
+    [InlineData(false)]
+    public async Task RequestDelegateLogsIOExceptionsAsDebugDoesNotAbortAndNeverThrows(bool throwOnBadRequests)
+    {
+        var source = """
+void TestAction(HttpContext httpContext, [FromBody] Todo todo)
+{
+    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/json";
+        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 RequestDelegateLogsJsonExceptionsAsDebugAndSets400Response()
+    {
+        var source = """
+void TestAction(HttpContext httpContext, [FromBody] Todo todo)
+{
+    httpContext.Items["invoked"] = true;
+}
+app.MapPost("/", TestAction);
+""";
+        var (_, compilation) = await RunGeneratorAsync(source);
+        var serviceProvider = CreateServiceProvider(serviceCollection =>
+        {
+            serviceCollection.Configure<RouteHandlerOptions>(options => options.ThrowOnBadRequest = false);
+        });
+        var endpoint = GetEndpointFromCompilation(compilation, serviceProvider: serviceProvider);
+        var jsonException = new JsonException();
+
+        var httpContext = CreateHttpContext();
+        httpContext.Request.Headers["Content-Type"] = "application/json";
+        httpContext.Request.Headers["Content-Length"] = "1";
+        httpContext.Request.Body = new ExceptionThrowingRequestBodyStream(jsonException);
+        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(2, "InvalidJsonRequestBody"), logMessage.EventId);
+        Assert.Equal(LogLevel.Debug, logMessage.LogLevel);
+        Assert.Equal(@"Failed to read parameter ""Todo todo"" from the request body as JSON.", logMessage.Message);
+        Assert.Same(jsonException, logMessage.Exception);
+        var logValues = Assert.IsAssignableFrom<IReadOnlyList<KeyValuePair<string, object>>>(logMessage.State);
+        Assert.Equal("Todo", logValues[0].Value);
+        Assert.Equal("todo", logValues[1].Value);
+    }
+
+    [Fact]
+    public async Task RequestDelegateThrowsForJsonExceptionsIfThrowOnBadRequest()
+    {
+        var source = """
+void TestAction(HttpContext httpContext, [FromBody] Todo todo)
+{
+    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 jsonException = new JsonException();
+
+        var httpContext = CreateHttpContext();
+        httpContext.Request.Headers["Content-Type"] = "application/json";
+        httpContext.Request.Headers["Content-Length"] = "1";
+        httpContext.Request.Body = new ExceptionThrowingRequestBodyStream(jsonException);
+        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 ""Todo todo"" from the request body as JSON.", badHttpRequestException.Message);
+        Assert.Equal(400, badHttpRequestException.StatusCode);
+        Assert.Same(jsonException, badHttpRequestException.InnerException);
+    }
+
+    [Fact]
+    public async Task RequestDelegateLogsMalformedJsonAsDebugAndSets400Response()
+    {
+        var source = """
+void TestAction(HttpContext httpContext, [FromBody] Todo todo)
+{
+    httpContext.Items["invoked"] = true;
+}
+app.MapPost("/", TestAction);
+""";
+        var (_, compilation) = await RunGeneratorAsync(source);
+        var serviceProvider = CreateServiceProvider(serviceCollection =>
+        {
+            serviceCollection.Configure<RouteHandlerOptions>(options => options.ThrowOnBadRequest = false);
+        });
+        var endpoint = GetEndpointFromCompilation(compilation, serviceProvider: serviceProvider);
+
+        var httpContext = CreateHttpContext();
+        httpContext.Request.Headers["Content-Type"] = "application/json";
+        httpContext.Request.Headers["Content-Length"] = "1";
+        httpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("{"));
+        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(2, "InvalidJsonRequestBody"), logMessage.EventId);
+        Assert.Equal(LogLevel.Debug, logMessage.LogLevel);
+        Assert.Equal(@"Failed to read parameter ""Todo todo"" from the request body as JSON.", logMessage.Message);
+        Assert.IsType<JsonException>(logMessage.Exception);
+        var logValues = Assert.IsAssignableFrom<IReadOnlyList<KeyValuePair<string, object>>>(logMessage.State);
+        Assert.Equal("Todo", logValues[0].Value);
+        Assert.Equal("todo", logValues[1].Value);
+    }
+
+    [Fact]
+    public async Task RequestDelegateThrowsForMalformedJsonIfThrowOnBadRequest()
+    {
+        var source = """
+void TestAction(HttpContext httpContext, [FromBody] Todo todo)
+{
+    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/json";
+        httpContext.Request.Headers["Content-Length"] = "1";
+        httpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("{"));
+        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 ""Todo todo"" from the request body as JSON.", badHttpRequestException.Message);
+        Assert.Equal(400, badHttpRequestException.StatusCode);
+        Assert.IsType<JsonException>(badHttpRequestException.InnerException);
+    }
+}

+ 57 - 45
src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.QueryParameters.cs

@@ -1,71 +1,83 @@
 // Licensed to the .NET Foundation under one or more agreements.
 // The .NET Foundation licenses this file to you under the MIT license.
-using System.Xml.Linq;
-using Microsoft.AspNetCore.Http.RequestDelegateGenerator.StaticRouteHandlerModel;
-using Microsoft.AspNetCore.Mvc;
 
+using System.Text;
+using Microsoft.AspNetCore.Http.RequestDelegateGenerator.StaticRouteHandlerModel;
+using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Primitives;
 
 namespace Microsoft.AspNetCore.Http.Generators.Tests;
 
 public abstract partial class RequestDelegateCreationTests
 {
-    public static object[][] MapAction_ExplicitQueryParam_StringReturn_Data
+    public static IEnumerable<object[]> QueryParamOptionalityData
     {
         get
         {
-            var expectedBody = "TestQueryValue";
-            var fromQueryRequiredSource = """app.MapGet("/", ([FromQuery] string queryValue) => queryValue);""";
-            var fromQueryWithNameRequiredSource = """app.MapGet("/", ([FromQuery(Name = "queryValue")] string parameterName) => parameterName);""";
-            var fromQueryWithNullNameRequiredSource = """app.MapGet("/", ([FromQuery(Name = null)] string queryValue) => queryValue);""";
-            var fromQueryNullableSource = """app.MapGet("/", ([FromQuery] string? queryValue) => queryValue ?? string.Empty);""";
-            var fromQueryDefaultValueSource = """
-#nullable disable
-string getQueryWithDefault([FromQuery] string queryValue = null) => queryValue ?? string.Empty;
-app.MapGet("/", getQueryWithDefault);
-#nullable restore
-""";
-
-            return new[]
-            {
-                new object[] { fromQueryRequiredSource, expectedBody, 200, expectedBody },
-                new object[] { fromQueryRequiredSource, null, 400, string.Empty },
-                new object[] { fromQueryWithNameRequiredSource, expectedBody, 200, expectedBody },
-                new object[] { fromQueryWithNameRequiredSource, null, 400, string.Empty },
-                new object[] { fromQueryWithNullNameRequiredSource, expectedBody, 200, expectedBody },
-                new object[] { fromQueryWithNullNameRequiredSource, null, 400, string.Empty },
-                new object[] { fromQueryNullableSource, expectedBody, 200, expectedBody },
-                new object[] { fromQueryNullableSource, null, 200, string.Empty },
-                new object[] { fromQueryDefaultValueSource, expectedBody, 200, expectedBody },
-                new object[] { fromQueryDefaultValueSource, null, 200, string.Empty },
-            };
+            return new List<object[]>
+                {
+                    new object[] { @"(string name) => $""Hello {name}!""", "name", null, true, null},
+                    new object[] { @"(string name) => $""Hello {name}!""", "name", "TestName", false, "Hello TestName!" },
+                    new object[] { @"(string name = ""DefaultName"") => $""Hello {name}!""", "name", null, false, "Hello DefaultName!" },
+                    new object[] { @"(string name = ""DefaultName"") => $""Hello {name}!""", "name", "TestName", false, "Hello TestName!" },
+                    new object[] { @"(string? name) => $""Hello {name}!""", "name", null, false, "Hello !" },
+                    new object[] { @"(string? name) => $""Hello {name}!""", "name", "TestName", false, "Hello TestName!"},
+
+                    new object[] { @"(int age) => $""Age: {age}""", "age", null, true, null},
+                    new object[] { @"(int age) => $""Age: {age}""", "age", "42", false, "Age: 42" },
+                    new object[] { @"(int age = 12) => $""Age: {age}""", "age", null, false, "Age: 12" },
+                    new object[] { @"(int age = 12) => $""Age: {age}""", "age", "42", false, "Age: 42" },
+                    new object[] { @"(int? age) => $""Age: {age}""", "age", null, false, "Age: " },
+                    new object[] { @"(int? age) => $""Age: {age}""", "age", "42", false, "Age: 42"},
+                };
         }
     }
 
     [Theory]
-    [MemberData(nameof(MapAction_ExplicitQueryParam_StringReturn_Data))]
-    public async Task MapAction_ExplicitQueryParam_StringReturn(string source, string queryValue, int expectedStatusCode, string expectedBody)
+    [MemberData(nameof(QueryParamOptionalityData))]
+    public async Task RequestDelegateHandlesQueryParamOptionality(string innerSource, string paramName, string queryParam, bool isInvalid, string expectedResponse)
     {
-        var (results, compilation) = await RunGeneratorAsync(source);
-        var endpoint = GetEndpointFromCompilation(compilation);
-
-        VerifyStaticEndpointModel(results, (endpointModel) =>
-        {
-            Assert.Equal("/", endpointModel.RoutePattern);
-            Assert.Equal("MapGet", endpointModel.HttpMethod);
-            var p = Assert.Single(endpointModel.Parameters);
-            Assert.Equal(EndpointParameterSource.Query, p.Source);
-            Assert.Equal("queryValue", p.LookupName);
-        });
+        var source = $"""
+string handler{innerSource};
+app.MapGet("/", handler);
+""";
+        var (_, compilation) = await RunGeneratorAsync(source);
+        var serviceProvider = CreateServiceProvider();
+        var endpoint = GetEndpointFromCompilation(compilation, serviceProvider: serviceProvider);
 
         var httpContext = CreateHttpContext();
-        if (queryValue is not null)
+        var responseBodyStream = new MemoryStream();
+        httpContext.Response.Body = responseBodyStream;
+
+        if (queryParam is not null)
         {
-            httpContext.Request.QueryString = new QueryString($"?queryValue={queryValue}");
+            httpContext.Request.Query = new QueryCollection(new Dictionary<string, StringValues>
+            {
+                [paramName] = queryParam
+            });
         }
 
         await endpoint.RequestDelegate(httpContext);
-        await VerifyResponseBodyAsync(httpContext, expectedBody, expectedStatusCode);
+
+        var logs = TestSink.Writes.ToArray();
+
+        if (isInvalid)
+        {
+            Assert.Equal(400, httpContext.Response.StatusCode);
+            var log = Assert.Single(logs);
+            Assert.Equal(LogLevel.Debug, log.LogLevel);
+            Assert.Equal(new EventId(4, "RequiredParameterNotProvided"), log.EventId);
+            var expectedType = paramName == "age" ? "int age" : $"string name";
+            var parameterSource = IsGeneratorEnabled ? "route or query string" : "query string";
+            Assert.Equal($@"Required parameter ""{expectedType}"" was not provided from {parameterSource}.", log.Message);
+        }
+        else
+        {
+            Assert.Equal(200, httpContext.Response.StatusCode);
+            Assert.False(httpContext.RequestAborted.IsCancellationRequested);
+            var decodedResponseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray());
+            Assert.Equal(expectedResponse, decodedResponseBody);
+        }
     }
 
     [Fact]

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

@@ -1,6 +1,11 @@
 // Licensed to the .NET Foundation under one or more agreements.
 // The .NET Foundation licenses this file to you under the MIT license.
+using System.Text;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.AspNetCore.Routing;
 using Microsoft.CodeAnalysis;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
 namespace Microsoft.AspNetCore.Http.Generators.Tests;
 
 public class RuntimeCreationTests : RequestDelegateCreationTests
@@ -21,4 +26,118 @@ app.MapGet("/", ({{bindAsyncType}} myNotBindAsyncParam) => { });
         var ex = Assert.Throws<InvalidOperationException>(() => GetEndpointFromCompilation(compilation));
         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);
+    }
 }

+ 66 - 1
src/Http/Http.Extensions/test/RequestDelegateGenerator/SharedTypes.cs

@@ -2,6 +2,8 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 using System.Diagnostics;
 using System.Reflection;
+using System.Text.Json;
+using System.Text.Json.Serialization;
 using Microsoft.AspNetCore.Http.Metadata;
 
 namespace Microsoft.AspNetCore.Http.Generators.Tests;
@@ -42,6 +44,24 @@ public class TryParseTodo : Todo
     }
 }
 
+[JsonPolymorphic]
+[JsonDerivedType(typeof(JsonTodoChild), nameof(JsonTodoChild))]
+public class JsonTodo : Todo
+{
+    public static async ValueTask<JsonTodo?> BindAsync(HttpContext context, ParameterInfo parameter)
+    {
+        // manually call deserialize so we don't check content type
+        var body = await JsonSerializer.DeserializeAsync<JsonTodo>(context.Request.Body);
+        context.Request.Body.Position = 0;
+        return body;
+    }
+}
+
+public class JsonTodoChild : JsonTodo
+{
+    public string? Child { get; set; }
+}
+
 public class CustomFromBodyAttribute : Attribute, IFromBodyMetadata
 {
     public bool AllowEmpty { get; set; }
@@ -150,7 +170,7 @@ public record MyBindAsyncRecord(Uri Uri)
         {
             throw new UnreachableException($"Unexpected parameter type: {parameter.ParameterType}");
         }
-        if (parameter.Name != "myBindAsyncParam")
+        if (parameter.Name?.StartsWith("myBindAsyncParam", StringComparison.Ordinal) == false)
         {
             throw new UnreachableException("Unexpected parameter name");
         }
@@ -406,3 +426,48 @@ public struct BodyStruct
 }
 
 #nullable restore
+
+public class ExceptionThrowingRequestBodyStream : Stream
+{
+    private readonly Exception _exceptionToThrow;
+
+    public ExceptionThrowingRequestBodyStream(Exception exceptionToThrow)
+    {
+        _exceptionToThrow = exceptionToThrow;
+    }
+
+    public override bool CanRead => true;
+
+    public override bool CanSeek => false;
+
+    public override bool CanWrite => false;
+
+    public override long Length => throw new NotImplementedException();
+
+    public override long Position { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
+
+    public override void Flush()
+    {
+        throw new NotImplementedException();
+    }
+
+    public override int Read(byte[] buffer, int offset, int count)
+    {
+        throw _exceptionToThrow;
+    }
+
+    public override long Seek(long offset, SeekOrigin origin)
+    {
+        throw new NotImplementedException();
+    }
+
+    public override void SetLength(long value)
+    {
+        throw new NotImplementedException();
+    }
+
+    public override void Write(byte[] buffer, int offset, int count)
+    {
+        throw new NotImplementedException();
+    }
+}

+ 46 - 0
src/Shared/RequestDelegateCreationMessages.cs

@@ -0,0 +1,46 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.AspNetCore.Http;
+
+internal static class RequestDelegateCreationLogging
+{
+    public const int RequestBodyIOExceptionEventId = 1;
+    public const string RequestBodyIOExceptionEventName = "RequestBodyIOException";
+    public const string RequestBodyIOExceptionMessage = "Reading the request body failed with an IOException.";
+
+    public const int InvalidJsonRequestBodyEventId = 2;
+    public const string InvalidJsonRequestBodyEventName = "InvalidJsonRequestBody";
+    public const string InvalidJsonRequestBodyLogMessage = @"Failed to read parameter ""{ParameterType} {ParameterName}"" from the request body as JSON.";
+    public const string InvalidJsonRequestBodyExceptionMessage = @"Failed to read parameter ""{0} {1}"" from the request body as JSON.";
+
+    public const int ParameterBindingFailedEventId = 3;
+    public const string ParameterBindingFailedEventName = "ParameterBindingFailed";
+    public const string ParameterBindingFailedLogMessage = @"Failed to bind parameter ""{ParameterType} {ParameterName}"" from ""{SourceValue}"".";
+    public const string ParameterBindingFailedExceptionMessage = @"Failed to bind parameter ""{0} {1}"" from ""{2}"".";
+
+    public const int RequiredParameterNotProvidedEventId = 4;
+    public const string RequiredParameterNotProvidedEventName = "RequiredParameterNotProvided";
+    public const string RequiredParameterNotProvidedLogMessage = @"Required parameter ""{ParameterType} {ParameterName}"" was not provided from {Source}.";
+    public const string RequiredParameterNotProvidedExceptionMessage = @"Required parameter ""{0} {1}"" was not provided from {2}.";
+
+    public const int ImplicitBodyNotProvidedEventId = 5;
+    public const string ImplicitBodyNotProvidedEventName = "ImplicitBodyNotProvided";
+    public const string ImplicitBodyNotProvidedLogMessage = @"Implicit body inferred for parameter ""{ParameterName}"" but no body was provided. Did you mean to use a Service instead?";
+    public const string ImplicitBodyNotProvidedExceptionMessage = @"Implicit body inferred for parameter ""{0}"" but no body was provided. Did you mean to use a Service instead?";
+
+    public const int UnexpectedJsonContentTypeEventId = 6;
+    public const string UnexpectedJsonContentTypeEventName = "UnexpectedContentType";
+    public const string UnexpectedJsonContentTypeLogMessage = @"Expected a supported JSON media type but got ""{ContentType}"".";
+    public const string UnexpectedJsonContentTypeExceptionMessage = @"Expected a supported JSON media type but got ""{0}"".";
+
+    public const int UnexpectedFormContentTypeEventId = 7;
+    public const string UnexpectedFormContentTypeLogEventName = "UnexpectedNonFormContentType";
+    public const string UnexpectedFormContentTypeLogMessage = @"Expected a supported form media type but got ""{ContentType}"".";
+    public const string UnexpectedFormContentTypeExceptionMessage = @"Expected a supported form media type but got ""{0}"".";
+
+    public const int InvalidFormRequestBodyEventId = 8;
+    public const string InvalidFormRequestBodyEventName = "InvalidFormRequestBody";
+    public const string InvalidFormRequestBodyLogMessage = @"Failed to read parameter ""{ParameterType} {ParameterName}"" from the request body as form.";
+    public const string InvalidFormRequestBodyExceptionMessage = @"Failed to read parameter ""{0} {1}"" from the request body as form.";
+}

+ 8 - 0
src/Shared/RoslynUtils/SymbolExtensions.cs

@@ -7,6 +7,7 @@ using System.Collections.Immutable;
 using System.Diagnostics.CodeAnalysis;
 using System.Linq;
 using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
 
 namespace Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure;
 
@@ -123,6 +124,13 @@ internal static class SymbolExtensions
             NullableAnnotation: NullableAnnotation.Annotated
         } || parameterSymbol.HasExplicitDefaultValue;
 
+    public static string GetDefaultValueString(this IParameterSymbol parameterSymbol)
+    {
+        return !parameterSymbol.HasExplicitDefaultValue
+            ? "null"
+            : SymbolDisplay.FormatLiteral((parameterSymbol.ExplicitDefaultValue ?? "null").ToString(), parameterSymbol.ExplicitDefaultValue is string);
+    }
+
     public static bool TryGetNamedArgumentValue<T>(this AttributeData attribute, string argumentName, out T? argumentValue)
     {
         argumentValue = default;