@@ -22,6 +22,8 @@ namespace Microsoft.AspNetCore.Http
22
22
/// </summary>
23
23
public static partial class RequestDelegateFactory
24
24
{
25
+ private static readonly NullabilityInfoContext nullabilityContext = new NullabilityInfoContext ( ) ;
26
+
25
27
private static readonly MethodInfo ExecuteTaskOfTMethod = typeof ( RequestDelegateFactory ) . GetMethod ( nameof ( ExecuteTask ) , BindingFlags . NonPublic | BindingFlags . Static ) ! ;
26
28
private static readonly MethodInfo ExecuteTaskOfStringMethod = typeof ( RequestDelegateFactory ) . GetMethod ( nameof ( ExecuteTaskOfString ) , BindingFlags . NonPublic | BindingFlags . Static ) ! ;
27
29
private static readonly MethodInfo ExecuteValueTaskOfTMethod = typeof ( RequestDelegateFactory ) . GetMethod ( nameof ( ExecuteValueTaskOfT ) , BindingFlags . NonPublic | BindingFlags . Static ) ! ;
@@ -31,12 +33,16 @@ public static partial class RequestDelegateFactory
31
33
private static readonly MethodInfo ExecuteValueResultTaskOfTMethod = typeof ( RequestDelegateFactory ) . GetMethod ( nameof ( ExecuteValueTaskResult ) , BindingFlags . NonPublic | BindingFlags . Static ) ! ;
32
34
private static readonly MethodInfo ExecuteObjectReturnMethod = typeof ( RequestDelegateFactory ) . GetMethod ( nameof ( ExecuteObjectReturn ) , BindingFlags . NonPublic | BindingFlags . Static ) ! ;
33
35
private static readonly MethodInfo GetRequiredServiceMethod = typeof ( ServiceProviderServiceExtensions ) . GetMethod ( nameof ( ServiceProviderServiceExtensions . GetRequiredService ) , BindingFlags . Public | BindingFlags . Static , new Type [ ] { typeof ( IServiceProvider ) } ) ! ;
36
+ private static readonly MethodInfo GetServiceMethod = typeof ( ServiceProviderServiceExtensions ) . GetMethod ( nameof ( ServiceProviderServiceExtensions . GetService ) , BindingFlags . Public | BindingFlags . Static , new Type [ ] { typeof ( IServiceProvider ) } ) ! ;
34
37
private static readonly MethodInfo ResultWriteResponseAsyncMethod = typeof ( RequestDelegateFactory ) . GetMethod ( nameof ( ExecuteResultWriteResponse ) , BindingFlags . NonPublic | BindingFlags . Static ) ! ;
35
38
private static readonly MethodInfo StringResultWriteResponseAsyncMethod = GetMethodInfo < Func < HttpResponse , string , Task > > ( ( response , text ) => HttpResponseWritingExtensions . WriteAsync ( response , text , default ) ) ;
36
39
private static readonly MethodInfo JsonResultWriteResponseAsyncMethod = GetMethodInfo < Func < HttpResponse , object , Task > > ( ( response , value ) => HttpResponseJsonExtensions . WriteAsJsonAsync ( response , value , default ) ) ;
37
40
private static readonly MethodInfo LogParameterBindingFailureMethod = GetMethodInfo < Action < HttpContext , string , string , string > > ( ( httpContext , parameterType , parameterName , sourceValue ) =>
38
41
Log . ParameterBindingFailed ( httpContext , parameterType , parameterName , sourceValue ) ) ;
39
42
43
+ private static readonly MethodInfo LogRequiredParameterNotProvidedMethod = GetMethodInfo < Action < HttpContext , string , string > > ( ( httpContext , parameterType , parameterName ) =>
44
+ Log . RequiredParameterNotProvided ( httpContext , parameterType , parameterName ) ) ;
45
+
40
46
private static readonly ParameterExpression TargetExpr = Expression . Parameter ( typeof ( object ) , "target" ) ;
41
47
private static readonly ParameterExpression HttpContextExpr = Expression . Parameter ( typeof ( HttpContext ) , "httpContext" ) ;
42
48
private static readonly ParameterExpression BodyValueExpr = Expression . Parameter ( typeof ( object ) , "bodyValue" ) ;
@@ -217,11 +223,11 @@ private static Expression CreateArgument(ParameterInfo parameter, FactoryContext
217
223
}
218
224
else if ( parameterCustomAttributes . OfType < IFromBodyMetadata > ( ) . FirstOrDefault ( ) is { } bodyAttribute )
219
225
{
220
- return BindParameterFromBody ( parameter . ParameterType , bodyAttribute . AllowEmpty , factoryContext ) ;
226
+ return BindParameterFromBody ( parameter , bodyAttribute . AllowEmpty , factoryContext ) ;
221
227
}
222
228
else if ( parameter . CustomAttributes . Any ( a => typeof ( IFromServiceMetadata ) . IsAssignableFrom ( a . AttributeType ) ) )
223
229
{
224
- return Expression . Call ( GetRequiredServiceMethod . MakeGenericMethod ( parameter . ParameterType ) , RequestServicesExpr ) ;
230
+ return BindParameterFromService ( parameter ) ;
225
231
}
226
232
else if ( parameter . ParameterType == typeof ( HttpContext ) )
227
233
{
@@ -256,16 +262,30 @@ private static Expression CreateArgument(ParameterInfo parameter, FactoryContext
256
262
}
257
263
else
258
264
{
265
+
266
+ var nullability = nullabilityContext . Create ( parameter ) ;
267
+ var isOptional = parameter . HasDefaultValue || nullability . ReadState == NullabilityState . Nullable ;
259
268
if ( factoryContext . ServiceProviderIsService is IServiceProviderIsService serviceProviderIsService )
260
269
{
261
- // If the parameter resolves as a service then get it from services
262
- if ( serviceProviderIsService . IsService ( parameter . ParameterType ) )
270
+ // If the parameter is required
271
+ if ( ! isOptional )
263
272
{
264
- return Expression . Call ( GetRequiredServiceMethod . MakeGenericMethod ( parameter . ParameterType ) , RequestServicesExpr ) ;
273
+ // And we are able to resolve a service for it
274
+ return serviceProviderIsService . IsService ( parameter . ParameterType )
275
+ ? Expression . Call ( GetRequiredServiceMethod . MakeGenericMethod ( parameter . ParameterType ) , RequestServicesExpr ) // Then get it from the DI
276
+ : BindParameterFromBody ( parameter , allowEmpty : false , factoryContext ) ; // Otherwise try to find it in the body
277
+ }
278
+ // If the parameter is optional
279
+ else
280
+ {
281
+ // Then try to resolve it as an optional service and fallback to a body otherwise
282
+ return Expression . Coalesce (
283
+ Expression . Call ( GetServiceMethod . MakeGenericMethod ( parameter . ParameterType ) , RequestServicesExpr ) ,
284
+ BindParameterFromBody ( parameter , allowEmpty : false , factoryContext ) ) ;
265
285
}
266
286
}
267
287
268
- return BindParameterFromBody ( parameter . ParameterType , allowEmpty : false , factoryContext ) ;
288
+ return BindParameterFromBody ( parameter , allowEmpty : false , factoryContext ) ;
269
289
}
270
290
}
271
291
@@ -479,13 +499,9 @@ private static Expression AddResponseWritingToMethodCall(Expression methodCall,
479
499
480
500
return async ( target , httpContext ) =>
481
501
{
482
- object ? bodyValue ;
502
+ object ? bodyValue = defaultBodyValue ;
483
503
484
- if ( factoryContext . AllowEmptyRequestBody && httpContext . Request . ContentLength == 0 )
485
- {
486
- bodyValue = defaultBodyValue ;
487
- }
488
- else
504
+ if ( httpContext . Request . ContentLength != 0 && httpContext . Request . HasJsonContentType ( ) )
489
505
{
490
506
try
491
507
{
@@ -516,21 +532,53 @@ private static Expression GetValueFromProperty(Expression sourceExpression, stri
516
532
return Expression . Convert ( indexExpression , typeof ( string ) ) ;
517
533
}
518
534
535
+ private static Expression BindParameterFromService ( ParameterInfo parameter )
536
+ {
537
+ var nullability = nullabilityContext . Create ( parameter ) ;
538
+ var isOptional = parameter . HasDefaultValue || nullability . ReadState == NullabilityState . Nullable ;
539
+
540
+ return isOptional
541
+ ? Expression . Call ( GetServiceMethod . MakeGenericMethod ( parameter . ParameterType ) , RequestServicesExpr )
542
+ : Expression . Call ( GetRequiredServiceMethod . MakeGenericMethod ( parameter . ParameterType ) , RequestServicesExpr ) ;
543
+ }
544
+
519
545
private static Expression BindParameterFromValue ( ParameterInfo parameter , Expression valueExpression , FactoryContext factoryContext )
520
546
{
547
+ var nullability = nullabilityContext . Create ( parameter ) ;
548
+ var isOptional = parameter . HasDefaultValue || nullability . ReadState == NullabilityState . Nullable ;
549
+
521
550
if ( parameter . ParameterType == typeof ( string ) )
522
551
{
523
- if ( ! parameter . HasDefaultValue )
552
+ factoryContext . UsingTempSourceString = true ;
553
+
554
+ if ( ! isOptional )
524
555
{
525
- return valueExpression ;
556
+ var checkRequiredStringParameterBlock = Expression . Block (
557
+ Expression . Assign ( TempSourceStringExpr , valueExpression ) ,
558
+ Expression . IfThen ( Expression . Not ( TempSourceStringNotNullExpr ) ,
559
+ Expression . Block (
560
+ Expression . Assign ( WasTryParseFailureExpr , Expression . Constant ( true ) ) ,
561
+ Expression . Call ( LogRequiredParameterNotProvidedMethod ,
562
+ HttpContextExpr , Expression . Constant ( parameter . ParameterType . Name ) , Expression . Constant ( parameter . Name ) )
563
+ )
564
+ )
565
+ ) ;
566
+
567
+ factoryContext . TryParseParams . Add ( ( TempSourceStringExpr , checkRequiredStringParameterBlock ) ) ;
568
+ return Expression . Block ( TempSourceStringExpr ) ;
569
+ }
570
+
571
+ // Allow nullable parameters that don't have a default value
572
+ if ( nullability . ReadState == NullabilityState . Nullable && ! parameter . HasDefaultValue )
573
+ {
574
+ return Expression . Block ( Expression . Assign ( TempSourceStringExpr , valueExpression ) ) ;
526
575
}
527
576
528
- factoryContext . UsingTempSourceString = true ;
529
577
return Expression . Block (
530
578
Expression . Assign ( TempSourceStringExpr , valueExpression ) ,
531
579
Expression . Condition ( TempSourceStringNotNullExpr ,
532
580
TempSourceStringExpr ,
533
- Expression . Constant ( parameter . DefaultValue ) ) ) ;
581
+ Expression . Convert ( Expression . Constant ( parameter . DefaultValue ) , parameter . ParameterType ) ) ) ;
534
582
}
535
583
536
584
factoryContext . UsingTempSourceString = true ;
@@ -598,6 +646,17 @@ private static Expression BindParameterFromValue(ParameterInfo parameter, Expres
598
646
599
647
var tryParseCall = tryParseMethodCall ( parsedValue ) ;
600
648
649
+ // If the parameter is required, fail to parse and log an error
650
+ var checkRequiredParaseableParameterBlock = Expression . Block (
651
+ Expression . IfThen ( Expression . Not ( TempSourceStringNotNullExpr ) ,
652
+ Expression . Block (
653
+ Expression . Assign ( WasTryParseFailureExpr , Expression . Constant ( true ) ) ,
654
+ Expression . Call ( LogRequiredParameterNotProvidedMethod ,
655
+ HttpContextExpr , parameterTypeNameConstant , parameterNameConstant )
656
+ )
657
+ )
658
+ ) ;
659
+
601
660
// If the parameter is nullable, we need to assign the "parsedValue" local to the nullable parameter on success.
602
661
Expression tryParseExpression = isNotNullable ?
603
662
Expression . IfThen ( Expression . Not ( tryParseCall ) , failBlock ) :
@@ -612,11 +671,18 @@ private static Expression BindParameterFromValue(ParameterInfo parameter, Expres
612
671
tryParseExpression ,
613
672
Expression . Assign ( argument , Expression . Constant ( parameter . DefaultValue ) ) ) ;
614
673
615
- var fullTryParseBlock = Expression . Block (
616
- // tempSourceString = httpContext.RequestValue["id"];
617
- Expression . Assign ( TempSourceStringExpr , valueExpression ) ,
618
- // if (tempSourceString != null) { ... }
619
- ifNotNullTryParse ) ;
674
+ var fullTryParseBlock = ! isOptional
675
+ ? Expression . Block (
676
+ // tempSourceString = httpContext.RequestValue["id"];
677
+ Expression . Assign ( TempSourceStringExpr , valueExpression ) ,
678
+ checkRequiredParaseableParameterBlock ,
679
+ // if (tempSourceString != null) { ... }
680
+ ifNotNullTryParse )
681
+ : Expression . Block (
682
+ // tempSourceString = httpContext.RequestValue["id"];
683
+ Expression . Assign ( TempSourceStringExpr , valueExpression ) ,
684
+ // if (tempSourceString != null) { ... }
685
+ ifNotNullTryParse ) ;
620
686
621
687
factoryContext . TryParseParams . Add ( ( argument , fullTryParseBlock ) ) ;
622
688
@@ -633,17 +699,46 @@ private static Expression BindParameterFromRouteValueOrQueryString(ParameterInfo
633
699
return BindParameterFromValue ( parameter , Expression . Coalesce ( routeValue , queryValue ) , factoryContext ) ;
634
700
}
635
701
636
- private static Expression BindParameterFromBody ( Type parameterType , bool allowEmpty , FactoryContext factoryContext )
702
+ private static Expression BindParameterFromBody ( ParameterInfo parameter , bool allowEmpty , FactoryContext factoryContext )
637
703
{
638
704
if ( factoryContext . JsonRequestBodyType is not null )
639
705
{
640
706
throw new InvalidOperationException ( "Action cannot have more than one FromBody attribute." ) ;
641
707
}
642
708
643
- factoryContext . JsonRequestBodyType = parameterType ;
644
- factoryContext . AllowEmptyRequestBody = allowEmpty ;
709
+ var nullability = nullabilityContext . Create ( parameter ) ;
710
+ var isOptional = parameter . HasDefaultValue || nullability . ReadState == NullabilityState . Nullable ;
711
+
712
+ factoryContext . JsonRequestBodyType = parameter . ParameterType ;
713
+ factoryContext . AllowEmptyRequestBody = allowEmpty || isOptional ;
714
+
715
+ var argument = Expression . Variable ( parameter . ParameterType , $ "{ parameter . Name } _local") ;
645
716
646
- return Expression . Convert ( BodyValueExpr , parameterType ) ;
717
+ if ( ! isOptional && ! allowEmpty )
718
+ {
719
+ var checkRequiredBodyBlock = Expression . Block (
720
+ Expression . Assign ( argument , Expression . Convert ( BodyValueExpr , parameter . ParameterType ) ) ,
721
+ Expression . IfThen ( Expression . Equal ( argument , Expression . Constant ( null ) ) ,
722
+ Expression . Block (
723
+ Expression . Assign ( WasTryParseFailureExpr , Expression . Constant ( true ) ) ,
724
+ Expression . Call ( LogRequiredParameterNotProvidedMethod ,
725
+ HttpContextExpr , Expression . Constant ( parameter . ParameterType . Name ) , Expression . Constant ( parameter . Name ) )
726
+ )
727
+ )
728
+ ) ;
729
+ factoryContext . TryParseParams . Add ( ( argument , checkRequiredBodyBlock ) ) ;
730
+ return argument ;
731
+ }
732
+
733
+ if ( parameter . HasDefaultValue )
734
+ {
735
+ // Convert(bodyValue ?? SomeDefault, Todo)
736
+ return Expression . Convert (
737
+ Expression . Coalesce ( BodyValueExpr , Expression . Constant ( parameter . DefaultValue ) ) ,
738
+ parameter . ParameterType ) ;
739
+ }
740
+
741
+ return Expression . Convert ( BodyValueExpr , parameter . ParameterType ) ;
647
742
}
648
743
649
744
private static MethodInfo GetMethodInfo < T > ( Expression < T > expr )
@@ -847,11 +942,19 @@ public static void RequestBodyInvalidDataException(HttpContext httpContext, Inva
847
942
public static void ParameterBindingFailed ( HttpContext httpContext , string parameterTypeName , string parameterName , string sourceValue )
848
943
=> ParameterBindingFailed ( GetLogger ( httpContext ) , parameterTypeName , parameterName , sourceValue ) ;
849
944
945
+ public static void RequiredParameterNotProvided ( HttpContext httpContext , string parameterTypeName , string parameterName )
946
+ => RequiredParameterNotProvided ( GetLogger ( httpContext ) , parameterTypeName , parameterName ) ;
947
+
850
948
[ LoggerMessage ( 3 , LogLevel . Debug ,
851
949
@"Failed to bind parameter ""{ParameterType} {ParameterName}"" from ""{SourceValue}""." ,
852
950
EventName = "ParamaterBindingFailed" ) ]
853
951
private static partial void ParameterBindingFailed ( ILogger logger , string parameterType , string parameterName , string sourceValue ) ;
854
952
953
+ [ LoggerMessage ( 4 , LogLevel . Debug ,
954
+ @"Required parameter ""{ParameterType} {ParameterName}"" was not provided." ,
955
+ EventName = "RequiredParameterNotProvided" ) ]
956
+ private static partial void RequiredParameterNotProvided ( ILogger logger , string parameterType , string parameterName ) ;
957
+
855
958
private static ILogger GetLogger ( HttpContext httpContext )
856
959
{
857
960
var loggerFactory = httpContext . RequestServices . GetRequiredService < ILoggerFactory > ( ) ;
0 commit comments