@@ -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