@@ -508,8 +508,8 @@ private static Expression CollectSourcePropertyValue(IHttpActionDescriptor actio
508508 {
509509 string [ ] parts = propertyPath . Split ( '.' ) ;
510510 ICollection < Expression > nullCheckTargets = new Collection < Expression > ( ) ;
511- ParameterExpression nestedEnumerableSelectorParameter = null ;
512- Expression nestedEnumerablePropertySelector = null ;
511+ Expression nestedEnumerableAnchor = null ;
512+
513513 for ( int i = 0 ; i < parts . Length ; i ++ )
514514 {
515515 string propertyName = parts [ i ] ;
@@ -521,6 +521,7 @@ private static Expression CollectSourcePropertyValue(IHttpActionDescriptor actio
521521 bool hasNestedProperties = parts . Length > 1 ;
522522 bool reachedEnd = i + 1 == parts . Length ;
523523 Expression nullCheckTarget = null ;
524+ string previousPropertyName = i > 0 ? parts [ i - 1 ] : null ;
524525
525526 if ( isItemParameter )
526527 {
@@ -531,19 +532,10 @@ private static Expression CollectSourcePropertyValue(IHttpActionDescriptor actio
531532 Type propertyEnumerableType = TryGetEnumerableType ( value . Type ) ;
532533 if ( propertyEnumerableType != null )
533534 {
534- if ( nestedEnumerableSelectorParameter == null )
535- {
536- nestedEnumerableSelectorParameter = Expression . Parameter ( propertyEnumerableType , "x" ) ;
537- nestedEnumerablePropertySelector = nestedEnumerableSelectorParameter ;
538- }
535+ sourcePropertyExpression = BuildFlattenNestedEnumerableExpression ( value , propertyEnumerableType , propertyPath , parts , i , out resultEnumerableType ) ;
539536
540- nestedEnumerablePropertySelector = Expression . Property ( nestedEnumerablePropertySelector , propertyName ) ;
541- Type nestedEnumerableType = TryGetEnumerableType ( nestedEnumerablePropertySelector . Type ) ;
542- if ( nestedEnumerableType == null )
543- continue ;
544-
545- // We are within a two nested collection properties so we need to flatten it using SelectMany
546- sourcePropertyExpression = BuildNestedEnumerableSelector ( value , nestedEnumerableSelectorParameter , nestedEnumerablePropertySelector , nestedEnumerableType , out resultEnumerableType ) ;
537+ // Skip to the end since we've processed the entire enumerable path
538+ i = parts . Length - 1 ;
547539 }
548540 }
549541
@@ -565,11 +557,11 @@ private static Expression CollectSourcePropertyValue(IHttpActionDescriptor actio
565557 }
566558 else
567559 {
568- bool isNestedEnumerablePair = value . Type . IsGenericType && value . Type . GetGenericTypeDefinition ( ) == typeof ( NestedEnumerablePair < , > ) ;
569- value = Expression . Property ( value , propertyName ) ;
560+ bool isNestedEnumerablePair = IsNestedEnumerablePair ( value ) ;
561+ if ( isNestedEnumerablePair )
562+ nestedEnumerableAnchor = value ;
570563
571- if ( ! isNestedEnumerablePair )
572- nullCheckTarget = value ;
564+ CollectSourcePropertyValue ( propertyName , previousPropertyName , nestedEnumerableAnchor , ref value , ref nullCheckTarget ) ;
573565 }
574566
575567 if ( ensureNullPropagation && nullCheckTarget != null && i + 1 < parts . Length )
@@ -578,7 +570,7 @@ private static Expression CollectSourcePropertyValue(IHttpActionDescriptor actio
578570
579571 if ( nullCheckTargets . Any ( ) )
580572 {
581- Expression test = nullCheckTargets . Select ( x => Expression . NotEqual ( x , Expression . Constant ( null ) ) ) . Aggregate ( Expression . AndAlso /* Short-circuit behavior like && in C# */ ) ;
573+ Expression test = nullCheckTargets . Select ( x => Expression . NotEqual ( x , Expression . Constant ( null ) ) ) . Aggregate ( Expression . AndAlso /* Short-circuit behavior like && in C# */ ) ;
582574 bool hasDefaultValue = parameter . DefaultValue != DBNull . Value && parameter . DefaultValue != null ;
583575 Expression fallbackValue = hasDefaultValue ? ( Expression ) Expression . Constant ( parameter . DefaultValue ) : Expression . Default ( parameter . ParameterType ) ;
584576 value = EnsureCorrectType ( parameter . InternalParameterName , value , parameter . ParameterType , actionParameter ) ;
@@ -587,6 +579,45 @@ private static Expression CollectSourcePropertyValue(IHttpActionDescriptor actio
587579
588580 return value ;
589581 }
582+ private static void CollectSourcePropertyValue ( string propertyName , string previousPropertyName , Expression nestedEnumerableAnchor , ref Expression value , ref Expression nullCheckTarget )
583+ {
584+ switch ( propertyName )
585+ {
586+ case ItemParameterSource . ParentPropertyName :
587+ value = Expression . Property ( value , nameof ( NestedEnumerablePair < object , object > . Parent ) ) ;
588+ break ;
589+
590+ case ItemParameterSource . ChildPropertyName :
591+ value = Expression . Property ( value , nameof ( NestedEnumerablePair < object , object > . Child ) ) ;
592+ break ;
593+
594+ case ItemParameterSource . IndexPropertyName :
595+ {
596+ if ( nestedEnumerableAnchor == null )
597+ throw new InvalidOperationException ( $ "Property '{ ItemParameterSource . IndexPropertyName } ' cannot be accessed on type '{ value . Type } '") ;
598+
599+ string indexPropertyName = previousPropertyName switch
600+ {
601+ ItemParameterSource . ParentPropertyName => nameof ( NestedEnumerablePair < object , object > . ParentIndex ) ,
602+ ItemParameterSource . ChildPropertyName => nameof ( NestedEnumerablePair < object , object > . ChildIndex ) ,
603+ _ => throw new InvalidOperationException ( $ "Property '{ ItemParameterSource . IndexPropertyName } ' cannot be accessed on type '{ value . Type } '")
604+ } ;
605+ value = Expression . Property ( nestedEnumerableAnchor , indexPropertyName ) ;
606+ break ;
607+ }
608+
609+ default :
610+ {
611+ if ( IsNestedEnumerablePair ( value ) )
612+ {
613+ value = Expression . Property ( value , nameof ( NestedEnumerablePair < object , object > . Child ) ) ;
614+ }
615+ value = Expression . Property ( value , propertyName ) ;
616+ nullCheckTarget = value ;
617+ break ;
618+ }
619+ }
620+ }
590621
591622 private static Expression CollectItemsParameterValue
592623 (
@@ -765,13 +796,43 @@ private static Expression BuildSingleValueStructuredTypeConversion(string parame
765796 return block ;
766797 }
767798
768- private static Expression BuildNestedEnumerableSelector ( Expression source , ParameterExpression collectionSelectorParameter , Expression collectionSelectorProperty , Type collectionType , out Type resultType )
799+ private static Expression BuildFlattenNestedEnumerableExpression ( Expression value , Type propertyEnumerableType , string propertyPath , string [ ] tokens , int startIndex , out Type resultEnumerableType )
769800 {
770- Type sourceType = collectionSelectorParameter . Type ;
771- resultType = typeof ( NestedEnumerablePair < , > ) . MakeGenericType ( sourceType , collectionType ) ;
772- Expression collectionSelector = Expression . Lambda ( collectionSelectorProperty , collectionSelectorParameter ) ;
773- Expression call = Expression . Call ( typeof ( HttpParameterResolver ) , nameof ( FlattenNestedEnumerable ) , [ sourceType , collectionType ] , source , collectionSelector ) ;
774- return call ;
801+ Expression result = value ;
802+ Type parentGenericType = propertyEnumerableType ;
803+ ParameterExpression selectorParameter = Expression . Parameter ( propertyEnumerableType , "x" ) ;
804+ Expression selectorProperty = selectorParameter ;
805+
806+ for ( int i = startIndex ; i < tokens . Length ; i ++ )
807+ {
808+ string token = tokens [ i ] ;
809+ bool reachedEnd = i + 1 == tokens . Length ;
810+
811+ selectorProperty = Expression . Property ( selectorProperty , token ) ;
812+
813+ Type enumerableType = TryGetEnumerableType ( selectorProperty . Type ) ;
814+
815+ if ( reachedEnd && enumerableType == null )
816+ {
817+ throw new InvalidOperationException ( $ "Expected property path to end in an enumerable type: { propertyPath } ") ;
818+ }
819+
820+ if ( enumerableType == null )
821+ continue ;
822+
823+ Type childGenericType = enumerableType ;
824+ MethodInfo flattenMethod = typeof ( HttpParameterResolver ) . SafeGetMethod ( nameof ( FlattenNestedEnumerable ) , BindingFlags . NonPublic | BindingFlags . Static )
825+ . MakeGenericMethod ( parentGenericType , childGenericType ) ;
826+
827+ Expression collectionSelector = Expression . Lambda ( selectorProperty , selectorParameter ) ;
828+ result = Expression . Call ( flattenMethod , result , collectionSelector ) ;
829+ parentGenericType = typeof ( NestedEnumerablePair < , > ) . MakeGenericType ( parentGenericType , childGenericType ) ;
830+ selectorParameter = Expression . Parameter ( parentGenericType , "x" ) ;
831+ selectorProperty = Expression . Property ( selectorParameter , nameof ( NestedEnumerablePair < object , object > . Child ) ) ;
832+ }
833+
834+ resultEnumerableType = parentGenericType ;
835+ return result ;
775836 }
776837
777838 private static MethodInfo GetStructuredTypeAddMethod ( Type type )
@@ -869,6 +930,11 @@ private static MethodInfo GetStructuredTypeFactoryMethod(Type implementationType
869930 throw new InvalidOperationException ( "Could not find structured type factory method 'StructuredType<>.From()'" ) ;
870931 }
871932
933+ private static bool IsNestedEnumerablePair ( Expression value )
934+ {
935+ return value . Type . IsGenericType && value . Type . GetGenericTypeDefinition ( ) == typeof ( NestedEnumerablePair < , > ) ;
936+ }
937+
872938 private static Type TryGetEnumerableType ( Type type )
873939 {
874940 if ( ! type . IsGenericType )
@@ -878,7 +944,6 @@ private static Type TryGetEnumerableType(Type type)
878944 return null ;
879945
880946 return type . GenericTypeArguments [ 0 ] ;
881-
882947 }
883948
884949 private static Type GetEnumerableType ( Type type )
0 commit comments