@@ -41,12 +41,11 @@ public static partial class RequestDelegateFactory
41
41
Log . ParameterBindingFailed ( httpContext , parameterType , parameterName , sourceValue ) ) ;
42
42
private static readonly MethodInfo LogRequiredParameterNotProvidedMethod = GetMethodInfo < Action < HttpContext , string , string > > ( ( httpContext , parameterType , parameterName ) =>
43
43
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 ) ) ;
46
44
47
45
private static readonly ParameterExpression TargetExpr = Expression . Parameter ( typeof ( object ) , "target" ) ;
48
46
private static readonly ParameterExpression BodyValueExpr = Expression . Parameter ( typeof ( object ) , "bodyValue" ) ;
49
47
private static readonly ParameterExpression WasParamCheckFailureExpr = Expression . Variable ( typeof ( bool ) , "wasParamCheckFailure" ) ;
48
+ private static readonly ParameterExpression BoundValuesArrayExpr = Expression . Parameter ( typeof ( object [ ] ) , "boundValues" ) ;
50
49
51
50
private static ParameterExpression HttpContextExpr => TryParseMethodCache . HttpContextExpr ;
52
51
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
198
197
{
199
198
var errorMessage = BuildErrorMessageForMultipleBodyParameters ( factoryContext ) ;
200
199
throw new InvalidOperationException ( errorMessage ) ;
201
-
202
200
}
203
201
204
202
return args ;
@@ -263,9 +261,9 @@ private static Expression CreateArgument(ParameterInfo parameter, FactoryContext
263
261
{
264
262
return RequestAbortedExpr ;
265
263
}
266
- else if ( TryParseMethodCache . HasTryParseHttpContextMethod ( parameter ) )
264
+ else if ( TryParseMethodCache . HasBindAsyncMethod ( parameter ) )
267
265
{
268
- return BindParameterFromTryParseHttpContext ( parameter , factoryContext ) ;
266
+ return BindParameterFromBindAsync ( parameter , factoryContext ) ;
269
267
}
270
268
else if ( parameter . ParameterType == typeof ( string ) || TryParseMethodCache . HasTryParseStringMethod ( parameter ) )
271
269
{
@@ -275,7 +273,6 @@ private static Expression CreateArgument(ParameterInfo parameter, FactoryContext
275
273
// when RDF.Create is manually invoked.
276
274
if ( factoryContext . RouteParameters is { } routeParams )
277
275
{
278
-
279
276
if ( routeParams . Contains ( parameter . Name , StringComparer . OrdinalIgnoreCase ) )
280
277
{
281
278
// 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(
361
358
var localVariables = new ParameterExpression [ factoryContext . ExtraLocals . Count + 1 ] ;
362
359
var checkParamAndCallMethod = new Expression [ factoryContext . ParamCheckExpressions . Count + 1 ] ;
363
360
364
-
365
361
for ( var i = 0 ; i < factoryContext . ExtraLocals . Count ; i ++ )
366
362
{
367
363
localVariables [ i ] = factoryContext . ExtraLocals [ i ] ;
@@ -508,14 +504,33 @@ private static Expression AddResponseWritingToMethodCall(Expression methodCall,
508
504
{
509
505
if ( factoryContext . JsonRequestBodyType is null )
510
506
{
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
+
511
530
return Expression . Lambda < Func < object ? , HttpContext , Task > > (
512
531
responseWritingMethodCall , TargetExpr , HttpContextExpr ) . Compile ( ) ;
513
532
}
514
533
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
-
519
534
var bodyType = factoryContext . JsonRequestBodyType ;
520
535
object ? defaultBodyValue = null ;
521
536
@@ -524,31 +539,82 @@ private static Expression AddResponseWritingToMethodCall(Expression methodCall,
524
539
defaultBodyValue = Activator . CreateInstance ( bodyType ) ;
525
540
}
526
541
527
- return async ( target , httpContext ) =>
542
+ if ( factoryContext . ParameterBinders . Count > 0 )
528
543
{
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 ) =>
532
553
{
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 ++ )
534
558
{
535
- bodyValue = await httpContext . Request . ReadFromJsonAsync ( bodyType ) ;
559
+ boundValues [ i ] = await binders [ i ] ( httpContext ) ;
536
560
}
537
- catch ( IOException ex )
561
+
562
+ var bodyValue = defaultBodyValue ;
563
+ var feature = httpContext . Features . Get < IHttpRequestBodyDetectionFeature > ( ) ;
564
+ if ( feature ? . CanHaveBody == true )
538
565
{
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
+ }
541
581
}
542
- catch ( InvalidDataException ex )
543
- {
544
582
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
+ }
548
614
}
549
- }
550
- await invoker ( target , httpContext , bodyValue ) ;
551
- } ;
615
+ await continuation ( target , httpContext , bodyValue ) ;
616
+ } ;
617
+ }
552
618
}
553
619
554
620
private static Expression GetValueFromProperty ( Expression sourceExpression , string key )
@@ -747,40 +813,40 @@ private static Expression BindParameterFromRouteValueOrQueryString(ParameterInfo
747
813
return BindParameterFromValue ( parameter , Expression . Coalesce ( routeValue , queryValue ) , factoryContext ) ;
748
814
}
749
815
750
- private static Expression BindParameterFromTryParseHttpContext ( ParameterInfo parameter , FactoryContext factoryContext )
816
+ private static Expression BindParameterFromBindAsync ( ParameterInfo parameter , FactoryContext factoryContext )
751
817
{
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 ;
765
821
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 ) ! ;
768
824
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 ) ;
771
828
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 ) ) ;
776
831
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
+ ) ;
779
844
780
- factoryContext . ExtraLocals . Add ( argument ) ;
781
- factoryContext . ParamCheckExpressions . Add ( fullParamCheckBlock ) ;
845
+ factoryContext . ParamCheckExpressions . Add ( checkRequiredBodyBlock ) ;
846
+ }
782
847
783
- return argument ;
848
+ // (ParamterType)boundValues[i]
849
+ return Expression . Convert ( boundValueExpr , parameter . ParameterType ) ;
784
850
}
785
851
786
852
private static Expression BindParameterFromBody ( ParameterInfo parameter , bool allowEmpty , FactoryContext factoryContext )
@@ -793,7 +859,6 @@ private static Expression BindParameterFromBody(ParameterInfo parameter, bool al
793
859
{
794
860
factoryContext . TrackedParameters . Remove ( parameterName ) ;
795
861
factoryContext . TrackedParameters . Add ( parameterName , "UNKNOWN" ) ;
796
-
797
862
}
798
863
}
799
864
@@ -925,7 +990,6 @@ static async Task ExecuteAwaited(Task<T> task, HttpContext httpContext)
925
990
926
991
private static Task ExecuteTaskOfString ( Task < string ? > task , HttpContext httpContext )
927
992
{
928
-
929
993
SetPlaintextContentType ( httpContext ) ;
930
994
EnsureRequestTaskNotNull ( task ) ;
931
995
@@ -1032,6 +1096,7 @@ private class FactoryContext
1032
1096
public bool UsingTempSourceString { get ; set ; }
1033
1097
public List < ParameterExpression > ExtraLocals { get ; } = new ( ) ;
1034
1098
public List < Expression > ParamCheckExpressions { get ; } = new ( ) ;
1099
+ public List < Func < HttpContext , ValueTask < object ? > > > ParameterBinders { get ; } = new ( ) ;
1035
1100
1036
1101
public Dictionary < string , string > TrackedParameters { get ; } = new ( ) ;
1037
1102
public bool HasMultipleBodyParameters { get ; set ; }
0 commit comments