@@ -508,17 +508,72 @@ 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 ;
511513 for ( int i = 0 ; i < parts . Length ; i ++ )
512514 {
513515 string propertyName = parts [ i ] ;
514516 if ( propertyName == SelfPropertyName )
515517 continue ;
516518
517- MemberExpression sourcePropertyExpression = Expression . Property ( value , propertyName ) ;
518- value = parameter . Items != null ? CollectItemsParameterValue ( action , requestParameter , argumentsParameter , dependencyResolverParameter , actionParameter , compilationContext , parameter , sourcePropertyExpression , sourceMap , ensureNullPropagation ) : sourcePropertyExpression ;
519+ bool isItemParameter = parameter . Items != null ;
520+ bool isNestedProperty = i > 0 ;
521+ bool hasNestedProperties = parts . Length > 1 ;
522+ bool reachedEnd = i + 1 == parts . Length ;
523+ Expression nullCheckTarget = null ;
519524
520- if ( ensureNullPropagation && i + 1 < parts . Length )
521- nullCheckTargets . Add ( sourcePropertyExpression ) ;
525+ if ( isItemParameter )
526+ {
527+ Expression sourcePropertyExpression = null ;
528+ Type resultEnumerableType = null ;
529+ if ( isNestedProperty )
530+ {
531+ Type propertyEnumerableType = TryGetEnumerableType ( value . Type ) ;
532+ if ( propertyEnumerableType != null )
533+ {
534+ if ( nestedEnumerableSelectorParameter == null )
535+ {
536+ nestedEnumerableSelectorParameter = Expression . Parameter ( propertyEnumerableType , "x" ) ;
537+ nestedEnumerablePropertySelector = nestedEnumerableSelectorParameter ;
538+ }
539+
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 ) ;
547+ }
548+ }
549+
550+ if ( sourcePropertyExpression == null )
551+ {
552+ sourcePropertyExpression = Expression . Property ( value , propertyName ) ;
553+
554+ if ( hasNestedProperties && ! reachedEnd )
555+ {
556+ value = sourcePropertyExpression ;
557+ continue ;
558+ }
559+
560+ nullCheckTarget = sourcePropertyExpression ;
561+ }
562+
563+ resultEnumerableType ??= GetEnumerableType ( sourcePropertyExpression . Type ) ;
564+ value = CollectItemsParameterValue ( action , requestParameter , argumentsParameter , dependencyResolverParameter , actionParameter , compilationContext , parameter , sourcePropertyExpression , resultEnumerableType , sourceMap , ensureNullPropagation ) ;
565+ }
566+ else
567+ {
568+ bool isNestedEnumerablePair = value . Type . IsGenericType && value . Type . GetGenericTypeDefinition ( ) == typeof ( NestedEnumerablePair < , > ) ;
569+ value = Expression . Property ( value , propertyName ) ;
570+
571+ if ( ! isNestedEnumerablePair )
572+ nullCheckTarget = value ;
573+ }
574+
575+ if ( ensureNullPropagation && nullCheckTarget != null && i + 1 < parts . Length )
576+ nullCheckTargets . Add ( nullCheckTarget ) ;
522577 }
523578
524579 if ( nullCheckTargets . Any ( ) )
@@ -543,11 +598,13 @@ IHttpActionDescriptor action
543598 , CompilationContext compilationContext
544599 , HttpParameterInfo parameter
545600 , Expression sourcePropertyExpression
601+ , Type itemType
546602 , IDictionary < string , Expression > sourceMap
547603 , bool ensureNullPropagation
548604 )
549605 {
550- Type itemType = GetItemType ( sourcePropertyExpression . Type ) ;
606+ Guard . IsNotNull ( itemType , nameof ( itemType ) ) ;
607+
551608 IDictionary < string , Expression > addMethodParameterValues = parameter . Items . AddItemMethod . GetParameters ( ) . ToDictionary ( x => x . Name , x => ( Expression ) null ) ;
552609 IDictionary < string , Type > addMethodParameterTypes = parameter . Items . AddItemMethod . GetParameters ( ) . ToDictionary ( x => x . Name , x => x . ParameterType ) ;
553610
@@ -708,6 +765,29 @@ private static Expression BuildSingleValueStructuredTypeConversion(string parame
708765 return block ;
709766 }
710767
768+ private static Expression BuildNestedEnumerableSelector ( Expression source , ParameterExpression collectionSelectorParameter , Expression collectionSelectorProperty , Type collectionType , out Type resultType )
769+ {
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 ;
775+ }
776+
777+ private static IEnumerable < NestedEnumerablePair < TParent , TChild > > FlattenNestedEnumerable < TParent , TChild > ( IEnumerable < TParent > source , Func < TParent , IEnumerable < TChild > > collectionSelector )
778+ {
779+ int parentIndex = 1 ;
780+ foreach ( TParent parent in source )
781+ {
782+ int childIndex = 1 ;
783+ foreach ( TChild child in collectionSelector ( parent ) )
784+ {
785+ yield return new NestedEnumerablePair < TParent , TChild > ( parentIndex , childIndex ++ , parent , child ) ;
786+ }
787+ parentIndex ++ ;
788+ }
789+ }
790+
711791 private static MethodInfo GetStructuredTypeAddMethod ( Type type )
712792 {
713793 MethodInfo addMethod = type . SafeGetMethod ( "Add" , BindingFlags . Public | BindingFlags . Instance ) ;
@@ -880,12 +960,25 @@ private static MethodInfo GetStructuredTypeFactoryMethod(Type implementationType
880960 throw new InvalidOperationException ( "Could not find structured type factory method 'StructuredType<>.From()'" ) ;
881961 }
882962
883- private static Type GetItemType ( Type enumerableType )
963+ private static Type TryGetEnumerableType ( Type type )
964+ {
965+ if ( ! type . IsGenericType )
966+ return null ;
967+
968+ if ( type . GetInterfaces ( ) . All ( x => x . GetGenericTypeDefinition ( ) != typeof ( IEnumerable < > ) ) )
969+ return null ;
970+
971+ return type . GenericTypeArguments [ 0 ] ;
972+
973+ }
974+
975+ private static Type GetEnumerableType ( Type type )
884976 {
885- if ( enumerableType . GetInterfaces ( ) . All ( x => x . GetGenericTypeDefinition ( ) != typeof ( IEnumerable < > ) ) )
886- throw new InvalidOperationException ( $ "Type does not implement IEnumerable<>: { enumerableType } ") ;
977+ Type itemType = TryGetEnumerableType ( type ) ;
978+ if ( itemType == null )
979+ throw new InvalidOperationException ( $ "Type does not implement IEnumerable<>: { type } ") ;
887980
888- return enumerableType . GenericTypeArguments [ 0 ] ;
981+ return itemType ;
889982 }
890983 #endregion
891984
0 commit comments