Skip to content

Commit 9aa70c2

Browse files
committed
Added support for nested collection parameter mapping
1 parent 2ccb783 commit 9aa70c2

25 files changed

Lines changed: 842 additions & 58 deletions
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
namespace Dibix
2+
{
3+
internal sealed class NestedEnumerablePair<TParent, TChild>
4+
{
5+
public int ParentIndex { get; }
6+
public int ChildIndex { get; }
7+
public TParent Parent { get; }
8+
public TChild Child { get; }
9+
10+
public NestedEnumerablePair(int parentIndex, int childIndex, TParent parent, TChild child)
11+
{
12+
ParentIndex = parentIndex;
13+
ChildIndex = childIndex;
14+
Parent = parent;
15+
Child = child;
16+
}
17+
}
18+
}

src/Dibix.Http.Server/Dibix.Http.Server.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
<Compile Include="..\..\shared\Http\HttpParameterName.cs" Link="Runtime\%(Filename)%(Extension)" />
1515
<Compile Include="..\..\shared\Http\RouteBuilder.cs" Link="Utilities\%(Filename)%(Extension)" />
1616
<Compile Include="..\..\shared\Metadata\IPropertyDescriptor.cs" Link="Model\%(Filename)%(Extension)" />
17+
<Compile Include="..\..\shared\Metadata\NestedEnumerablePair.cs" Link="Model\%(Filename)%(Extension)" />
1718
<Compile Include="..\..\shared\Metadata\PrimitiveType.cs" Link="Model\%(Filename)%(Extension)" />
1819
<Compile Include="..\..\shared\Metadata\PrimitiveTypeReference.cs" Link="Model\%(Filename)%(Extension)" />
1920
<Compile Include="..\..\shared\Metadata\TypeReference.cs" Link="Model\%(Filename)%(Extension)" />

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

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

src/Dibix.Sdk.CodeGeneration/Dibix.Sdk.CodeGeneration.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
<Compile Include="..\..\shared\Json\JsonSchemaExtensions.cs" Link="Json\%(Filename)%(Extension)" />
2222
<Compile Include="..\..\shared\Json\ValidatingJsonDefinitionReader.cs" Link="Json\%(Filename)%(Extension)" />
2323
<Compile Include="..\..\shared\Metadata\IPropertyDescriptor.cs" Link="Model\%(Filename)%(Extension)" />
24+
<Compile Include="..\..\shared\Metadata\NestedEnumerablePair.cs" Link="Model\%(Filename)%(Extension)" />
2425
<Compile Include="..\..\shared\Metadata\PrimitiveType.cs" Link="Model\%(Filename)%(Extension)" />
2526
<Compile Include="..\..\shared\Metadata\PrimitiveTypeReference.cs" Link="Symbol\%(Filename)%(Extension)" />
2627
<Compile Include="..\..\shared\Metadata\TypeReference.cs" Link="Symbol\%(Filename)%(Extension)" />

src/Dibix.Sdk.CodeGeneration/Model/ActionParameterPropertySource.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ internal sealed class ActionParameterPropertySource : ActionParameterSource, IAc
88
{
99
public ActionParameterSourceDefinition Definition { get; }
1010
public string PropertyPath { get; }
11+
public string PropertyName => PropertyPath.Split('.')[0];
1112
public string Converter { get; }
1213
public IReadOnlyCollection<ActionParameterPropertySourceNode> Nodes { get; }
1314
public IReadOnlyCollection<ActionParameterItemSource> ItemSources { get; }

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

Lines changed: 53 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -106,26 +106,66 @@ private void CollectItemPropertySourceNodes(ActionParameterPropertySourceBuilder
106106
if (lastNode == null)
107107
throw new InvalidOperationException($"Missing resolved source property node for item property mapping ({rootPropertySourceBuilder.PropertyName})");
108108

109-
TypeReference type = lastNode.Property.Type;
110-
if (type is not SchemaTypeReference propertySchemaTypeReference)
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+
116+
IList<string> segments = new Collection<string>();
117+
118+
foreach (string propertyName in propertySource.PropertyName.Split('.'))
111119
{
112-
logger.LogError($"Unexpected type '{type?.GetType()}' for property '{rootPropertySourceBuilder.PropertyName}'. Only object schemas can be used for UDT item mappings.", rootPropertySourceBuilder.Location.Source, rootPropertySourceBuilder.Location.Line, rootPropertySourceBuilder.Location.Column);
113-
return;
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)
124+
{
125+
if (propertyName == nameof(NestedEnumerablePair<object, object>.Parent))
126+
{
127+
if (!(isObjectSchema = TryCollectNodeSchema(nestedEnumerableParent, out propertySchemaTypeReference, out objectSchema, logger, rootPropertySourceBuilder, schemaRegistry)))
128+
return;
129+
130+
continue;
131+
}
132+
133+
if (propertyName == nameof(NestedEnumerablePair<object, object>.Child))
134+
{
135+
continue;
136+
}
137+
}
138+
139+
segments.Add(propertyName);
114140
}
115141

116-
SchemaDefinition propertySchema = schemaRegistry.GetSchema(propertySchemaTypeReference);
117-
if (propertySchema is not ObjectSchema objectSchema)
118-
{
119-
logger.LogError($"Unexpected type '{propertySchema?.GetType()}' for property '{rootPropertySourceBuilder.PropertyName}'. Only object schemas can be used for UDT item mappings.", rootPropertySourceBuilder.Location.Source, rootPropertySourceBuilder.Location.Line, rootPropertySourceBuilder.Location.Column);
142+
if (!isObjectSchema)
120143
return;
144+
145+
CollectPropertySourceNodes(propertySource, segments, propertySchemaTypeReference, objectSchema);
146+
}
147+
148+
private static bool TryCollectNodeSchema(ActionParameterPropertySourceNode node, out SchemaTypeReference propertySchemaTypeReference, out ObjectSchema objectSchema, ILogger logger, ActionParameterPropertySourceBuilder rootPropertySourceBuilder, ISchemaRegistry schemaRegistry)
149+
{
150+
TypeReference type = node.Property.Type;
151+
if (type is not SchemaTypeReference typeReference)
152+
{
153+
propertySchemaTypeReference = null;
154+
objectSchema = null;
155+
return false;
121156
}
122157

123-
IList<string> segments = new Collection<string>();
124-
segments.AddRange(propertySource.PropertyName.Split('.'));
125-
if (segments.Last() == ItemParameterSource.IndexPropertyName)
126-
segments.RemoveAt(segments.Count - 1);
158+
SchemaDefinition propertySchema = schemaRegistry.GetSchema(typeReference);
159+
if (propertySchema is not ObjectSchema schema)
160+
{
161+
propertySchemaTypeReference = null;
162+
objectSchema = null;
163+
return false;
164+
}
127165

128-
CollectPropertySourceNodes(propertySource, segments, propertySchemaTypeReference, objectSchema);
166+
propertySchemaTypeReference = typeReference;
167+
objectSchema = schema;
168+
return true;
129169
}
130170

131171
private void CollectPropertySourceNodes(ActionParameterPropertySourceBuilder propertySourceBuilder, IEnumerable<string> segments, TypeReference typeReference, ObjectSchema schema)

src/Dibix.Sdk.CodeGeneration/Validation/UserDefinedTypeParameterModelValidator.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ private bool ValidateParameter(ActionParameter parameter, ActionDefinition actio
7070
ObjectSchemaProperty sourceProperty = bodyObjectSchema.Properties.SingleOrDefault(x => String.Equals(x.Name, parameter.InternalParameterName, StringComparison.OrdinalIgnoreCase));
7171
ActionParameterPropertySource propertySource = parameter.ParameterSource as ActionParameterPropertySource;
7272
if (sourceProperty == null && propertySource != null)
73-
sourceProperty = bodyObjectSchema.Properties.SingleOrDefault(x => String.Equals(x.Name, propertySource.PropertyPath, StringComparison.OrdinalIgnoreCase));
73+
sourceProperty = bodyObjectSchema.Properties.SingleOrDefault(x => String.Equals(x.Name, propertySource.PropertyName, StringComparison.OrdinalIgnoreCase));
7474

7575
ActionTarget target = action.Target;
7676
if (sourceProperty == null)

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

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ private sealed class ExplicitHttpBodyItem
9393
{
9494
public int Id { get; }
9595
public string Name { get; }
96+
public ExplicitHttpBodyItemChildContainer Child { get; set; }
9697

9798
public ExplicitHttpBodyItem(int id, string name)
9899
{
@@ -101,6 +102,22 @@ public ExplicitHttpBodyItem(int id, string name)
101102
}
102103
}
103104

105+
private sealed class ExplicitHttpBodyItemChildContainer
106+
{
107+
public ICollection<ExplicitHttpBodyItemChild> Children { get; } = new List<ExplicitHttpBodyItemChild>();
108+
public ICollection<int> PrimitiveChildren { get; } = new List<int>();
109+
}
110+
111+
private sealed class ExplicitHttpBodyItemChild
112+
{
113+
public int Id { get; }
114+
115+
public ExplicitHttpBodyItemChild(int id)
116+
{
117+
Id = id;
118+
}
119+
}
120+
104121
private sealed class ExplicitHttpBodyParameterInput
105122
{
106123
public int targetid { get; set; }
@@ -221,6 +238,19 @@ protected override void CollectMetadata(ISqlMetadataCollector collector)
221238
}
222239
}
223240

241+
private sealed class ExplicitHttpBodyItemChildSet : StructuredType<ExplicitHttpBodyItemChildSet>
242+
{
243+
public override string TypeName => "y";
244+
245+
public void Add(int itemid, int childid) => AddRecord(itemid, childid);
246+
247+
protected override void CollectMetadata(ISqlMetadataCollector collector)
248+
{
249+
collector.RegisterMetadata("itemid", SqlDbType.Int);
250+
collector.RegisterMetadata("childid", SqlDbType.Int);
251+
}
252+
}
253+
224254
private sealed class ImplicitHttpBodyItemSet : StructuredType<ImplicitHttpBodyItemSet>
225255
{
226256
public override string TypeName => "x";

0 commit comments

Comments
 (0)