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

Infer default sources for parameters of minimal actions (#31603)

This does a big chunk of #30248 while heavily refactoring the RequestDelegateFactory by splitting it out into a bunch of smaller methods.

For part 2 of #30248, a "scalar value type that lives in the System namespace", I went with the alternative of "Something convention-based like the presence of a bool TryParse(string, out T) method" instead. This conventions work for types in any namespace including user-defined types.
Stephen Halter 5 лет назад
Родитель
Сommit
bd61ac75ac

+ 15 - 18
AspNetCore.sln

@@ -1558,12 +1558,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "BrowserTesting", "BrowserTe
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.BrowserTesting", "src\Shared\BrowserTesting\src\Microsoft.AspNetCore.BrowserTesting.csproj", "{B739074E-6652-4F5B-B37E-775DC2245FEC}"
 EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{722E5A66-D84A-4689-AA87-7197FF5D7070}"
-EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WasmLinkerTest", "src\Components\WebAssembly\testassets\WasmLinkerTest\WasmLinkerTest.csproj", "{3B375FFC-1E38-453E-A26D-A510CCD3339E}"
 EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MapActionSample", "src\Http\Routing\samples\MapActionSample\MapActionSample.csproj", "{8F510BAA-FA6B-4648-8F98-28DF5C69DBB2}"
-EndProject
 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{71287382-95EF-490D-A285-87196E29E88A}"
 EndProject
 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "HostedBlazorWebassemblyApp", "HostedBlazorWebassemblyApp", "{B4226BE2-DCB7-40C5-93F2-94C9BD6F4394}"
@@ -1604,6 +1600,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorWinFormsApp", "src\Co
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Http.Abstractions.Microbenchmarks", "src\Http\Http.Abstractions\perf\Microbenchmarks\Microsoft.AspNetCore.Http.Abstractions.Microbenchmarks.csproj", "{3F752B48-2936-4FCA-B0DC-4AB0F788F897}"
 EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MapActionSample", "src\Http\samples\MapActionSample\MapActionSample.csproj", "{A661D867-708A-494E-8B6B-6558804F9A3F}"
+EndProject
 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "testassets", "testassets", "{F0849E7E-61DB-4849-9368-9E7BC125DCB0}"
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WinFormsTestApp", "src\Components\WebView\Platforms\WindowsForms\testassets\WinFormsTestApp\WinFormsTestApp.csproj", "{99EE7769-3C81-477B-B947-0A5CBCD5B27D}"
@@ -7483,18 +7481,6 @@ Global
 		{3B375FFC-1E38-453E-A26D-A510CCD3339E}.Release|x64.Build.0 = Release|Any CPU
 		{3B375FFC-1E38-453E-A26D-A510CCD3339E}.Release|x86.ActiveCfg = Release|Any CPU
 		{3B375FFC-1E38-453E-A26D-A510CCD3339E}.Release|x86.Build.0 = Release|Any CPU
-		{8F510BAA-FA6B-4648-8F98-28DF5C69DBB2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
-		{8F510BAA-FA6B-4648-8F98-28DF5C69DBB2}.Debug|Any CPU.Build.0 = Debug|Any CPU
-		{8F510BAA-FA6B-4648-8F98-28DF5C69DBB2}.Debug|x64.ActiveCfg = Debug|Any CPU
-		{8F510BAA-FA6B-4648-8F98-28DF5C69DBB2}.Debug|x64.Build.0 = Debug|Any CPU
-		{8F510BAA-FA6B-4648-8F98-28DF5C69DBB2}.Debug|x86.ActiveCfg = Debug|Any CPU
-		{8F510BAA-FA6B-4648-8F98-28DF5C69DBB2}.Debug|x86.Build.0 = Debug|Any CPU
-		{8F510BAA-FA6B-4648-8F98-28DF5C69DBB2}.Release|Any CPU.ActiveCfg = Release|Any CPU
-		{8F510BAA-FA6B-4648-8F98-28DF5C69DBB2}.Release|Any CPU.Build.0 = Release|Any CPU
-		{8F510BAA-FA6B-4648-8F98-28DF5C69DBB2}.Release|x64.ActiveCfg = Release|Any CPU
-		{8F510BAA-FA6B-4648-8F98-28DF5C69DBB2}.Release|x64.Build.0 = Release|Any CPU
-		{8F510BAA-FA6B-4648-8F98-28DF5C69DBB2}.Release|x86.ActiveCfg = Release|Any CPU
-		{8F510BAA-FA6B-4648-8F98-28DF5C69DBB2}.Release|x86.Build.0 = Release|Any CPU
 		{8F6F73F7-0DDA-4AA3-9887-2FB0141786AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{8F6F73F7-0DDA-4AA3-9887-2FB0141786AC}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{8F6F73F7-0DDA-4AA3-9887-2FB0141786AC}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -7627,6 +7613,18 @@ Global
 		{3F752B48-2936-4FCA-B0DC-4AB0F788F897}.Release|x64.Build.0 = Release|Any CPU
 		{3F752B48-2936-4FCA-B0DC-4AB0F788F897}.Release|x86.ActiveCfg = Release|Any CPU
 		{3F752B48-2936-4FCA-B0DC-4AB0F788F897}.Release|x86.Build.0 = Release|Any CPU
+		{A661D867-708A-494E-8B6B-6558804F9A3F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{A661D867-708A-494E-8B6B-6558804F9A3F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{A661D867-708A-494E-8B6B-6558804F9A3F}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{A661D867-708A-494E-8B6B-6558804F9A3F}.Debug|x64.Build.0 = Debug|Any CPU
+		{A661D867-708A-494E-8B6B-6558804F9A3F}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{A661D867-708A-494E-8B6B-6558804F9A3F}.Debug|x86.Build.0 = Debug|Any CPU
+		{A661D867-708A-494E-8B6B-6558804F9A3F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{A661D867-708A-494E-8B6B-6558804F9A3F}.Release|Any CPU.Build.0 = Release|Any CPU
+		{A661D867-708A-494E-8B6B-6558804F9A3F}.Release|x64.ActiveCfg = Release|Any CPU
+		{A661D867-708A-494E-8B6B-6558804F9A3F}.Release|x64.Build.0 = Release|Any CPU
+		{A661D867-708A-494E-8B6B-6558804F9A3F}.Release|x86.ActiveCfg = Release|Any CPU
+		{A661D867-708A-494E-8B6B-6558804F9A3F}.Release|x86.Build.0 = Release|Any CPU
 		{99EE7769-3C81-477B-B947-0A5CBCD5B27D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{99EE7769-3C81-477B-B947-0A5CBCD5B27D}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{99EE7769-3C81-477B-B947-0A5CBCD5B27D}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -8482,9 +8480,7 @@ Global
 		{22EA0993-8DFC-40C2-8481-8E85E21EFB56} = {41BB7BA4-AC08-4E9A-83EA-6D587A5B951C}
 		{8F33439F-5532-45D6-8A44-20EF9104AA9D} = {5F0044F2-4C66-46A8-BD79-075F001AA034}
 		{B739074E-6652-4F5B-B37E-775DC2245FEC} = {8F33439F-5532-45D6-8A44-20EF9104AA9D}
-		{722E5A66-D84A-4689-AA87-7197FF5D7070} = {54C42F57-5447-4C21-9812-4AF665567566}
 		{3B375FFC-1E38-453E-A26D-A510CCD3339E} = {7D2B0799-A634-42AC-AE77-5D167BA51389}
-		{8F510BAA-FA6B-4648-8F98-28DF5C69DBB2} = {722E5A66-D84A-4689-AA87-7197FF5D7070}
 		{71287382-95EF-490D-A285-87196E29E88A} = {562D5067-8CD8-4F19-BCBB-873204932C61}
 		{B4226BE2-DCB7-40C5-93F2-94C9BD6F4394} = {71287382-95EF-490D-A285-87196E29E88A}
 		{8F6F73F7-0DDA-4AA3-9887-2FB0141786AC} = {B4226BE2-DCB7-40C5-93F2-94C9BD6F4394}
@@ -8505,6 +8501,7 @@ Global
 		{3BA297F8-1CA1-492D-AE64-A60B825D8501} = {D4E9A2C5-0838-42DF-BC80-C829C4C9137E}
 		{CC740832-D268-47A3-9058-B9054F8397E2} = {D3B76F4E-A980-45BF-AEA1-EA3175B0B5A1}
 		{3F752B48-2936-4FCA-B0DC-4AB0F788F897} = {DCBBDB52-4A49-4141-8F4D-81C0FFFB7BD5}
+		{A661D867-708A-494E-8B6B-6558804F9A3F} = {EB5E294B-9ED5-43BF-AFA9-1CD2327F3DC1}
 		{F0849E7E-61DB-4849-9368-9E7BC125DCB0} = {D4E9A2C5-0838-42DF-BC80-C829C4C9137E}
 		{99EE7769-3C81-477B-B947-0A5CBCD5B27D} = {F0849E7E-61DB-4849-9368-9E7BC125DCB0}
 		{94D0D6F3-8632-41DE-908B-47A787D570FF} = {5241CF68-66A0-4724-9BAA-36DB959A5B11}

+ 460 - 240
src/Http/Http.Extensions/src/RequestDelegateFactory.cs

@@ -2,9 +2,8 @@
 // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 
 using System;
-using System.Collections.Generic;
+using System.Collections.Concurrent;
 using System.Diagnostics;
-using System.Globalization;
 using System.IO;
 using System.Linq;
 using System.Linq.Expressions;
@@ -23,28 +22,39 @@ namespace Microsoft.AspNetCore.Http
     /// </summary>
     public static class RequestDelegateFactory
     {
-        private static readonly MethodInfo ChangeTypeMethodInfo = GetMethodInfo<Func<object, Type, object>>((value, type) => Convert.ChangeType(value, type, CultureInfo.InvariantCulture));
-        private static readonly MethodInfo ExecuteTaskOfTMethodInfo = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteTask), BindingFlags.NonPublic | BindingFlags.Static)!;
-        private static readonly MethodInfo ExecuteTaskOfStringMethodInfo = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteTaskOfString), BindingFlags.NonPublic | BindingFlags.Static)!;
-        private static readonly MethodInfo ExecuteValueTaskOfTMethodInfo = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteValueTaskOfT), BindingFlags.NonPublic | BindingFlags.Static)!;
-        private static readonly MethodInfo ExecuteValueTaskMethodInfo = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteValueTask), BindingFlags.NonPublic | BindingFlags.Static)!;
-        private static readonly MethodInfo ExecuteValueTaskOfStringMethodInfo = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteValueTaskOfString), BindingFlags.NonPublic | BindingFlags.Static)!;
-        private static readonly MethodInfo ExecuteTaskResultOfTMethodInfo = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteTaskResult), BindingFlags.NonPublic | BindingFlags.Static)!;
-        private static readonly MethodInfo ExecuteValueResultTaskOfTMethodInfo = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteValueTaskResult), BindingFlags.NonPublic | BindingFlags.Static)!;
-        private static readonly MethodInfo GetRequiredServiceMethodInfo = typeof(ServiceProviderServiceExtensions).GetMethod(nameof(ServiceProviderServiceExtensions.GetRequiredService), BindingFlags.Public | BindingFlags.Static, new Type[] { typeof(IServiceProvider) })!;
-        private static readonly MethodInfo ResultWriteResponseAsync = typeof(IResult).GetMethod(nameof(IResult.ExecuteAsync), BindingFlags.Public | BindingFlags.Instance)!;
-        private static readonly MethodInfo StringResultWriteResponseAsync = GetMethodInfo<Func<HttpResponse, string, Task>>((response, text) => HttpResponseWritingExtensions.WriteAsync(response, text, default));
-        private static readonly MethodInfo JsonResultWriteResponseAsync = GetMethodInfo<Func<HttpResponse, object, Task>>((response, value) => HttpResponseJsonExtensions.WriteAsJsonAsync(response, value, default));
-        private static readonly MemberInfo CompletedTaskMemberInfo = GetMemberInfo<Func<Task>>(() => Task.CompletedTask);
-
-        private static readonly ParameterExpression TargetArg = Expression.Parameter(typeof(object), "target");
-        private static readonly ParameterExpression HttpContextParameter = Expression.Parameter(typeof(HttpContext), "httpContext");
-        private static readonly ParameterExpression DeserializedBodyArg = Expression.Parameter(typeof(object), "bodyValue");
-
-        private static readonly MemberExpression RequestServicesExpr = Expression.Property(HttpContextParameter, nameof(HttpContext.RequestServices));
-        private static readonly MemberExpression HttpRequestExpr = Expression.Property(HttpContextParameter, nameof(HttpContext.Request));
-        private static readonly MemberExpression HttpResponseExpr = Expression.Property(HttpContextParameter, nameof(HttpContext.Response));
-        private static readonly MemberExpression RequestAbortedExpr = Expression.Property(HttpContextParameter, nameof(HttpContext.RequestAborted));
+        private static readonly MethodInfo ExecuteTaskOfTMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteTask), BindingFlags.NonPublic | BindingFlags.Static)!;
+        private static readonly MethodInfo ExecuteTaskOfStringMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteTaskOfString), BindingFlags.NonPublic | BindingFlags.Static)!;
+        private static readonly MethodInfo ExecuteValueTaskOfTMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteValueTaskOfT), BindingFlags.NonPublic | BindingFlags.Static)!;
+        private static readonly MethodInfo ExecuteValueTaskMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteValueTask), BindingFlags.NonPublic | BindingFlags.Static)!;
+        private static readonly MethodInfo ExecuteValueTaskOfStringMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteValueTaskOfString), BindingFlags.NonPublic | BindingFlags.Static)!;
+        private static readonly MethodInfo ExecuteTaskResultOfTMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteTaskResult), BindingFlags.NonPublic | BindingFlags.Static)!;
+        private static readonly MethodInfo ExecuteValueResultTaskOfTMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteValueTaskResult), BindingFlags.NonPublic | BindingFlags.Static)!;
+        private static readonly MethodInfo GetRequiredServiceMethod = typeof(ServiceProviderServiceExtensions).GetMethod(nameof(ServiceProviderServiceExtensions.GetRequiredService), BindingFlags.Public | BindingFlags.Static, new Type[] { typeof(IServiceProvider) })!;
+        private static readonly MethodInfo ResultWriteResponseAsyncMethod = typeof(IResult).GetMethod(nameof(IResult.ExecuteAsync), BindingFlags.Public | BindingFlags.Instance)!;
+        private static readonly MethodInfo StringResultWriteResponseAsyncMethod = GetMethodInfo<Func<HttpResponse, string, Task>>((response, text) => HttpResponseWritingExtensions.WriteAsync(response, text, default));
+        private static readonly MethodInfo JsonResultWriteResponseAsyncMethod = GetMethodInfo<Func<HttpResponse, object, Task>>((response, value) => HttpResponseJsonExtensions.WriteAsJsonAsync(response, value, default));
+        private static readonly MethodInfo EnumTryParseMethod = GetEnumTryParseMethod();
+        private static readonly MethodInfo LogParameterBindingFailureMethod = GetMethodInfo<Action<HttpContext, string, string, string>>((httpContext, parameterType, parameterName, sourceValue) =>
+            Log.ParameterBindingFailed(httpContext, parameterType, parameterName, sourceValue));
+
+        private static readonly ParameterExpression TargetExpr = Expression.Parameter(typeof(object), "target");
+        private static readonly ParameterExpression HttpContextExpr = Expression.Parameter(typeof(HttpContext), "httpContext");
+        private static readonly ParameterExpression BodyValueExpr = Expression.Parameter(typeof(object), "bodyValue");
+        private static readonly ParameterExpression WasTryParseFailureExpr = Expression.Variable(typeof(bool), "wasTryParseFailure");
+        private static readonly ParameterExpression TempSourceStringExpr = Expression.Variable(typeof(string), "tempSourceString");
+
+        private static readonly MemberExpression RequestServicesExpr = Expression.Property(HttpContextExpr, nameof(HttpContext.RequestServices));
+        private static readonly MemberExpression HttpRequestExpr = Expression.Property(HttpContextExpr, nameof(HttpContext.Request));
+        private static readonly MemberExpression HttpResponseExpr = Expression.Property(HttpContextExpr, nameof(HttpContext.Response));
+        private static readonly MemberExpression RequestAbortedExpr = Expression.Property(HttpContextExpr, nameof(HttpContext.RequestAborted));
+        private static readonly MemberExpression RouteValuesExpr = Expression.Property(HttpRequestExpr, nameof(HttpRequest.RouteValues));
+        private static readonly MemberExpression QueryExpr = Expression.Property(HttpRequestExpr, nameof(HttpRequest.Query));
+        private static readonly MemberExpression HeadersExpr = Expression.Property(HttpRequestExpr, nameof(HttpRequest.Headers));
+        private static readonly MemberExpression FormExpr = Expression.Property(HttpRequestExpr, nameof(HttpRequest.Form));
+        private static readonly MemberExpression StatusCodeExpr = Expression.Property(HttpResponseExpr, nameof(HttpResponse.StatusCode));
+        private static readonly MemberExpression CompletedTaskExpr = Expression.Property(null, (PropertyInfo)GetMemberInfo<Func<Task>>(() => Task.CompletedTask));
+
+        private static readonly ConcurrentDictionary<Type, MethodInfo?> TryParseMethodCache = new();
 
         /// <summary>
         /// Creates a <see cref="RequestDelegate"/> implementation for <paramref name="action"/>.
@@ -60,15 +70,15 @@ namespace Microsoft.AspNetCore.Http
 
             var targetExpression = action.Target switch
             {
-                object => Expression.Convert(TargetArg, action.Target.GetType()),
+                object => Expression.Convert(TargetExpr, action.Target.GetType()),
                 null => null,
             };
 
-            var untargetedRequestDelegate = CreateRequestDelegate(action.Method, targetExpression);
+            var targetableRequestDelegate = CreateTargetableRequestDelegate(action.Method, targetExpression);
 
             return httpContext =>
             {
-                return untargetedRequestDelegate(action.Target, httpContext);
+                return targetableRequestDelegate(action.Target, httpContext);
             };
         }
 
@@ -84,11 +94,11 @@ namespace Microsoft.AspNetCore.Http
                 throw new ArgumentNullException(nameof(methodInfo));
             }
 
-            var untargetedRequestDelegate = CreateRequestDelegate(methodInfo, targetExpression: null);
+            var targetableRequestDelegate = CreateTargetableRequestDelegate(methodInfo, targetExpression: null);
 
             return httpContext =>
             {
-                return untargetedRequestDelegate(null, httpContext);
+                return targetableRequestDelegate(null, httpContext);
             };
         }
 
@@ -115,16 +125,16 @@ namespace Microsoft.AspNetCore.Http
                 throw new ArgumentException($"A {nameof(targetFactory)} was provided, but {nameof(methodInfo)} does not have a Declaring type.");
             }
 
-            var targetExpression = Expression.Convert(TargetArg, methodInfo.DeclaringType);
-            var untargetedRequestDelegate = CreateRequestDelegate(methodInfo, targetExpression);
+            var targetExpression = Expression.Convert(TargetExpr, methodInfo.DeclaringType);
+            var targetableRequestDelegate = CreateTargetableRequestDelegate(methodInfo, targetExpression);
 
             return httpContext =>
             {
-                return untargetedRequestDelegate(targetFactory(httpContext), httpContext);
+                return targetableRequestDelegate(targetFactory(httpContext), httpContext);
             };
         }
 
-        private static Func<object?, HttpContext, Task> CreateRequestDelegate(MethodInfo methodInfo, Expression? targetExpression)
+        private static Func<object?, HttpContext, Task> CreateTargetableRequestDelegate(MethodInfo methodInfo, Expression? targetExpression)
         {
             // Non void return type
 
@@ -142,235 +152,319 @@ namespace Microsoft.AspNetCore.Http
             //     return default;
             // }
 
-            var consumeBodyDirectly = false;
-            var consumeBodyAsForm = false;
-            Type? bodyType = null;
-            var allowEmptyBody = false;
+            var factoryContext = new FactoryContext();
 
-            // This argument represents the deserialized body returned from IHttpRequestReader
-            // when the method has a FromBody attribute declared
+            var arguments = CreateArguments(methodInfo.GetParameters(), factoryContext);
 
-            var methodParameters = methodInfo.GetParameters();
-            var args = new List<Expression>(methodParameters.Length);
+            var responseWritingMethodCall = factoryContext.CheckForTryParseFailure ?
+                CreateTryParseCheckingResponseWritingMethodCall(methodInfo, targetExpression, arguments) :
+                CreateResponseWritingMethodCall(methodInfo, targetExpression, arguments);
 
-            foreach (var parameter in methodParameters)
+            return HandleRequestBodyAndCompileRequestDelegate(responseWritingMethodCall, factoryContext);
+        }
+
+        private static Expression[] CreateArguments(ParameterInfo[]? parameters, FactoryContext factoryContext)
+        {
+            if (parameters is null || parameters.Length == 0)
             {
-                Expression paramterExpression = Expression.Default(parameter.ParameterType);
+                return Array.Empty<Expression>();
+            }
 
-                var parameterCustomAttributes = parameter.GetCustomAttributes();
+            var args = new Expression[parameters.Length];
 
-                if (parameterCustomAttributes.OfType<IFromRouteMetadata>().FirstOrDefault() is { } routeAttribute)
-                {
-                    var routeValuesProperty = Expression.Property(HttpRequestExpr, nameof(HttpRequest.RouteValues));
-                    paramterExpression = BindParamenter(routeValuesProperty, parameter, routeAttribute.Name);
-                }
-                else if (parameterCustomAttributes.OfType<IFromQueryMetadata>().FirstOrDefault() is { } queryAttribute)
-                {
-                    var queryProperty = Expression.Property(HttpRequestExpr, nameof(HttpRequest.Query));
-                    paramterExpression = BindParamenter(queryProperty, parameter, queryAttribute.Name);
-                }
-                else if (parameterCustomAttributes.OfType<IFromHeaderMetadata>().FirstOrDefault() is { } headerAttribute)
-                {
-                    var headersProperty = Expression.Property(HttpRequestExpr, nameof(HttpRequest.Headers));
-                    paramterExpression = BindParamenter(headersProperty, parameter, headerAttribute.Name);
-                }
-                else if (parameterCustomAttributes.OfType<IFromBodyMetadata>().FirstOrDefault() is { } bodyAttribute)
-                {
-                    if (consumeBodyDirectly)
-                    {
-                        throw new InvalidOperationException("Action cannot have more than one FromBody attribute.");
-                    }
+            for (var i = 0; i < parameters.Length; i++)
+            {
+                args[i] = CreateArgument(parameters[i], factoryContext);
+            }
 
-                    if (consumeBodyAsForm)
-                    {
-                        ThrowCannotReadBodyDirectlyAndAsForm();
-                    }
+            return args;
+        }
 
-                    consumeBodyDirectly = true;
-                    allowEmptyBody = bodyAttribute.AllowEmpty;
-                    bodyType = parameter.ParameterType;
-                    paramterExpression = Expression.Convert(DeserializedBodyArg, bodyType);
-                }
-                else if (parameterCustomAttributes.OfType<IFromFormMetadata>().FirstOrDefault() is { } formAttribute)
-                {
-                    if (consumeBodyDirectly)
-                    {
-                        ThrowCannotReadBodyDirectlyAndAsForm();
-                    }
+        private static Expression CreateArgument(ParameterInfo parameter, FactoryContext factoryContext)
+        {
+            if (parameter.Name is null)
+            {
+                throw new InvalidOperationException("A parameter does not have a name! Was it genererated? All parameters must be named.");
+            }
 
-                    consumeBodyAsForm = true;
+            var parameterCustomAttributes = parameter.GetCustomAttributes();
 
-                    var formProperty = Expression.Property(HttpRequestExpr, nameof(HttpRequest.Form));
-                    paramterExpression = BindParamenter(formProperty, parameter, parameter.Name);
-                }
-                else if (parameter.CustomAttributes.Any(a => typeof(IFromServiceMetadata).IsAssignableFrom(a.AttributeType)))
+            if (parameterCustomAttributes.OfType<IFromRouteMetadata>().FirstOrDefault() is { } routeAttribute)
+            {
+                return BindParameterFromProperty(parameter, RouteValuesExpr, routeAttribute.Name ?? parameter.Name, factoryContext);
+            }
+            else if (parameterCustomAttributes.OfType<IFromQueryMetadata>().FirstOrDefault() is { } queryAttribute)
+            {
+                return BindParameterFromProperty(parameter, QueryExpr, queryAttribute.Name ?? parameter.Name, factoryContext);
+            }
+            else if (parameterCustomAttributes.OfType<IFromHeaderMetadata>().FirstOrDefault() is { } headerAttribute)
+            {
+                return BindParameterFromProperty(parameter, HeadersExpr, headerAttribute.Name ?? parameter.Name, factoryContext);
+            }
+            else if (parameterCustomAttributes.OfType<IFromBodyMetadata>().FirstOrDefault() is { } bodyAttribute)
+            {
+                if (factoryContext.RequestBodyMode is RequestBodyMode.AsJson)
                 {
-                    paramterExpression = Expression.Call(GetRequiredServiceMethodInfo.MakeGenericMethod(parameter.ParameterType), RequestServicesExpr);
+                    throw new InvalidOperationException("Action cannot have more than one FromBody attribute.");
                 }
-                else if (parameter.ParameterType == typeof(IFormCollection))
+
+                if (factoryContext.RequestBodyMode is RequestBodyMode.AsForm)
                 {
-                    if (consumeBodyDirectly)
-                    {
-                        ThrowCannotReadBodyDirectlyAndAsForm();
-                    }
+                    ThrowCannotReadBodyDirectlyAndAsForm();
+                }
 
-                    consumeBodyAsForm = true;
+                factoryContext.RequestBodyMode = RequestBodyMode.AsJson;
+                factoryContext.JsonRequestBodyType = parameter.ParameterType;
+                factoryContext.AllowEmptyRequestBody = bodyAttribute.AllowEmpty;
 
-                    paramterExpression = Expression.Property(HttpRequestExpr, nameof(HttpRequest.Form));
-                }
-                else if (parameter.ParameterType == typeof(HttpContext))
+                return Expression.Convert(BodyValueExpr, parameter.ParameterType);
+            }
+            else if (parameterCustomAttributes.OfType<IFromFormMetadata>().FirstOrDefault() is { } formAttribute)
+            {
+                if (factoryContext.RequestBodyMode is RequestBodyMode.AsJson)
                 {
-                    paramterExpression = HttpContextParameter;
+                    ThrowCannotReadBodyDirectlyAndAsForm();
                 }
-                else if (parameter.ParameterType == typeof(CancellationToken))
+
+                factoryContext.RequestBodyMode = RequestBodyMode.AsForm;
+
+                return BindParameterFromProperty(parameter, FormExpr, formAttribute.Name ?? parameter.Name, factoryContext);
+            }
+            else if (parameter.CustomAttributes.Any(a => typeof(IFromServiceMetadata).IsAssignableFrom(a.AttributeType)))
+            {
+                return Expression.Call(GetRequiredServiceMethod.MakeGenericMethod(parameter.ParameterType), RequestServicesExpr);
+            }
+            else if (parameter.ParameterType == typeof(IFormCollection))
+            {
+                if (factoryContext.RequestBodyMode is RequestBodyMode.AsJson)
                 {
-                    paramterExpression = RequestAbortedExpr;
+                    ThrowCannotReadBodyDirectlyAndAsForm();
                 }
 
-                args.Add(paramterExpression);
+                factoryContext.RequestBodyMode = RequestBodyMode.AsForm;
+
+                return Expression.Property(HttpRequestExpr, nameof(HttpRequest.Form));
             }
+            else if (parameter.ParameterType == typeof(HttpContext))
+            {
+                return HttpContextExpr;
+            }
+            else if (parameter.ParameterType == typeof(CancellationToken))
+            {
+                return RequestAbortedExpr;
+            }
+            else if (parameter.ParameterType == typeof(string) || HasTryParseMethod(parameter))
+            {
+                return BindParameterFromRouteValueOrQueryString(parameter, parameter.Name, factoryContext);
+            }
+            else
+            {
+                return Expression.Call(GetRequiredServiceMethod.MakeGenericMethod(parameter.ParameterType), RequestServicesExpr);
+            }
+        }
 
-            Expression? body = null;
+        private static Expression CreateMethodCall(MethodInfo methodInfo, Expression? target, Expression[] arguments) =>
+            target is null ?
+                Expression.Call(methodInfo, arguments) :
+                Expression.Call(target, methodInfo, arguments);
 
-            MethodCallExpression methodCall;
+        private static Expression CreateResponseWritingMethodCall(MethodInfo methodInfo, Expression? target, Expression[] arguments)
+        {
+            var callMethod = CreateMethodCall(methodInfo, target, arguments);
+            return AddResponseWritingToMethodCall(callMethod, methodInfo.ReturnType);
+        }
 
-            if (targetExpression is null)
+        // If we're calling TryParse and wasTryParseFailure indicates it failed, set a 400 StatusCode instead of calling the method.
+        private static Expression CreateTryParseCheckingResponseWritingMethodCall(MethodInfo methodInfo, Expression? target, Expression[] arguments)
+        {
+            // {
+            //     bool wasTryParseFailure = false;
+            //     string tempSourceString;
+            //
+            //     // Assume "[FromRoute] int id" is the first parameter.
+            //
+            //     tempSourceString = httpContext.RequestValue["id"];
+            //     int param1 = tempSourceString == null ? default :
+            //     {
+            //          int parsedValue = default;
+            //
+            //          if (!int.TryParse(tempSourceString, out parsedValue))
+            //          {
+            //              wasTryParseFailure = true;
+            //              Log.ParameterBindingFailed(httpContext, "Int32", "id", tempSourceString)
+            //          }
+            //
+            //          return parsedValue;
+            //     };
+            //
+            //     tempSourceString = httpContext.RequestValue["param2"];
+            //     int param2 = tempSourceString == null ? default :
+            //     // ...
+            //
+            //     return wasTryParseFailure ?
+            //         {
+            //              httpContext.Response.StatusCode = 400;
+            //              return Task.CompletedTask;
+            //         } :
+            //         {
+            //             // Logic generated by AddResponseWritingToMethodCall() that calls action(param1, parm2, ...)
+            //         };
+            // }
+
+            var parameters = methodInfo.GetParameters();
+            var storedArguments = new ParameterExpression[parameters.Length];
+            var localVariables = new ParameterExpression[parameters.Length + 2];
+
+            for (var i = 0; i < parameters.Length; i++)
             {
-                methodCall = Expression.Call(methodInfo, args);
+                storedArguments[i] = localVariables[i] = Expression.Parameter(parameters[i].ParameterType);
             }
-            else
+
+            localVariables[parameters.Length] = WasTryParseFailureExpr;
+            localVariables[parameters.Length + 1] = TempSourceStringExpr;
+
+            var assignAndCall = new Expression[parameters.Length + 1];
+            for (var i = 0; i < parameters.Length; i++)
             {
-                methodCall = Expression.Call(targetExpression, methodInfo, args);
+                assignAndCall[i] = Expression.Assign(localVariables[i], arguments[i]);
             }
 
+            var set400StatusAndReturnCompletedTask = Expression.Block(
+                    Expression.Assign(StatusCodeExpr, Expression.Constant(400)),
+                    CompletedTaskExpr);
+
+            var methodCall = CreateMethodCall(methodInfo, target, storedArguments);
+
+            var checkWasTryParseFailure = Expression.Condition(WasTryParseFailureExpr,
+                set400StatusAndReturnCompletedTask,
+                AddResponseWritingToMethodCall(methodCall, methodInfo.ReturnType));
+
+            assignAndCall[parameters.Length] = checkWasTryParseFailure;
+
+            return Expression.Block(localVariables, assignAndCall);
+        }
+
+        private static Expression AddResponseWritingToMethodCall(Expression methodCall, Type returnType)
+        {
             // Exact request delegate match
-            if (methodInfo.ReturnType == typeof(void))
+            if (returnType == typeof(void))
             {
-                var bodyExpressions = new List<Expression>
-                {
-                    methodCall,
-                    Expression.Property(null, (PropertyInfo)CompletedTaskMemberInfo)
-                };
-
-                body = Expression.Block(bodyExpressions);
+                return Expression.Block(methodCall, CompletedTaskExpr);
             }
-            else if (AwaitableInfo.IsTypeAwaitable(methodInfo.ReturnType, out var info))
+            else if (AwaitableInfo.IsTypeAwaitable(returnType, out _))
             {
-                if (methodInfo.ReturnType == typeof(Task))
+                if (returnType == typeof(Task))
                 {
-                    body = methodCall;
+                    return methodCall;
                 }
-                else if (methodInfo.ReturnType == typeof(ValueTask))
+                else if (returnType == typeof(ValueTask))
                 {
-                    body = Expression.Call(
-                                        ExecuteValueTaskMethodInfo,
-                                        methodCall);
+                    return Expression.Call(
+                        ExecuteValueTaskMethod,
+                        methodCall);
                 }
-                else if (methodInfo.ReturnType.IsGenericType &&
-                         methodInfo.ReturnType.GetGenericTypeDefinition() == typeof(Task<>))
+                else if (returnType.IsGenericType &&
+                         returnType.GetGenericTypeDefinition() == typeof(Task<>))
                 {
-                    var typeArg = methodInfo.ReturnType.GetGenericArguments()[0];
+                    var typeArg = returnType.GetGenericArguments()[0];
 
                     if (typeof(IResult).IsAssignableFrom(typeArg))
                     {
-                        body = Expression.Call(
-                                           ExecuteTaskResultOfTMethodInfo.MakeGenericMethod(typeArg),
-                                           methodCall,
-                                           HttpContextParameter);
+                        return Expression.Call(
+                            ExecuteTaskResultOfTMethod.MakeGenericMethod(typeArg),
+                            methodCall,
+                            HttpContextExpr);
+                    }
+                    // ExecuteTask<T>(action(..), httpContext);
+                    else if (typeArg == typeof(string))
+                    {
+                        return Expression.Call(
+                            ExecuteTaskOfStringMethod,
+                            methodCall,
+                            HttpContextExpr);
                     }
                     else
                     {
-                        // ExecuteTask<T>(action(..), httpContext);
-                        if (typeArg == typeof(string))
-                        {
-                            body = Expression.Call(
-                                             ExecuteTaskOfStringMethodInfo,
-                                             methodCall,
-                                             HttpContextParameter);
-                        }
-                        else
-                        {
-                            body = Expression.Call(
-                                             ExecuteTaskOfTMethodInfo.MakeGenericMethod(typeArg),
-                                             methodCall,
-                                             HttpContextParameter);
-                        }
+                        return Expression.Call(
+                            ExecuteTaskOfTMethod.MakeGenericMethod(typeArg),
+                            methodCall,
+                            HttpContextExpr);
                     }
                 }
-                else if (methodInfo.ReturnType.IsGenericType &&
-                         methodInfo.ReturnType.GetGenericTypeDefinition() == typeof(ValueTask<>))
+                else if (returnType.IsGenericType &&
+                         returnType.GetGenericTypeDefinition() == typeof(ValueTask<>))
                 {
-                    var typeArg = methodInfo.ReturnType.GetGenericArguments()[0];
+                    var typeArg = returnType.GetGenericArguments()[0];
 
                     if (typeof(IResult).IsAssignableFrom(typeArg))
                     {
-                        body = Expression.Call(
-                                           ExecuteValueResultTaskOfTMethodInfo.MakeGenericMethod(typeArg),
-                                           methodCall,
-                                           HttpContextParameter);
+                        return Expression.Call(
+                            ExecuteValueResultTaskOfTMethod.MakeGenericMethod(typeArg),
+                            methodCall,
+                            HttpContextExpr);
+                    }
+                    // ExecuteTask<T>(action(..), httpContext);
+                    else if (typeArg == typeof(string))
+                    {
+                        return Expression.Call(
+                            ExecuteValueTaskOfStringMethod,
+                            methodCall,
+                            HttpContextExpr);
                     }
                     else
                     {
-                        // ExecuteTask<T>(action(..), httpContext);
-                        if (typeArg == typeof(string))
-                        {
-                            body = Expression.Call(
-                                       ExecuteValueTaskOfStringMethodInfo,
-                                       methodCall,
-                                       HttpContextParameter);
-                        }
-                        else
-                        {
-                            body = Expression.Call(
-                                       ExecuteValueTaskOfTMethodInfo.MakeGenericMethod(typeArg),
-                                       methodCall,
-                                       HttpContextParameter);
-                        }
+                        return Expression.Call(
+                            ExecuteValueTaskOfTMethod.MakeGenericMethod(typeArg),
+                            methodCall,
+                            HttpContextExpr);
                     }
                 }
                 else
                 {
                     // TODO: Handle custom awaitables
-                    throw new NotSupportedException($"Unsupported return type: {methodInfo.ReturnType}");
+                    throw new NotSupportedException($"Unsupported return type: {returnType}");
                 }
             }
-            else if (typeof(IResult).IsAssignableFrom(methodInfo.ReturnType))
+            else if (typeof(IResult).IsAssignableFrom(returnType))
             {
-                body = Expression.Call(methodCall, ResultWriteResponseAsync, HttpContextParameter);
+                return Expression.Call(methodCall, ResultWriteResponseAsyncMethod, HttpContextExpr);
             }
-            else if (methodInfo.ReturnType == typeof(string))
+            else if (returnType == typeof(string))
             {
-                body = Expression.Call(StringResultWriteResponseAsync, HttpResponseExpr, methodCall, Expression.Constant(CancellationToken.None));
+                return Expression.Call(StringResultWriteResponseAsyncMethod, HttpResponseExpr, methodCall, Expression.Constant(CancellationToken.None));
             }
-            else if (methodInfo.ReturnType.IsValueType)
+            else if (returnType.IsValueType)
             {
                 var box = Expression.TypeAs(methodCall, typeof(object));
-                body = Expression.Call(JsonResultWriteResponseAsync, HttpResponseExpr, box, Expression.Constant(CancellationToken.None));
+                return Expression.Call(JsonResultWriteResponseAsyncMethod, HttpResponseExpr, box, Expression.Constant(CancellationToken.None));
             }
             else
             {
-                body = Expression.Call(JsonResultWriteResponseAsync, HttpResponseExpr, methodCall, Expression.Constant(CancellationToken.None));
+                return Expression.Call(JsonResultWriteResponseAsyncMethod, HttpResponseExpr, methodCall, Expression.Constant(CancellationToken.None));
             }
+        }
 
-            Func<object?, HttpContext, Task>? requestDelegate = null;
-
-            if (consumeBodyDirectly)
+        private static Func<object?, HttpContext, Task> HandleRequestBodyAndCompileRequestDelegate(Expression responseWritingMethodCall, FactoryContext factoryContext)
+        {
+            if (factoryContext.RequestBodyMode is RequestBodyMode.AsJson)
             {
                 // We need to generate the code for reading from the body before calling into the delegate
-                var lambda = Expression.Lambda<Func<object?, HttpContext, object?, Task>>(body, TargetArg, HttpContextParameter, DeserializedBodyArg);
-                var invoker = lambda.Compile();
+                var invoker = Expression.Lambda<Func<object?, HttpContext, object?, Task>>(
+                    responseWritingMethodCall, TargetExpr, HttpContextExpr, BodyValueExpr).Compile();
+
+                var bodyType = factoryContext.JsonRequestBodyType!;
                 object? defaultBodyValue = null;
 
-                if (allowEmptyBody && bodyType!.IsValueType)
+                if (factoryContext.AllowEmptyRequestBody && bodyType.IsValueType)
                 {
                     defaultBodyValue = Activator.CreateInstance(bodyType);
                 }
 
-                requestDelegate = async (target, httpContext) =>
+                return async (target, httpContext) =>
                 {
                     object? bodyValue;
 
-                    if (allowEmptyBody && httpContext.Request.ContentLength == 0)
+                    if (factoryContext.AllowEmptyRequestBody && httpContext.Request.ContentLength == 0)
                     {
                         bodyValue = defaultBodyValue;
                     }
@@ -378,17 +472,16 @@ namespace Microsoft.AspNetCore.Http
                     {
                         try
                         {
-                            bodyValue = await httpContext.Request.ReadFromJsonAsync(bodyType!);
+                            bodyValue = await httpContext.Request.ReadFromJsonAsync(bodyType);
                         }
                         catch (IOException ex)
                         {
-                            Log.RequestBodyIOException(GetLogger(httpContext), ex);
-                            httpContext.Abort();
+                            Log.RequestBodyIOException(httpContext, ex);
                             return;
                         }
                         catch (InvalidDataException ex)
                         {
-                            Log.RequestBodyInvalidDataException(GetLogger(httpContext), ex);
+                            Log.RequestBodyInvalidDataException(httpContext, ex);
                             httpContext.Response.StatusCode = 400;
                             return;
                         }
@@ -397,12 +490,12 @@ namespace Microsoft.AspNetCore.Http
                     await invoker(target, httpContext, bodyValue);
                 };
             }
-            else if (consumeBodyAsForm)
+            else if (factoryContext.RequestBodyMode is RequestBodyMode.AsForm)
             {
-                var lambda = Expression.Lambda<Func<object?, HttpContext, Task>>(body, TargetArg, HttpContextParameter);
-                var invoker = lambda.Compile();
+                var invoker = Expression.Lambda<Func<object?, HttpContext, Task>>(
+                    responseWritingMethodCall, TargetExpr, HttpContextExpr).Compile();
 
-                requestDelegate = async (target, httpContext) =>
+                return async (target, httpContext) =>
                 {
                     // Generating async code would just be insane so if the method needs the form populate it here
                     // so the within the method it's cached
@@ -412,13 +505,13 @@ namespace Microsoft.AspNetCore.Http
                     }
                     catch (IOException ex)
                     {
-                        Log.RequestBodyIOException(GetLogger(httpContext), ex);
+                        Log.RequestBodyIOException(httpContext, ex);
                         httpContext.Abort();
                         return;
                     }
                     catch (InvalidDataException ex)
                     {
-                        Log.RequestBodyInvalidDataException(GetLogger(httpContext), ex);
+                        Log.RequestBodyInvalidDataException(httpContext, ex);
                         httpContext.Response.StatusCode = 400;
                         return;
                     }
@@ -428,74 +521,169 @@ namespace Microsoft.AspNetCore.Http
             }
             else
             {
-                var lambda = Expression.Lambda<Func<object?, HttpContext, Task>>(body, TargetArg, HttpContextParameter);
-                var invoker = lambda.Compile();
-
-                requestDelegate = invoker;
+                return Expression.Lambda<Func<object?, HttpContext, Task>>(
+                    responseWritingMethodCall, TargetExpr, HttpContextExpr).Compile();
             }
-
-            return requestDelegate;
         }
 
-        private static ILogger GetLogger(HttpContext httpContext)
+        private static MethodInfo GetEnumTryParseMethod()
         {
-            var loggerFactory = httpContext.RequestServices.GetRequiredService<ILoggerFactory>();
-            return loggerFactory.CreateLogger("Microsoft.AspNetCore.Routing.MapAction");
+            var staticEnumMethods = typeof(Enum).GetMethods(BindingFlags.Public | BindingFlags.Static);
+
+            foreach (var method in staticEnumMethods)
+            {
+                if (!method.IsGenericMethod || method.Name != "TryParse" || method.ReturnType != typeof(bool))
+                {
+                    continue;
+                }
+
+                var tryParseParameters = method.GetParameters();
+
+                if (tryParseParameters.Length == 2 &&
+                    tryParseParameters[0].ParameterType == typeof(string) &&
+                    tryParseParameters[1].IsOut)
+                {
+                    return method;
+                }
+            }
+
+            throw new Exception("static bool System.Enum.TryParse<TEnum>(string? value, out TEnum result) does not exist!!?!?");
         }
 
-        private static Expression BindParamenter(Expression sourceExpression, ParameterInfo parameter, string? name)
+        // TODO: Use InvariantCulture where possible? Or is CurrentCulture fine because it's more flexible?
+        private static MethodInfo? FindTryParseMethod(Type type)
         {
-            var key = name ?? parameter.Name;
-            var type = Nullable.GetUnderlyingType(parameter.ParameterType) ?? parameter.ParameterType;
-            var valueArg = Expression.Convert(
-                                Expression.MakeIndex(sourceExpression,
-                                                     sourceExpression.Type.GetProperty("Item"),
-                                                     new[] { Expression.Constant(key) }),
-                                typeof(string));
+            static MethodInfo? Finder(Type type)
+            {
+                if (type.IsEnum)
+                {
+                    return EnumTryParseMethod.MakeGenericMethod(type);
+                }
 
-            MethodInfo parseMethod = (from m in type.GetMethods(BindingFlags.Public | BindingFlags.Static)
-                                      let parameters = m.GetParameters()
-                                      where m.Name == "Parse" && parameters.Length == 1 && parameters[0].ParameterType == typeof(string)
-                                      select m).FirstOrDefault()!;
+                var staticMethods = type.GetMethods(BindingFlags.Public | BindingFlags.Static);
 
-            Expression? expr = null;
+                foreach (var method in staticMethods)
+                {
+                    if (method.Name != "TryParse" || method.ReturnType != typeof(bool))
+                    {
+                        continue;
+                    }
 
-            if (parseMethod != null)
-            {
-                expr = Expression.Call(parseMethod, valueArg);
-            }
-            else if (parameter.ParameterType != valueArg.Type)
-            {
-                // Convert.ChangeType()
-                expr = Expression.Call(ChangeTypeMethodInfo, valueArg, Expression.Constant(type));
-            }
-            else
-            {
-                expr = valueArg;
+                    var tryParseParameters = method.GetParameters();
+
+                    if (tryParseParameters.Length == 2 &&
+                        tryParseParameters[0].ParameterType == typeof(string) &&
+                        tryParseParameters[1].IsOut &&
+                        tryParseParameters[1].ParameterType == type.MakeByRefType())
+                    {
+                        return method;
+                    }
+                }
+
+                return null;
             }
 
-            if (expr.Type != parameter.ParameterType)
+            return TryParseMethodCache.GetOrAdd(type, Finder);
+        }
+
+        private static bool HasTryParseMethod(ParameterInfo parameter)
+        {
+            var nonNullableParameterType = Nullable.GetUnderlyingType(parameter.ParameterType) ?? parameter.ParameterType;
+            return FindTryParseMethod(nonNullableParameterType) is not null;
+        }
+
+        private static Expression GetValueFromProperty(Expression sourceExpression, string key)
+        {
+            var itemProperty = sourceExpression.Type.GetProperty("Item");
+            var indexArguments = new[] { Expression.Constant(key) };
+            var indexExpression = Expression.MakeIndex(sourceExpression, itemProperty, indexArguments);
+            return Expression.Convert(indexExpression, typeof(string));
+        }
+
+        private static Expression BindParameterFromValue(ParameterInfo parameter, Expression valueExpression, FactoryContext factoryContext)
+        {
+            if (parameter.ParameterType == typeof(string))
             {
-                expr = Expression.Convert(expr, parameter.ParameterType);
+                return valueExpression;
             }
 
-            Expression defaultExpression;
-            if (parameter.HasDefaultValue)
+            var underlyingNullableType = Nullable.GetUnderlyingType(parameter.ParameterType);
+            var nonNullableParameterType = underlyingNullableType ?? parameter.ParameterType;
+            var tryParseMethod = FindTryParseMethod(nonNullableParameterType);
+
+            if (tryParseMethod is null)
             {
-                defaultExpression = Expression.Constant(parameter.DefaultValue);
+                throw new InvalidOperationException($"No public static bool {parameter.ParameterType.Name}.TryParse(string, out {parameter.ParameterType.Name}) method found for {parameter.Name}.");
             }
-            else
+
+            // bool wasTryParseFailure = false;
+            // string tempSourceString;
+            //
+            // // Assume "[FromRoute] int id" is the first parameter.
+            // tempSourceString = httpContext.RequestValue["id"];
+            //
+            // int param1 = tempSourceString == null ? default :
+            // {
+            //      int parsedValue = default;
+            //
+            //      if (!int.TryParse(tempSourceString, out parsedValue))
+            //      {
+            //          wasTryParseFailure = true;
+            //          Log.ParameterBindingFailed(httpContext, "Int32", "id", tempSourceString)
+            //      }
+            //
+            //      return parsedValue;
+            // };
+
+            factoryContext.CheckForTryParseFailure = true;
+
+            var parsedValue = Expression.Variable(nonNullableParameterType);
+
+            var parameterTypeNameConstant = Expression.Constant(parameter.ParameterType.Name);
+            var parameterNameConstant = Expression.Constant(parameter.Name);
+
+            var failBlock = Expression.Block(
+                Expression.Assign(WasTryParseFailureExpr, Expression.Constant(true)),
+                Expression.Call(LogParameterBindingFailureMethod,
+                    HttpContextExpr, parameterTypeNameConstant, parameterNameConstant, TempSourceStringExpr));
+
+            var tryParseCall = Expression.Call(tryParseMethod, TempSourceStringExpr, parsedValue);
+            var ifFailExpression = Expression.IfThen(Expression.Not(tryParseCall), failBlock);
+
+            Expression parsedValueExpression = Expression.Block(new[] { parsedValue },
+                ifFailExpression,
+                parsedValue);
+
+            // Convert back to nullable if necessary.
+            if (underlyingNullableType is not null)
             {
-                defaultExpression = Expression.Default(parameter.ParameterType);
+                parsedValueExpression = Expression.Convert(parsedValueExpression, parameter.ParameterType);
             }
 
-            // property[key] == null ? default : (ParameterType){Type}.Parse(property[key]);
-            expr = Expression.Condition(
-                Expression.Equal(valueArg, Expression.Constant(null)),
+            Expression defaultExpression = parameter.HasDefaultValue ?
+                Expression.Constant(parameter.DefaultValue) :
+                Expression.Default(parameter.ParameterType);
+
+            // tempSourceString = httpContext.RequestValue["id"];
+            var storeValueToTemp = Expression.Assign(TempSourceStringExpr, valueExpression);
+
+            // int param1 = tempSourcString == null ? default : ...
+            var ternary = Expression.Condition(
+                Expression.Equal(TempSourceStringExpr, Expression.Constant(null)),
                 defaultExpression,
-                expr);
+                parsedValueExpression);
+
+            return Expression.Block(storeValueToTemp, ternary);
+        }
 
-            return expr;
+        private static Expression BindParameterFromProperty(ParameterInfo parameter, MemberExpression property, string key, FactoryContext factoryContext) =>
+            BindParameterFromValue(parameter, GetValueFromProperty(property, key), factoryContext);
+
+        private static Expression BindParameterFromRouteValueOrQueryString(ParameterInfo parameter, string key, FactoryContext factoryContext)
+        {
+            var routeValue = GetValueFromProperty(RouteValuesExpr, key);
+            var queryValue = GetValueFromProperty(QueryExpr, key);
+            return BindParameterFromValue(parameter, Expression.Coalesce(routeValue, queryValue), factoryContext);
         }
 
         private static MethodInfo GetMethodInfo<T>(Expression<T> expr)
@@ -611,6 +799,22 @@ namespace Microsoft.AspNetCore.Http
             throw new InvalidOperationException("Action cannot mix FromBody and FromForm on the same method.");
         }
 
+        private enum RequestBodyMode
+        {
+            None,
+            AsJson,
+            AsForm,
+        }
+
+        private class FactoryContext
+        {
+            public RequestBodyMode RequestBodyMode { get; set; }
+            public Type? JsonRequestBodyType { get; set; }
+            public bool AllowEmptyRequestBody { get; set; }
+
+            public bool CheckForTryParseFailure { get; set; }
+        }
+
         private static class Log
         {
             private static readonly Action<ILogger, Exception> _requestBodyIOException = LoggerMessage.Define(
@@ -623,14 +827,30 @@ namespace Microsoft.AspNetCore.Http
                 new EventId(2, "RequestBodyInvalidDataException"),
                 "Reading the request body failed with an InvalidDataException.");
 
-            public static void RequestBodyIOException(ILogger logger, IOException exception)
+            private static readonly Action<ILogger, string, string, string, Exception?> _parameterBindingFailed = LoggerMessage.Define<string, string, string>(
+                LogLevel.Debug,
+                new EventId(3, "ParamaterBindingFailed"),
+                @"Failed to bind parameter ""{ParameterType} {ParameterName}"" from ""{SourceValue}"".");
+
+            public static void RequestBodyIOException(HttpContext httpContext, IOException exception)
+            {
+                _requestBodyIOException(GetLogger(httpContext), exception);
+            }
+
+            public static void RequestBodyInvalidDataException(HttpContext httpContext, InvalidDataException exception)
+            {
+                _requestBodyInvalidDataException(GetLogger(httpContext), exception);
+            }
+
+            public static void ParameterBindingFailed(HttpContext httpContext, string parameterTypeName, string parameterName, string sourceValue)
             {
-                _requestBodyIOException(logger, exception);
+                _parameterBindingFailed(GetLogger(httpContext), parameterTypeName, parameterName, sourceValue, null);
             }
 
-            public static void RequestBodyInvalidDataException(ILogger logger, InvalidDataException exception)
+            private static ILogger GetLogger(HttpContext httpContext)
             {
-                _requestBodyInvalidDataException(logger, exception);
+                var loggerFactory = httpContext.RequestServices.GetRequiredService<ILoggerFactory>();
+                return loggerFactory.CreateLogger(typeof(RequestDelegateFactory));
             }
         }
     }

+ 277 - 111
src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs

@@ -7,7 +7,12 @@ using System;
 using System.Collections.Generic;
 using System.Globalization;
 using System.IO;
+using System.Linq.Expressions;
+using System.Net;
+using System.Net.Sockets;
+using System.Numerics;
 using System.Reflection;
+using System.Reflection.Metadata;
 using System.Text;
 using System.Text.Json;
 using System.Threading;
@@ -15,15 +20,15 @@ using System.Threading.Tasks;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Http.Features;
 using Microsoft.AspNetCore.Http.Metadata;
+using Microsoft.AspNetCore.Testing;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Logging.Testing;
 using Microsoft.Extensions.Primitives;
 using Xunit;
 
 namespace Microsoft.AspNetCore.Routing.Internal
 {
-    public class RequestDelegateFactoryTests
+    public class RequestDelegateFactoryTests : LoggedTest
     {
         public static IEnumerable<object[]> NoResult
         {
@@ -124,7 +129,7 @@ namespace Microsoft.AspNetCore.Routing.Internal
                 _invokedValue = invokedValue;
             }
 
-            private void NonStaticTestAction(HttpContext httpContext)
+            public void NonStaticTestAction(HttpContext httpContext)
             {
                 httpContext.Items.Add("invoked", _invokedValue);
             }
@@ -132,10 +137,11 @@ namespace Microsoft.AspNetCore.Routing.Internal
 
         [Fact]
         public async Task NonStaticMethodInfoOverloadWorksWithBasicReflection()
+
         {
             var methodInfo = typeof(TestNonStaticActionClass).GetMethod(
-                "NonStaticTestAction",
-                BindingFlags.NonPublic | BindingFlags.Instance,
+                nameof(TestNonStaticActionClass.NonStaticTestAction),
+                BindingFlags.Public | BindingFlags.Instance,
                 new[] { typeof(HttpContext) });
 
             var invoked = false;
@@ -185,103 +191,46 @@ namespace Microsoft.AspNetCore.Routing.Internal
             Assert.Equal("targetFactory", exNullTargetFactory.ParamName);
         }
 
-        public static IEnumerable<object[]> FromRouteResult
-        {
-            get
-            {
-                void TestAction(HttpContext httpContext, [FromRoute] int value)
-                {
-                    StoreInput(httpContext, value);
-                };
-
-                Task TaskTestAction(HttpContext httpContext, [FromRoute] int value)
-                {
-                    StoreInput(httpContext, value);
-                    return Task.CompletedTask;
-                }
-
-                ValueTask ValueTaskTestAction(HttpContext httpContext, [FromRoute] int value)
-                {
-                    StoreInput(httpContext, value);
-                    return ValueTask.CompletedTask;
-                }
-
-                return new List<object[]>
-                {
-                    new object[] { (Action<HttpContext, int>)TestAction },
-                    new object[] { (Func<HttpContext, int, Task>)TaskTestAction },
-                    new object[] { (Func<HttpContext, int, ValueTask>)ValueTaskTestAction },
-                };
-            }
-        }
-        private static void StoreInput(HttpContext httpContext, object value)
-        {
-            httpContext.Items.Add("input", value);
-        }
-
-        [Theory]
-        [MemberData(nameof(FromRouteResult))]
-        public async Task RequestDelegatePopulatesFromRouteParameterBasedOnParameterName(Delegate @delegate)
+        [Fact]
+        public async Task RequestDelegatePopulatesFromRouteParameterBasedOnParameterName()
         {
             const string paramName = "value";
             const int originalRouteParam = 42;
 
+            void TestAction(HttpContext httpContext, [FromRoute] int value)
+            {
+                httpContext.Items.Add("input", value);
+            }
+
             var httpContext = new DefaultHttpContext();
             httpContext.Request.RouteValues[paramName] = originalRouteParam.ToString(NumberFormatInfo.InvariantInfo);
 
-            var requestDelegate = RequestDelegateFactory.Create(@delegate);
+            var requestDelegate = RequestDelegateFactory.Create((Action<HttpContext, int>)TestAction);
 
             await requestDelegate(httpContext);
 
-            Assert.Equal(originalRouteParam, httpContext.Items["input"] as int?);
-        }
-
-        public static IEnumerable<object[]> FromRouteOptionalResult
-        {
-            get
-            {
-                return new List<object[]>
-                {
-                    new object[] { (Action<HttpContext, int>)TestAction },
-                    new object[] { (Func<HttpContext, int, Task>)TaskTestAction },
-                    new object[] { (Func<HttpContext, int, ValueTask>)ValueTaskTestAction }
-                };
-            }
+            Assert.Equal(originalRouteParam, httpContext.Items["input"]);
         }
 
         private static void TestAction(HttpContext httpContext, [FromRoute] int value = 42)
         {
-            StoreInput(httpContext, value);
-        }
-
-        private static Task TaskTestAction(HttpContext httpContext, [FromRoute] int value = 42)
-        {
-            StoreInput(httpContext, value);
-            return Task.CompletedTask;
-        }
-
-        private static ValueTask ValueTaskTestAction(HttpContext httpContext, [FromRoute] int value = 42)
-        {
-            StoreInput(httpContext, value);
-            return ValueTask.CompletedTask;
+            httpContext.Items.Add("input", value);
         }
 
-        [Theory]
-        [MemberData(nameof(FromRouteOptionalResult))]
-        public async Task RequestDelegatePopulatesFromRouteOptionalParameter(Delegate @delegate)
+        [Fact]
+        public async Task RequestDelegatePopulatesFromRouteOptionalParameter()
         {
             var httpContext = new DefaultHttpContext();
 
-            var requestDelegate = RequestDelegateFactory.Create(@delegate);
+            var requestDelegate = RequestDelegateFactory.Create((Action<HttpContext, int>)TestAction);
 
             await requestDelegate(httpContext);
 
-            Assert.Equal(42, httpContext.Items["input"] as int?);
+            Assert.Equal(42, httpContext.Items["input"]);
         }
 
-        [Theory]
-        [MemberData(nameof(FromRouteOptionalResult))]
-        public async Task RequestDelegatePopulatesFromRouteOptionalParameterBasedOnParameterName(Delegate @delegate)
+        [Fact]
+        public async Task RequestDelegatePopulatesFromRouteOptionalParameterBasedOnParameterName()
         {
             const string paramName = "value";
             const int originalRouteParam = 47;
@@ -290,11 +239,11 @@ namespace Microsoft.AspNetCore.Routing.Internal
 
             httpContext.Request.RouteValues[paramName] = originalRouteParam.ToString(NumberFormatInfo.InvariantInfo);
 
-            var requestDelegate = RequestDelegateFactory.Create(@delegate);
+            var requestDelegate = RequestDelegateFactory.Create((Action<HttpContext, int>)TestAction);
 
             await requestDelegate(httpContext);
 
-            Assert.Equal(47, httpContext.Items["input"] as int?);
+            Assert.Equal(47, httpContext.Items["input"]);
         }
 
         [Fact]
@@ -343,6 +292,206 @@ namespace Microsoft.AspNetCore.Routing.Internal
             Assert.Equal(0, deserializedRouteParam);
         }
 
+        public static object?[][] TryParsableParameters
+        {
+            get
+            {
+                static void Store<T>(HttpContext httpContext, T tryParsable)
+                {
+                    httpContext.Items["tryParsable"] = tryParsable;
+                }
+
+                var now = DateTime.Now;
+
+                return new[]
+                {
+                    // string is not technically "TryParsable", but it's the special case.
+                    new object[] { (Action<HttpContext, string>)Store, "plain string", "plain string" },
+                    new object[] { (Action<HttpContext, int>)Store, "-42", -42 },
+                    new object[] { (Action<HttpContext, uint>)Store, "42", 42U },
+                    new object[] { (Action<HttpContext, bool>)Store, "true", true },
+                    new object[] { (Action<HttpContext, short>)Store, "-42", (short)-42 },
+                    new object[] { (Action<HttpContext, ushort>)Store, "42", (ushort)42 },
+                    new object[] { (Action<HttpContext, long>)Store, "-42", -42L },
+                    new object[] { (Action<HttpContext, ulong>)Store, "42", 42UL },
+                    new object[] { (Action<HttpContext, IntPtr>)Store, "-42", new IntPtr(-42) },
+                    new object[] { (Action<HttpContext, char>)Store, "A", 'A' },
+                    new object[] { (Action<HttpContext, double>)Store, "0.5", 0.5 },
+                    new object[] { (Action<HttpContext, float>)Store, "0.5", 0.5f },
+                    new object[] { (Action<HttpContext, Half>)Store, "0.5", (Half)0.5f },
+                    new object[] { (Action<HttpContext, decimal>)Store, "0.5", 0.5m },
+                    new object[] { (Action<HttpContext, DateTime>)Store, now.ToString("o"), now },
+                    new object[] { (Action<HttpContext, DateTimeOffset>)Store, "1970-01-01T00:00:00.0000000+00:00", DateTimeOffset.UnixEpoch },
+                    new object[] { (Action<HttpContext, TimeSpan>)Store, "00:00:42", TimeSpan.FromSeconds(42) },
+                    new object[] { (Action<HttpContext, Guid>)Store, "00000000-0000-0000-0000-000000000000", Guid.Empty },
+                    new object[] { (Action<HttpContext, Version>)Store, "6.0.0.42", new Version("6.0.0.42") },
+                    new object[] { (Action<HttpContext, BigInteger>)Store, "-42", new BigInteger(-42) },
+                    new object[] { (Action<HttpContext, IPAddress>)Store, "127.0.0.1", IPAddress.Loopback },
+                    new object[] { (Action<HttpContext, IPEndPoint>)Store, "127.0.0.1:80", new IPEndPoint(IPAddress.Loopback, 80) },
+                    new object[] { (Action<HttpContext, AddressFamily>)Store, "Unix", AddressFamily.Unix },
+                    new object[] { (Action<HttpContext, ILOpCode>)Store, "Nop", ILOpCode.Nop },
+                    new object[] { (Action<HttpContext, AssemblyFlags>)Store, "PublicKey,Retargetable", AssemblyFlags.PublicKey | AssemblyFlags.Retargetable },
+                    new object[] { (Action<HttpContext, int?>)Store, "42", 42 },
+                    new object[] { (Action<HttpContext, MyEnum>)Store, "ValueB", MyEnum.ValueB },
+                    new object[] { (Action<HttpContext, MyTryParsableRecord>)Store, "https://example.org", new MyTryParsableRecord(new Uri("https://example.org")) },
+                    new object?[] { (Action<HttpContext, int>)Store, null, 0 },
+                    new object?[] { (Action<HttpContext, int?>)Store, null, null },
+                };
+            }
+        }
+
+        private enum MyEnum { ValueA, ValueB, }
+
+        private record MyTryParsableRecord(Uri Uri)
+        {
+            public static bool TryParse(string? value, out MyTryParsableRecord? result)
+            {
+                if (!Uri.TryCreate(value, UriKind.Absolute, out var uri))
+                {
+                    result = null;
+                    return false;
+                }
+
+                result = new MyTryParsableRecord(uri);
+                return true;
+            }
+        }
+
+        [Theory]
+        [MemberData(nameof(TryParsableParameters))]
+        public async Task RequestDelegatePopulatesUnattributedTryParsableParametersFromRouteValue(Delegate action, string? routeValue, object? expectedParameterValue)
+        {
+            var invalidDataException = new InvalidDataException();
+            var serviceCollection = new ServiceCollection();
+            serviceCollection.AddSingleton(LoggerFactory);
+
+            var httpContext = new DefaultHttpContext();
+            httpContext.Request.RouteValues["tryParsable"] = routeValue;
+            httpContext.Features.Set<IHttpRequestLifetimeFeature>(new TestHttpRequestLifetimeFeature());
+            httpContext.RequestServices = serviceCollection.BuildServiceProvider();
+
+            var requestDelegate = RequestDelegateFactory.Create(action);
+
+            await requestDelegate(httpContext);
+
+            Assert.Equal(expectedParameterValue, httpContext.Items["tryParsable"]);
+        }
+
+        [Theory]
+        [MemberData(nameof(TryParsableParameters))]
+        public async Task RequestDelegatePopulatesUnattributedTryParsableParametersFromQueryString(Delegate action, string? routeValue, object? expectedParameterValue)
+        {
+            var httpContext = new DefaultHttpContext();
+            httpContext.Request.Query = new QueryCollection(new Dictionary<string, StringValues>
+            {
+                ["tryParsable"] = routeValue
+            });
+
+            var requestDelegate = RequestDelegateFactory.Create(action);
+
+            await requestDelegate(httpContext);
+
+            Assert.Equal(expectedParameterValue, httpContext.Items["tryParsable"]);
+        }
+
+        [Fact]
+        public async Task RequestDelegatePopulatesUnattributedTryParsableParametersFromRouteValueBeforeQueryString()
+        {
+            var httpContext = new DefaultHttpContext();
+
+            httpContext.Request.RouteValues["tryParsable"] = "42";
+            httpContext.Request.Query = new QueryCollection(new Dictionary<string, StringValues>
+            {
+                ["tryParsable"] = "invalid!"
+            });
+
+            var requestDelegate = RequestDelegateFactory.Create((Action<HttpContext, int>)((httpContext, tryParsable) =>
+            {
+                    httpContext.Items["tryParsable"] = tryParsable;
+            }));
+
+            await requestDelegate(httpContext);
+
+            Assert.Equal(42, httpContext.Items["tryParsable"]);
+        }
+
+        public static object[][] DelegatesWithInvalidAttributes
+        {
+            get
+            {
+                void InvalidFromRoute([FromRoute] object notTryParsable) { }
+                void InvalidFromQuery([FromQuery] object notTryParsable) { }
+                void InvalidFromHeader([FromHeader] object notTryParsable) { }
+                void InvalidFromForm([FromForm] object notTryParsable) { }
+
+                return new[]
+                {
+                    new object[] { (Action<object>)InvalidFromRoute },
+                    new object[] { (Action<object>)InvalidFromQuery },
+                    new object[] { (Action<object>)InvalidFromHeader },
+                    new object[] { (Action<object>)InvalidFromForm },
+                };
+            }
+        }
+
+        [Theory]
+        [MemberData(nameof(DelegatesWithInvalidAttributes))]
+        public void CreateThrowsInvalidOperationExceptionWhenAttributeRequiresTryParseMethodThatDoesNotExist(Delegate action)
+        {
+            var ex = Assert.Throws<InvalidOperationException>(() => RequestDelegateFactory.Create(action));
+            Assert.Equal("No public static bool Object.TryParse(string, out Object) method found for notTryParsable.", ex.Message);
+        }
+
+        [Fact]
+        public void CreateThrowsInvalidOperationExceptionGivenUnnamedArgument()
+        {
+            var unnamedParameter = Expression.Parameter(typeof(int));
+            var lambda = Expression.Lambda(Expression.Block(), unnamedParameter);
+            var ex = Assert.Throws<InvalidOperationException>(() => RequestDelegateFactory.Create((Action<int>)lambda.Compile()));
+            Assert.Equal("A parameter does not have a name! Was it genererated? All parameters must be named.", ex.Message);
+        }
+
+        [Fact]
+        public async Task RequestDelegateLogsTryParsableFailuresAsDebugAndSets400Response()
+        {
+            var invoked = false;
+
+            void TestAction([FromRoute] int tryParsable, [FromRoute] int tryParsable2)
+            {
+                invoked = true;
+            }
+
+            var invalidDataException = new InvalidDataException();
+            var serviceCollection = new ServiceCollection();
+            serviceCollection.AddSingleton(LoggerFactory);
+
+            var httpContext = new DefaultHttpContext();
+            httpContext.Request.RouteValues["tryParsable"] = "invalid!";
+            httpContext.Request.RouteValues["tryParsable2"] = "invalid again!";
+            httpContext.Features.Set<IHttpRequestLifetimeFeature>(new TestHttpRequestLifetimeFeature());
+            httpContext.RequestServices = serviceCollection.BuildServiceProvider();
+
+            var requestDelegate = RequestDelegateFactory.Create((Action<int, int>)TestAction);
+
+            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(3, "ParamaterBindingFailed"), logs[0].EventId);
+            Assert.Equal(LogLevel.Debug, logs[0].LogLevel);
+            Assert.Equal(@"Failed to bind parameter ""Int32 tryParsable"" from ""invalid!"".", logs[0].Message);
+
+            Assert.Equal(new EventId(3, "ParamaterBindingFailed"), logs[0].EventId);
+            Assert.Equal(LogLevel.Debug, logs[0].LogLevel);
+            Assert.Equal(@"Failed to bind parameter ""Int32 tryParsable2"" from ""invalid again!"".", logs[1].Message);
+        }
+
         [Fact]
         public async Task RequestDelegatePopulatesFromQueryParameterBasedOnParameterName()
         {
@@ -485,13 +634,10 @@ namespace Microsoft.AspNetCore.Routing.Internal
         }
 
         [Fact]
-        public async Task RequestDelegateLogsFromBodyIOExceptionsAsDebugAndAborts()
+        public async Task RequestDelegateLogsFromBodyIOExceptionsAsDebugAndDoesNotAbort()
         {
             var invoked = false;
 
-            var sink = new TestSink(context => context.LoggerName == "Microsoft.AspNetCore.Routing.MapAction");
-            var testLoggerFactory = new TestLoggerFactory(sink, enabled: true);
-
             void TestAction([FromBody] Todo todo)
             {
                 invoked = true;
@@ -499,7 +645,7 @@ namespace Microsoft.AspNetCore.Routing.Internal
 
             var ioException = new IOException();
             var serviceCollection = new ServiceCollection();
-            serviceCollection.AddSingleton<ILoggerFactory>(testLoggerFactory);
+            serviceCollection.AddSingleton(LoggerFactory);
 
             var httpContext = new DefaultHttpContext();
             httpContext.Request.Headers["Content-Type"] = "application/json";
@@ -512,9 +658,9 @@ namespace Microsoft.AspNetCore.Routing.Internal
             await requestDelegate(httpContext);
 
             Assert.False(invoked);
-            Assert.True(httpContext.RequestAborted.IsCancellationRequested);
+            Assert.False(httpContext.RequestAborted.IsCancellationRequested);
 
-            var logMessage = Assert.Single(sink.Writes);
+            var logMessage = Assert.Single(TestSink.Writes);
             Assert.Equal(new EventId(1, "RequestBodyIOException"), logMessage.EventId);
             Assert.Equal(LogLevel.Debug, logMessage.LogLevel);
             Assert.Same(ioException, logMessage.Exception);
@@ -525,9 +671,6 @@ namespace Microsoft.AspNetCore.Routing.Internal
         {
             var invoked = false;
 
-            var sink = new TestSink(context => context.LoggerName == "Microsoft.AspNetCore.Routing.MapAction");
-            var testLoggerFactory = new TestLoggerFactory(sink, enabled: true);
-
             void TestAction([FromBody] Todo todo)
             {
                 invoked = true;
@@ -535,7 +678,7 @@ namespace Microsoft.AspNetCore.Routing.Internal
 
             var invalidDataException = new InvalidDataException();
             var serviceCollection = new ServiceCollection();
-            serviceCollection.AddSingleton<ILoggerFactory>(testLoggerFactory);
+            serviceCollection.AddSingleton(LoggerFactory);
 
             var httpContext = new DefaultHttpContext();
             httpContext.Request.Headers["Content-Type"] = "application/json";
@@ -551,7 +694,7 @@ namespace Microsoft.AspNetCore.Routing.Internal
             Assert.False(httpContext.RequestAborted.IsCancellationRequested);
             Assert.Equal(400, httpContext.Response.StatusCode);
 
-            var logMessage = Assert.Single(sink.Writes);
+            var logMessage = Assert.Single(TestSink.Writes);
             Assert.Equal(new EventId(2, "RequestBodyInvalidDataException"), logMessage.EventId);
             Assert.Equal(LogLevel.Debug, logMessage.LogLevel);
             Assert.Same(invalidDataException, logMessage.Exception);
@@ -590,9 +733,6 @@ namespace Microsoft.AspNetCore.Routing.Internal
         {
             var invoked = false;
 
-            var sink = new TestSink(context => context.LoggerName == "Microsoft.AspNetCore.Routing.MapAction");
-            var testLoggerFactory = new TestLoggerFactory(sink, enabled: true);
-
             void TestAction([FromForm] int value)
             {
                 invoked = true;
@@ -600,7 +740,7 @@ namespace Microsoft.AspNetCore.Routing.Internal
 
             var ioException = new IOException();
             var serviceCollection = new ServiceCollection();
-            serviceCollection.AddSingleton<ILoggerFactory>(testLoggerFactory);
+            serviceCollection.AddSingleton(LoggerFactory);
 
             var httpContext = new DefaultHttpContext();
             httpContext.Request.Headers["Content-Type"] = "application/x-www-form-urlencoded";
@@ -615,7 +755,7 @@ namespace Microsoft.AspNetCore.Routing.Internal
             Assert.False(invoked);
             Assert.True(httpContext.RequestAborted.IsCancellationRequested);
 
-            var logMessage = Assert.Single(sink.Writes);
+            var logMessage = Assert.Single(TestSink.Writes);
             Assert.Equal(new EventId(1, "RequestBodyIOException"), logMessage.EventId);
             Assert.Equal(LogLevel.Debug, logMessage.LogLevel);
             Assert.Same(ioException, logMessage.Exception);
@@ -626,9 +766,6 @@ namespace Microsoft.AspNetCore.Routing.Internal
         {
             var invoked = false;
 
-            var sink = new TestSink(context => context.LoggerName == "Microsoft.AspNetCore.Routing.MapAction");
-            var testLoggerFactory = new TestLoggerFactory(sink, enabled: true);
-
             void TestAction([FromForm] int value)
             {
                 invoked = true;
@@ -636,7 +773,7 @@ namespace Microsoft.AspNetCore.Routing.Internal
 
             var invalidDataException = new InvalidDataException();
             var serviceCollection = new ServiceCollection();
-            serviceCollection.AddSingleton<ILoggerFactory>(testLoggerFactory);
+            serviceCollection.AddSingleton(LoggerFactory);
 
             var httpContext = new DefaultHttpContext();
             httpContext.Request.Headers["Content-Type"] = "application/x-www-form-urlencoded";
@@ -652,7 +789,7 @@ namespace Microsoft.AspNetCore.Routing.Internal
             Assert.False(httpContext.RequestAborted.IsCancellationRequested);
             Assert.Equal(400, httpContext.Response.StatusCode);
 
-            var logMessage = Assert.Single(sink.Writes);
+            var logMessage = Assert.Single(TestSink.Writes);
             Assert.Equal(new EventId(2, "RequestBodyInvalidDataException"), logMessage.EventId);
             Assert.Equal(LogLevel.Debug, logMessage.LogLevel);
             Assert.Same(invalidDataException, logMessage.Exception);
@@ -676,16 +813,33 @@ namespace Microsoft.AspNetCore.Routing.Internal
             Assert.Throws<InvalidOperationException>(() => RequestDelegateFactory.Create((Action<int, int>)TestAction));
         }
 
-        [Fact]
-        public async Task RequestDelegatePopulatesFromServiceParameterBasedOnParameterType()
+        public static object[][] FromServiceParameter
         {
-            var myOriginalService = new MyService();
-            MyService? injectedService = null;
-
-            void TestAction([FromService] MyService myService)
+            get
             {
-                injectedService = myService;
+                void TestExplicitFromService(HttpContext httpContext, [FromService] MyService myService)
+                {
+                    httpContext.Items.Add("service", myService);
+                }
+
+                void TestImpliedFromService(HttpContext httpContext, MyService myService)
+                {
+                    httpContext.Items.Add("service", myService);
+                }
+
+                return new[]
+                {
+                    new[] { (Action<HttpContext, MyService>)TestExplicitFromService },
+                    new[] { (Action<HttpContext, MyService>)TestImpliedFromService },
+                };
             }
+        }
+
+        [Theory]
+        [MemberData(nameof(FromServiceParameter))]
+        public async Task RequestDelegatePopulatesParametersFromServiceWithAndWithoutAttribute(Delegate action)
+        {
+            var myOriginalService = new MyService();
 
             var serviceCollection = new ServiceCollection();
             serviceCollection.AddSingleton(myOriginalService);
@@ -693,11 +847,23 @@ namespace Microsoft.AspNetCore.Routing.Internal
             var httpContext = new DefaultHttpContext();
             httpContext.RequestServices = serviceCollection.BuildServiceProvider();
 
-            var requestDelegate = RequestDelegateFactory.Create((Action<MyService>)TestAction);
+            var requestDelegate = RequestDelegateFactory.Create((Action<HttpContext, MyService>)action);
 
             await requestDelegate(httpContext);
 
-            Assert.Same(myOriginalService, injectedService);
+            Assert.Same(myOriginalService, httpContext.Items["service"]);
+        }
+
+        [Theory]
+        [MemberData(nameof(FromServiceParameter))]
+        public async Task RequestDelegateRequiresServiceForAllFromServiceParameters(Delegate action)
+        {
+            var httpContext = new DefaultHttpContext();
+            httpContext.RequestServices = (new ServiceCollection()).BuildServiceProvider();
+
+            var requestDelegate = RequestDelegateFactory.Create((Action<HttpContext, MyService>)action);
+
+            await Assert.ThrowsAsync<InvalidOperationException>(() => requestDelegate(httpContext));
         }
 
         [Fact]

+ 6 - 8
src/Http/HttpAbstractions.slnf

@@ -1,4 +1,4 @@
-{
+{
   "solution": {
     "path": "..\\..\\AspNetCore.sln",
     "projects": [
@@ -11,13 +11,14 @@
       "src\\Http\\Authentication.Core\\test\\Microsoft.AspNetCore.Authentication.Core.Test.csproj",
       "src\\Http\\Headers\\src\\Microsoft.Net.Http.Headers.csproj",
       "src\\Http\\Headers\\test\\Microsoft.Net.Http.Headers.Tests.csproj",
+      "src\\Http\\Http.Abstractions\\perf\\Microbenchmarks\\Microsoft.AspNetCore.Http.Abstractions.Microbenchmarks.csproj",
       "src\\Http\\Http.Abstractions\\src\\Microsoft.AspNetCore.Http.Abstractions.csproj",
       "src\\Http\\Http.Abstractions\\test\\Microsoft.AspNetCore.Http.Abstractions.Tests.csproj",
       "src\\Http\\Http.Extensions\\src\\Microsoft.AspNetCore.Http.Extensions.csproj",
       "src\\Http\\Http.Extensions\\test\\Microsoft.AspNetCore.Http.Extensions.Tests.csproj",
       "src\\Http\\Http.Features\\src\\Microsoft.AspNetCore.Http.Features.csproj",
       "src\\Http\\Http.Features\\test\\Microsoft.AspNetCore.Http.Features.Tests.csproj",
-      "src\\Http\\Http\\perf\\Microsoft.AspNetCore.Http.Performance.csproj",
+      "src\\Http\\Http\\perf\\Microbenchmarks\\Microsoft.AspNetCore.Http.Microbenchmarks.csproj",
       "src\\Http\\Http\\src\\Microsoft.AspNetCore.Http.csproj",
       "src\\Http\\Http\\test\\Microsoft.AspNetCore.Http.Tests.csproj",
       "src\\Http\\Metadata\\src\\Microsoft.AspNetCore.Metadata.csproj",
@@ -25,19 +26,17 @@
       "src\\Http\\Owin\\test\\Microsoft.AspNetCore.Owin.Tests.csproj",
       "src\\Http\\Routing.Abstractions\\src\\Microsoft.AspNetCore.Routing.Abstractions.csproj",
       "src\\Http\\Routing.Abstractions\\test\\Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests.csproj",
-      "src\\Http\\Http\\perf\\Microbenchmarks\\Microsoft.AspNetCore.Http.Microbenchmarks.csproj",
-      "src\\Http\\Http.Abstractions\\perf\\Microbenchmarks\\Microsoft.AspNetCore.Http.Abstractions.Microbenchmarks.csproj",
       "src\\Http\\Routing\\perf\\Microbenchmarks\\Microsoft.AspNetCore.Routing.Microbenchmarks.csproj",
-      "src\\Http\\Routing\\samples\\MapActionSample\\MapActionSample.csproj",
       "src\\Http\\Routing\\src\\Microsoft.AspNetCore.Routing.csproj",
       "src\\Http\\Routing\\test\\FunctionalTests\\Microsoft.AspNetCore.Routing.FunctionalTests.csproj",
       "src\\Http\\Routing\\test\\UnitTests\\Microsoft.AspNetCore.Routing.Tests.csproj",
       "src\\Http\\Routing\\test\\testassets\\Benchmarks\\Benchmarks.csproj",
       "src\\Http\\Routing\\test\\testassets\\RoutingSandbox\\RoutingSandbox.csproj",
       "src\\Http\\Routing\\test\\testassets\\RoutingWebSite\\RoutingWebSite.csproj",
-      "src\\Servers\\Kestrel\\Kestrel\\src\\Microsoft.AspNetCore.Server.Kestrel.csproj",
+      "src\\Http\\WebUtilities\\perf\\Microbenchmarks\\Microsoft.AspNetCore.WebUtilities.Microbenchmarks.csproj",
       "src\\Http\\WebUtilities\\src\\Microsoft.AspNetCore.WebUtilities.csproj",
       "src\\Http\\WebUtilities\\test\\Microsoft.AspNetCore.WebUtilities.Tests.csproj",
+      "src\\Http\\samples\\MapActionSample\\MapActionSample.csproj",
       "src\\Http\\samples\\SampleApp\\HttpAbstractions.SampleApp.csproj",
       "src\\Middleware\\CORS\\src\\Microsoft.AspNetCore.Cors.csproj",
       "src\\Middleware\\HttpOverrides\\src\\Microsoft.AspNetCore.HttpOverrides.csproj",
@@ -50,8 +49,7 @@
       "src\\Servers\\Kestrel\\Kestrel\\src\\Microsoft.AspNetCore.Server.Kestrel.csproj",
       "src\\Servers\\Kestrel\\Transport.Sockets\\src\\Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.csproj",
       "src\\Testing\\src\\Microsoft.AspNetCore.Testing.csproj",
-      "src\\WebEncoders\\src\\Microsoft.Extensions.WebEncoders.csproj",
-      "src\\Http\\WebUtilities\\perf\\Microbenchmarks\\Microsoft.AspNetCore.WebUtilities.Microbenchmarks.csproj"
+      "src\\WebEncoders\\src\\Microsoft.Extensions.WebEncoders.csproj"
     ]
   }
 }

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

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

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

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

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

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

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


+ 33 - 0
src/Http/samples/MapActionSample/Program.cs

@@ -0,0 +1,33 @@
+using System;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+
+using var host = Host.CreateDefaultBuilder(args)
+    .ConfigureWebHostDefaults(webBuilder =>
+    {
+        webBuilder.Configure(app =>
+        {
+            app.UseRouting();
+
+            app.UseEndpoints(endpoints =>
+            {
+                Todo EchoTodo([FromBody] Todo todo) => todo;
+                endpoints.MapPost("/EchoTodo", (Func<Todo, Todo>)EchoTodo);
+
+                string Plaintext() => "Hello, World!";
+                endpoints.MapGet("/plaintext", (Func<string>)Plaintext);
+
+                object Json() => new { message = "Hello, World!" };
+                endpoints.MapGet("/json", (Func<object>)Json);
+            });
+
+        });
+    })
+    .Build();
+
+await host.RunAsync();
+
+record Todo(int Id, string Name, bool IsComplete);

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


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


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