Skip to content

Commit 1329a6f

Browse files
authored
Added support for type based parameter binding (#35496)
* Added support for type based parameter binding - Added a convention that allows custom async binding logic to run for parameters that have a static BindAsync method that takes an HttpContext and return a ValueTask of object. This allows customers to write custom binders based solely on type (it's an extension of the existing TryParse pattern). - There's allocation overhead per request once there's a parameter binder for a delegate. This is because we need to box all of the arguments since we're not using generated code to compute data from the list of binders. - Changed TryParse tests to BindAsync tests and added more tests.
1 parent 57aac27 commit 1329a6f

File tree

6 files changed

+338
-150
lines changed

6 files changed

+338
-150
lines changed

src/Http/Http.Extensions/src/RequestDelegateFactory.cs

Lines changed: 122 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,11 @@ public static partial class RequestDelegateFactory
4141
Log.ParameterBindingFailed(httpContext, parameterType, parameterName, sourceValue));
4242
private static readonly MethodInfo LogRequiredParameterNotProvidedMethod = GetMethodInfo<Action<HttpContext, string, string>>((httpContext, parameterType, parameterName) =>
4343
Log.RequiredParameterNotProvided(httpContext, parameterType, parameterName));
44-
private static readonly MethodInfo LogParameterBindingFromHttpContextFailedMethod = GetMethodInfo<Action<HttpContext, string, string>>((httpContext, parameterType, parameterName) =>
45-
Log.ParameterBindingFromHttpContextFailed(httpContext, parameterType, parameterName));
4644

4745
private static readonly ParameterExpression TargetExpr = Expression.Parameter(typeof(object), "target");
4846
private static readonly ParameterExpression BodyValueExpr = Expression.Parameter(typeof(object), "bodyValue");
4947
private static readonly ParameterExpression WasParamCheckFailureExpr = Expression.Variable(typeof(bool), "wasParamCheckFailure");
48+
private static readonly ParameterExpression BoundValuesArrayExpr = Expression.Parameter(typeof(object[]), "boundValues");
5049

5150
private static ParameterExpression HttpContextExpr => TryParseMethodCache.HttpContextExpr;
5251
private static readonly MemberExpression RequestServicesExpr = Expression.Property(HttpContextExpr, typeof(HttpContext).GetProperty(nameof(HttpContext.RequestServices))!);
@@ -198,7 +197,6 @@ private static Expression[] CreateArguments(ParameterInfo[]? parameters, Factory
198197
{
199198
var errorMessage = BuildErrorMessageForMultipleBodyParameters(factoryContext);
200199
throw new InvalidOperationException(errorMessage);
201-
202200
}
203201

204202
return args;
@@ -263,9 +261,9 @@ private static Expression CreateArgument(ParameterInfo parameter, FactoryContext
263261
{
264262
return RequestAbortedExpr;
265263
}
266-
else if (TryParseMethodCache.HasTryParseHttpContextMethod(parameter))
264+
else if (TryParseMethodCache.HasBindAsyncMethod(parameter))
267265
{
268-
return BindParameterFromTryParseHttpContext(parameter, factoryContext);
266+
return BindParameterFromBindAsync(parameter, factoryContext);
269267
}
270268
else if (parameter.ParameterType == typeof(string) || TryParseMethodCache.HasTryParseStringMethod(parameter))
271269
{
@@ -275,7 +273,6 @@ private static Expression CreateArgument(ParameterInfo parameter, FactoryContext
275273
// when RDF.Create is manually invoked.
276274
if (factoryContext.RouteParameters is { } routeParams)
277275
{
278-
279276
if (routeParams.Contains(parameter.Name, StringComparer.OrdinalIgnoreCase))
280277
{
281278
// We're in the fallback case and we have a parameter and route parameter match so don't fallback
@@ -361,7 +358,6 @@ private static Expression CreateParamCheckingResponseWritingMethodCall(
361358
var localVariables = new ParameterExpression[factoryContext.ExtraLocals.Count + 1];
362359
var checkParamAndCallMethod = new Expression[factoryContext.ParamCheckExpressions.Count + 1];
363360

364-
365361
for (var i = 0; i < factoryContext.ExtraLocals.Count; i++)
366362
{
367363
localVariables[i] = factoryContext.ExtraLocals[i];
@@ -508,14 +504,33 @@ private static Expression AddResponseWritingToMethodCall(Expression methodCall,
508504
{
509505
if (factoryContext.JsonRequestBodyType is null)
510506
{
507+
if (factoryContext.ParameterBinders.Count > 0)
508+
{
509+
// We need to generate the code for reading from the custom binders calling into the delegate
510+
var continuation = Expression.Lambda<Func<object?, HttpContext, object?[], Task>>(
511+
responseWritingMethodCall, TargetExpr, HttpContextExpr, BoundValuesArrayExpr).Compile();
512+
513+
// Looping over arrays is faster
514+
var binders = factoryContext.ParameterBinders.ToArray();
515+
var count = binders.Length;
516+
517+
return async (target, httpContext) =>
518+
{
519+
var boundValues = new object?[count];
520+
521+
for (var i = 0; i < count; i++)
522+
{
523+
boundValues[i] = await binders[i](httpContext);
524+
}
525+
526+
await continuation(target, httpContext, boundValues);
527+
};
528+
}
529+
511530
return Expression.Lambda<Func<object?, HttpContext, Task>>(
512531
responseWritingMethodCall, TargetExpr, HttpContextExpr).Compile();
513532
}
514533

515-
// We need to generate the code for reading from the body before calling into the delegate
516-
var invoker = Expression.Lambda<Func<object?, HttpContext, object?, Task>>(
517-
responseWritingMethodCall, TargetExpr, HttpContextExpr, BodyValueExpr).Compile();
518-
519534
var bodyType = factoryContext.JsonRequestBodyType;
520535
object? defaultBodyValue = null;
521536

@@ -524,31 +539,82 @@ private static Expression AddResponseWritingToMethodCall(Expression methodCall,
524539
defaultBodyValue = Activator.CreateInstance(bodyType);
525540
}
526541

527-
return async (target, httpContext) =>
542+
if (factoryContext.ParameterBinders.Count > 0)
528543
{
529-
object? bodyValue = defaultBodyValue;
530-
var feature = httpContext.Features.Get<IHttpRequestBodyDetectionFeature>();
531-
if (feature?.CanHaveBody == true)
544+
// We need to generate the code for reading from the body before calling into the delegate
545+
var continuation = Expression.Lambda<Func<object?, HttpContext, object?, object?[], Task>>(
546+
responseWritingMethodCall, TargetExpr, HttpContextExpr, BodyValueExpr, BoundValuesArrayExpr).Compile();
547+
548+
// Looping over arrays is faster
549+
var binders = factoryContext.ParameterBinders.ToArray();
550+
var count = binders.Length;
551+
552+
return async (target, httpContext) =>
532553
{
533-
try
554+
// Run these first so that they can potentially read and rewind the body
555+
var boundValues = new object?[count];
556+
557+
for (var i = 0; i < count; i++)
534558
{
535-
bodyValue = await httpContext.Request.ReadFromJsonAsync(bodyType);
559+
boundValues[i] = await binders[i](httpContext);
536560
}
537-
catch (IOException ex)
561+
562+
var bodyValue = defaultBodyValue;
563+
var feature = httpContext.Features.Get<IHttpRequestBodyDetectionFeature>();
564+
if (feature?.CanHaveBody == true)
538565
{
539-
Log.RequestBodyIOException(httpContext, ex);
540-
return;
566+
try
567+
{
568+
bodyValue = await httpContext.Request.ReadFromJsonAsync(bodyType);
569+
}
570+
catch (IOException ex)
571+
{
572+
Log.RequestBodyIOException(httpContext, ex);
573+
return;
574+
}
575+
catch (InvalidDataException ex)
576+
{
577+
Log.RequestBodyInvalidDataException(httpContext, ex);
578+
httpContext.Response.StatusCode = 400;
579+
return;
580+
}
541581
}
542-
catch (InvalidDataException ex)
543-
{
544582

545-
Log.RequestBodyInvalidDataException(httpContext, ex);
546-
httpContext.Response.StatusCode = 400;
547-
return;
583+
await continuation(target, httpContext, bodyValue, boundValues);
584+
};
585+
}
586+
else
587+
{
588+
// We need to generate the code for reading from the body before calling into the delegate
589+
var continuation = Expression.Lambda<Func<object?, HttpContext, object?, Task>>(
590+
responseWritingMethodCall, TargetExpr, HttpContextExpr, BodyValueExpr).Compile();
591+
592+
return async (target, httpContext) =>
593+
{
594+
var bodyValue = defaultBodyValue;
595+
var feature = httpContext.Features.Get<IHttpRequestBodyDetectionFeature>();
596+
if (feature?.CanHaveBody == true)
597+
{
598+
try
599+
{
600+
bodyValue = await httpContext.Request.ReadFromJsonAsync(bodyType);
601+
}
602+
catch (IOException ex)
603+
{
604+
Log.RequestBodyIOException(httpContext, ex);
605+
return;
606+
}
607+
catch (InvalidDataException ex)
608+
{
609+
610+
Log.RequestBodyInvalidDataException(httpContext, ex);
611+
httpContext.Response.StatusCode = 400;
612+
return;
613+
}
548614
}
549-
}
550-
await invoker(target, httpContext, bodyValue);
551-
};
615+
await continuation(target, httpContext, bodyValue);
616+
};
617+
}
552618
}
553619

554620
private static Expression GetValueFromProperty(Expression sourceExpression, string key)
@@ -747,40 +813,40 @@ private static Expression BindParameterFromRouteValueOrQueryString(ParameterInfo
747813
return BindParameterFromValue(parameter, Expression.Coalesce(routeValue, queryValue), factoryContext);
748814
}
749815

750-
private static Expression BindParameterFromTryParseHttpContext(ParameterInfo parameter, FactoryContext factoryContext)
816+
private static Expression BindParameterFromBindAsync(ParameterInfo parameter, FactoryContext factoryContext)
751817
{
752-
// bool wasParamCheckFailure = false;
753-
//
754-
// // Assume "Foo param1" is the first parameter and "public static bool TryParse(HttpContext context, out Foo foo)" exists.
755-
// Foo param1_local;
756-
//
757-
// if (!Foo.TryParse(httpContext, out param1_local))
758-
// {
759-
// wasParamCheckFailure = true;
760-
// Log.ParameterBindingFromHttpContextFailed(httpContext, "Foo", "foo")
761-
// }
762-
763-
var argument = Expression.Variable(parameter.ParameterType, $"{parameter.Name}_local");
764-
var tryParseMethodCall = TryParseMethodCache.FindTryParseHttpContextMethod(parameter.ParameterType);
818+
// We reference the boundValues array by parameter index here
819+
var nullability = NullabilityContext.Create(parameter);
820+
var isOptional = parameter.HasDefaultValue || nullability.ReadState == NullabilityState.Nullable;
765821

766-
// There's no way to opt-in to using a TryParse method on HttpContext other than defining the method, so it's guaranteed to exist here.
767-
Debug.Assert(tryParseMethodCall is not null);
822+
// Get the BindAsync method
823+
var body = TryParseMethodCache.FindBindAsyncMethod(parameter.ParameterType)!;
768824

769-
var parameterTypeNameConstant = Expression.Constant(parameter.ParameterType.Name);
770-
var parameterNameConstant = Expression.Constant(parameter.Name);
825+
// Compile the delegate to the BindAsync method for this parameter index
826+
var bindAsyncDelegate = Expression.Lambda<Func<HttpContext, ValueTask<object?>>>(body, HttpContextExpr).Compile();
827+
factoryContext.ParameterBinders.Add(bindAsyncDelegate);
771828

772-
var failBlock = Expression.Block(
773-
Expression.Assign(WasParamCheckFailureExpr, Expression.Constant(true)),
774-
Expression.Call(LogParameterBindingFromHttpContextFailedMethod,
775-
HttpContextExpr, parameterTypeNameConstant, parameterNameConstant));
829+
// boundValues[index]
830+
var boundValueExpr = Expression.ArrayIndex(BoundValuesArrayExpr, Expression.Constant(factoryContext.ParameterBinders.Count - 1));
776831

777-
var tryParseCall = tryParseMethodCall(argument);
778-
var fullParamCheckBlock = Expression.IfThen(Expression.Not(tryParseCall), failBlock);
832+
if (!isOptional)
833+
{
834+
var checkRequiredBodyBlock = Expression.Block(
835+
Expression.IfThen(
836+
Expression.Equal(boundValueExpr, Expression.Constant(null)),
837+
Expression.Block(
838+
Expression.Assign(WasParamCheckFailureExpr, Expression.Constant(true)),
839+
Expression.Call(LogRequiredParameterNotProvidedMethod,
840+
HttpContextExpr, Expression.Constant(parameter.ParameterType.Name), Expression.Constant(parameter.Name))
841+
)
842+
)
843+
);
779844

780-
factoryContext.ExtraLocals.Add(argument);
781-
factoryContext.ParamCheckExpressions.Add(fullParamCheckBlock);
845+
factoryContext.ParamCheckExpressions.Add(checkRequiredBodyBlock);
846+
}
782847

783-
return argument;
848+
// (ParamterType)boundValues[i]
849+
return Expression.Convert(boundValueExpr, parameter.ParameterType);
784850
}
785851

786852
private static Expression BindParameterFromBody(ParameterInfo parameter, bool allowEmpty, FactoryContext factoryContext)
@@ -793,7 +859,6 @@ private static Expression BindParameterFromBody(ParameterInfo parameter, bool al
793859
{
794860
factoryContext.TrackedParameters.Remove(parameterName);
795861
factoryContext.TrackedParameters.Add(parameterName, "UNKNOWN");
796-
797862
}
798863
}
799864

@@ -925,7 +990,6 @@ static async Task ExecuteAwaited(Task<T> task, HttpContext httpContext)
925990

926991
private static Task ExecuteTaskOfString(Task<string?> task, HttpContext httpContext)
927992
{
928-
929993
SetPlaintextContentType(httpContext);
930994
EnsureRequestTaskNotNull(task);
931995

@@ -1032,6 +1096,7 @@ private class FactoryContext
10321096
public bool UsingTempSourceString { get; set; }
10331097
public List<ParameterExpression> ExtraLocals { get; } = new();
10341098
public List<Expression> ParamCheckExpressions { get; } = new();
1099+
public List<Func<HttpContext, ValueTask<object?>>> ParameterBinders { get; } = new();
10351100

10361101
public Dictionary<string, string> TrackedParameters { get; } = new();
10371102
public bool HasMultipleBodyParameters { get; set; }

0 commit comments

Comments
 (0)