@@ -22,6 +22,8 @@ namespace Microsoft.AspNetCore.Http
2222 /// </summary>
2323 public static partial class RequestDelegateFactory
2424 {
25+ private static readonly NullabilityInfoContext nullabilityContext = new NullabilityInfoContext ( ) ;
26+
2527 private static readonly MethodInfo ExecuteTaskOfTMethod = typeof ( RequestDelegateFactory ) . GetMethod ( nameof ( ExecuteTask ) , BindingFlags . NonPublic | BindingFlags . Static ) ! ;
2628 private static readonly MethodInfo ExecuteTaskOfStringMethod = typeof ( RequestDelegateFactory ) . GetMethod ( nameof ( ExecuteTaskOfString ) , BindingFlags . NonPublic | BindingFlags . Static ) ! ;
2729 private static readonly MethodInfo ExecuteValueTaskOfTMethod = typeof ( RequestDelegateFactory ) . GetMethod ( nameof ( ExecuteValueTaskOfT ) , BindingFlags . NonPublic | BindingFlags . Static ) ! ;
@@ -31,12 +33,16 @@ public static partial class RequestDelegateFactory
3133 private static readonly MethodInfo ExecuteValueResultTaskOfTMethod = typeof ( RequestDelegateFactory ) . GetMethod ( nameof ( ExecuteValueTaskResult ) , BindingFlags . NonPublic | BindingFlags . Static ) ! ;
3234 private static readonly MethodInfo ExecuteObjectReturnMethod = typeof ( RequestDelegateFactory ) . GetMethod ( nameof ( ExecuteObjectReturn ) , BindingFlags . NonPublic | BindingFlags . Static ) ! ;
3335 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 ) } ) ! ;
3437 private static readonly MethodInfo ResultWriteResponseAsyncMethod = typeof ( RequestDelegateFactory ) . GetMethod ( nameof ( ExecuteResultWriteResponse ) , BindingFlags . NonPublic | BindingFlags . Static ) ! ;
3538 private static readonly MethodInfo StringResultWriteResponseAsyncMethod = GetMethodInfo < Func < HttpResponse , string , Task > > ( ( response , text ) => HttpResponseWritingExtensions . WriteAsync ( response , text , default ) ) ;
3639 private static readonly MethodInfo JsonResultWriteResponseAsyncMethod = GetMethodInfo < Func < HttpResponse , object , Task > > ( ( response , value ) => HttpResponseJsonExtensions . WriteAsJsonAsync ( response , value , default ) ) ;
3740 private static readonly MethodInfo LogParameterBindingFailureMethod = GetMethodInfo < Action < HttpContext , string , string , string > > ( ( httpContext , parameterType , parameterName , sourceValue ) =>
3841 Log . ParameterBindingFailed ( httpContext , parameterType , parameterName , sourceValue ) ) ;
3942
43+ private static readonly MethodInfo LogRequiredParameterNotProvidedMethod = GetMethodInfo < Action < HttpContext , string , string > > ( ( httpContext , parameterType , parameterName ) =>
44+ Log . RequiredParameterNotProvided ( httpContext , parameterType , parameterName ) ) ;
45+
4046 private static readonly ParameterExpression TargetExpr = Expression . Parameter ( typeof ( object ) , "target" ) ;
4147 private static readonly ParameterExpression HttpContextExpr = Expression . Parameter ( typeof ( HttpContext ) , "httpContext" ) ;
4248 private static readonly ParameterExpression BodyValueExpr = Expression . Parameter ( typeof ( object ) , "bodyValue" ) ;
@@ -217,11 +223,11 @@ private static Expression CreateArgument(ParameterInfo parameter, FactoryContext
217223 }
218224 else if ( parameterCustomAttributes . OfType < IFromBodyMetadata > ( ) . FirstOrDefault ( ) is { } bodyAttribute )
219225 {
220- return BindParameterFromBody ( parameter . ParameterType , bodyAttribute . AllowEmpty , factoryContext ) ;
226+ return BindParameterFromBody ( parameter , bodyAttribute . AllowEmpty , factoryContext ) ;
221227 }
222228 else if ( parameter . CustomAttributes . Any ( a => typeof ( IFromServiceMetadata ) . IsAssignableFrom ( a . AttributeType ) ) )
223229 {
224- return Expression . Call ( GetRequiredServiceMethod . MakeGenericMethod ( parameter . ParameterType ) , RequestServicesExpr ) ;
230+ return BindParameterFromService ( parameter ) ;
225231 }
226232 else if ( parameter . ParameterType == typeof ( HttpContext ) )
227233 {
@@ -256,16 +262,30 @@ private static Expression CreateArgument(ParameterInfo parameter, FactoryContext
256262 }
257263 else
258264 {
265+
266+ var nullability = nullabilityContext . Create ( parameter ) ;
267+ var isOptional = parameter . HasDefaultValue || nullability . ReadState == NullabilityState . Nullable ;
259268 if ( factoryContext . ServiceProviderIsService is IServiceProviderIsService serviceProviderIsService )
260269 {
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 )
263272 {
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 ) ) ;
265285 }
266286 }
267287
268- return BindParameterFromBody ( parameter . ParameterType , allowEmpty : false , factoryContext ) ;
288+ return BindParameterFromBody ( parameter , allowEmpty : false , factoryContext ) ;
269289 }
270290 }
271291
@@ -479,13 +499,9 @@ private static Expression AddResponseWritingToMethodCall(Expression methodCall,
479499
480500 return async ( target , httpContext ) =>
481501 {
482- object ? bodyValue ;
502+ object ? bodyValue = defaultBodyValue ;
483503
484- if ( factoryContext . AllowEmptyRequestBody && httpContext . Request . ContentLength == 0 )
485- {
486- bodyValue = defaultBodyValue ;
487- }
488- else
504+ if ( httpContext . Request . ContentLength != 0 && httpContext . Request . HasJsonContentType ( ) )
489505 {
490506 try
491507 {
@@ -516,21 +532,53 @@ private static Expression GetValueFromProperty(Expression sourceExpression, stri
516532 return Expression . Convert ( indexExpression , typeof ( string ) ) ;
517533 }
518534
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+
519545 private static Expression BindParameterFromValue ( ParameterInfo parameter , Expression valueExpression , FactoryContext factoryContext )
520546 {
547+ var nullability = nullabilityContext . Create ( parameter ) ;
548+ var isOptional = parameter . HasDefaultValue || nullability . ReadState == NullabilityState . Nullable ;
549+
521550 if ( parameter . ParameterType == typeof ( string ) )
522551 {
523- if ( ! parameter . HasDefaultValue )
552+ factoryContext . UsingTempSourceString = true ;
553+
554+ if ( ! isOptional )
524555 {
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 ) ) ;
526575 }
527576
528- factoryContext . UsingTempSourceString = true ;
529577 return Expression . Block (
530578 Expression . Assign ( TempSourceStringExpr , valueExpression ) ,
531579 Expression . Condition ( TempSourceStringNotNullExpr ,
532580 TempSourceStringExpr ,
533- Expression . Constant ( parameter . DefaultValue ) ) ) ;
581+ Expression . Convert ( Expression . Constant ( parameter . DefaultValue ) , parameter . ParameterType ) ) ) ;
534582 }
535583
536584 factoryContext . UsingTempSourceString = true ;
@@ -598,6 +646,17 @@ private static Expression BindParameterFromValue(ParameterInfo parameter, Expres
598646
599647 var tryParseCall = tryParseMethodCall ( parsedValue ) ;
600648
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+
601660 // If the parameter is nullable, we need to assign the "parsedValue" local to the nullable parameter on success.
602661 Expression tryParseExpression = isNotNullable ?
603662 Expression . IfThen ( Expression . Not ( tryParseCall ) , failBlock ) :
@@ -612,11 +671,18 @@ private static Expression BindParameterFromValue(ParameterInfo parameter, Expres
612671 tryParseExpression ,
613672 Expression . Assign ( argument , Expression . Constant ( parameter . DefaultValue ) ) ) ;
614673
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 ) ;
620686
621687 factoryContext . TryParseParams . Add ( ( argument , fullTryParseBlock ) ) ;
622688
@@ -633,17 +699,46 @@ private static Expression BindParameterFromRouteValueOrQueryString(ParameterInfo
633699 return BindParameterFromValue ( parameter , Expression . Coalesce ( routeValue , queryValue ) , factoryContext ) ;
634700 }
635701
636- private static Expression BindParameterFromBody ( Type parameterType , bool allowEmpty , FactoryContext factoryContext )
702+ private static Expression BindParameterFromBody ( ParameterInfo parameter , bool allowEmpty , FactoryContext factoryContext )
637703 {
638704 if ( factoryContext . JsonRequestBodyType is not null )
639705 {
640706 throw new InvalidOperationException ( "Action cannot have more than one FromBody attribute." ) ;
641707 }
642708
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") ;
645716
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 ) ;
647742 }
648743
649744 private static MethodInfo GetMethodInfo < T > ( Expression < T > expr )
@@ -847,11 +942,19 @@ public static void RequestBodyInvalidDataException(HttpContext httpContext, Inva
847942 public static void ParameterBindingFailed ( HttpContext httpContext , string parameterTypeName , string parameterName , string sourceValue )
848943 => ParameterBindingFailed ( GetLogger ( httpContext ) , parameterTypeName , parameterName , sourceValue ) ;
849944
945+ public static void RequiredParameterNotProvided ( HttpContext httpContext , string parameterTypeName , string parameterName )
946+ => RequiredParameterNotProvided ( GetLogger ( httpContext ) , parameterTypeName , parameterName ) ;
947+
850948 [ LoggerMessage ( 3 , LogLevel . Debug ,
851949 @"Failed to bind parameter ""{ParameterType} {ParameterName}"" from ""{SourceValue}""." ,
852950 EventName = "ParamaterBindingFailed" ) ]
853951 private static partial void ParameterBindingFailed ( ILogger logger , string parameterType , string parameterName , string sourceValue ) ;
854952
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+
855958 private static ILogger GetLogger ( HttpContext httpContext )
856959 {
857960 var loggerFactory = httpContext . RequestServices . GetRequiredService < ILoggerFactory > ( ) ;
0 commit comments