Skip to content

Commit e68a54f

Browse files
committed
Add support for more levels when using nested collection parameter mapping
1 parent 3481f24 commit e68a54f

16 files changed

Lines changed: 339 additions & 86 deletions

File tree

shared/ParameterSources/ItemParameterSource.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,7 @@
44
internal sealed class ItemParameterSource : ActionParameterSourceDefinition<ItemParameterSource>
55
{
66
public const string IndexPropertyName = "$INDEX";
7+
public const string ParentPropertyName = "$PARENT";
8+
public const string ChildPropertyName = "$CHILD";
79
}
810
}

src/Dibix.Http.Server/Runtime/HttpParameterResolver.cs

Lines changed: 91 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -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)

src/Dibix.Sdk.CodeGeneration/Registration/PropertyPathParameterSourceReader.cs

Lines changed: 22 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -102,43 +102,41 @@ private void CollectItemPropertySourceNodes(ActionParameterPropertySourceBuilder
102102
return;
103103
}
104104

105-
ActionParameterPropertySourceNode lastNode = rootPropertySourceBuilder.Nodes.LastOrDefault();
106-
if (lastNode == null)
105+
if (!rootPropertySourceBuilder.Nodes.Any())
107106
throw new InvalidOperationException($"Missing resolved source property node for item property mapping ({rootPropertySourceBuilder.PropertyName})");
108107

109-
ActionParameterPropertySourceNode nestedEnumerableParent = rootPropertySourceBuilder.Nodes
110-
.Reverse()
111-
.Skip(1)
112-
.FirstOrDefault(x => x.Property.Type.IsEnumerable);
113-
114-
bool isObjectSchema = TryCollectNodeSchema(lastNode, out SchemaTypeReference propertySchemaTypeReference, out ObjectSchema objectSchema, logger, rootPropertySourceBuilder, schemaRegistry);
115-
108+
Stack<ActionParameterPropertySourceNode> nodes = new Stack<ActionParameterPropertySourceNode>(rootPropertySourceBuilder.Nodes);
116109
IList<string> segments = new Collection<string>();
117110

111+
ActionParameterPropertySourceNode currentNode = nodes.Pop();
112+
118113
foreach (string propertyName in propertySource.PropertyName.Split('.'))
119114
{
120-
if (propertyName is ItemParameterSource.IndexPropertyName or nameof(NestedEnumerablePair<object, object>.ParentIndex) or nameof(NestedEnumerablePair<object, object>.ChildIndex))
121-
break;
122-
123-
if (nestedEnumerableParent != null)
115+
switch (propertyName)
124116
{
125-
if (propertyName == nameof(NestedEnumerablePair<object, object>.Parent))
126-
{
127-
if (!(isObjectSchema = TryCollectNodeSchema(nestedEnumerableParent, out propertySchemaTypeReference, out objectSchema, logger, rootPropertySourceBuilder, schemaRegistry)))
128-
return;
129-
117+
case ItemParameterSource.ParentPropertyName:
118+
// $PARENT means the next item property up in the hierarchy
119+
// Non-item properties can be accessed regularly when on the item parent.
120+
// For example: $PARENT.NonEnumerableChild.SomeProperty
121+
do currentNode = nodes.Pop();
122+
while (!currentNode.Property.Type.IsEnumerable);
130123
continue;
131-
}
132124

133-
if (propertyName == nameof(NestedEnumerablePair<object, object>.Child))
134-
{
125+
case ItemParameterSource.ChildPropertyName:
135126
continue;
136-
}
137-
}
138127

139-
segments.Add(propertyName);
128+
case ItemParameterSource.IndexPropertyName:
129+
case nameof(NestedEnumerablePair<object, object>.ParentIndex):
130+
case nameof(NestedEnumerablePair<object, object>.ChildIndex):
131+
break;
132+
133+
default:
134+
segments.Add(propertyName);
135+
break;
136+
}
140137
}
141138

139+
bool isObjectSchema = TryCollectNodeSchema(currentNode, out SchemaTypeReference propertySchemaTypeReference, out ObjectSchema objectSchema, logger, rootPropertySourceBuilder, schemaRegistry);
142140
if (!isObjectSchema)
143141
return;
144142

tests/Dibix.Http.Server.Tests/HttpParameterResolverTest.Base.cs

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,21 @@ private sealed class ExplicitHttpBodyItemChildContainer
111111
private sealed class ExplicitHttpBodyItemChild
112112
{
113113
public int Id { get; }
114+
public int AnotherId { get; }
115+
public ICollection<ExplicitHttpBodyItemNestedChild> NestedChildren { get; } = new List<ExplicitHttpBodyItemNestedChild>();
114116

115-
public ExplicitHttpBodyItemChild(int id)
117+
public ExplicitHttpBodyItemChild(int id, int anotherId)
118+
{
119+
Id = id;
120+
AnotherId = anotherId;
121+
}
122+
}
123+
124+
private sealed class ExplicitHttpBodyItemNestedChild
125+
{
126+
public int Id { get; }
127+
128+
public ExplicitHttpBodyItemNestedChild(int id)
116129
{
117130
Id = id;
118131
}
@@ -251,6 +264,20 @@ protected override void CollectMetadata(ISqlMetadataCollector collector)
251264
}
252265
}
253266

267+
private sealed class ExplicitHttpBodyItemNestedChildSet : StructuredType<ExplicitHttpBodyItemNestedChildSet>
268+
{
269+
public override string TypeName => "z";
270+
271+
public void Add(int itemid, int anotherid, int nestedchildid) => AddRecord(itemid, anotherid, nestedchildid);
272+
273+
protected override void CollectMetadata(ISqlMetadataCollector collector)
274+
{
275+
collector.RegisterMetadata("itemid", SqlDbType.Int);
276+
collector.RegisterMetadata("anotherid", SqlDbType.Int);
277+
collector.RegisterMetadata("nestedchildid", SqlDbType.Int);
278+
}
279+
}
280+
254281
private sealed class ImplicitHttpBodyItemSet : StructuredType<ImplicitHttpBodyItemSet>
255282
{
256283
public override string TypeName => "x";

0 commit comments

Comments
 (0)