Skip to content

Commit e33dc6c

Browse files
CopilotStefH
andauthored
Fix NotSupportedException when parsing nested object initialization (#979)
* Initial plan * Fix error when parsing nested object initialization (issue #701) Agent-Logs-Url: https://github.com/zzzprojects/System.Linq.Dynamic.Core/sessions/4683789c-0227-43d7-9432-4863adea1d4c Co-authored-by: StefH <249938+StefH@users.noreply.github.com> * Refactor TryRebuildMemberInitExpression to use out parameter pattern Agent-Logs-Url: https://github.com/zzzprojects/System.Linq.Dynamic.Core/sessions/537f61ea-9dbe-4ae4-9fa8-0358f826f195 Co-authored-by: StefH <249938+StefH@users.noreply.github.com> * Add three-level-deep nested new test to exercise recursive TryRebuildMemberInitExpression Agent-Logs-Url: https://github.com/zzzprojects/System.Linq.Dynamic.Core/sessions/42b1ec4e-90f4-484b-ae3a-73f3bee9c50b Co-authored-by: StefH <249938+StefH@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: StefH <249938+StefH@users.noreply.github.com>
1 parent b939626 commit e33dc6c

File tree

2 files changed

+121
-3
lines changed

2 files changed

+121
-3
lines changed

src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1678,14 +1678,70 @@ private Expression CreateNewExpression(List<DynamicProperty> properties, List<Ex
16781678
propertyOrFieldType = fieldInfo.FieldType;
16791679
}
16801680

1681-
// Call Promote and if that returns false, just try to convert the expression to the destination type using Expression.Convert
1682-
var promoted = _parsingConfig.ExpressionPromoter.Promote(expressions[i], propertyOrFieldType, true, true) ?? Expression.Convert(expressions[i], propertyOrFieldType);
1683-
memberBindings[i] = Expression.Bind(memberInfo, promoted);
1681+
// Call Promote and if that returns null, try to rebuild a nested MemberInitExpression for the target type,
1682+
// and if that also fails, try to convert the expression to the destination type using Expression.Convert.
1683+
var promoted = _parsingConfig.ExpressionPromoter.Promote(expressions[i], propertyOrFieldType, true, true);
1684+
if (promoted == null && expressions[i] is MemberInitExpression memberInitExpression &&
1685+
TryRebuildMemberInitExpression(memberInitExpression, propertyOrFieldType, out var rebuilt))
1686+
{
1687+
promoted = rebuilt;
1688+
}
1689+
memberBindings[i] = Expression.Bind(memberInfo, promoted ?? Expression.Convert(expressions[i], propertyOrFieldType));
16841690
}
16851691

16861692
return Expression.MemberInit(Expression.New(type), memberBindings);
16871693
}
16881694

1695+
private static bool TryRebuildMemberInitExpression(MemberInitExpression memberInitExpression, Type targetType, [NotNullWhen(true)] out Expression? expression)
1696+
{
1697+
expression = null;
1698+
1699+
var defaultConstructor = targetType.GetConstructor(Type.EmptyTypes);
1700+
if (defaultConstructor == null)
1701+
{
1702+
return false;
1703+
}
1704+
1705+
var newBindings = new MemberBinding[memberInitExpression.Bindings.Count];
1706+
for (var i = 0; i < memberInitExpression.Bindings.Count; i++)
1707+
{
1708+
if (memberInitExpression.Bindings[i] is not MemberAssignment assignment)
1709+
{
1710+
return false;
1711+
}
1712+
1713+
var memberName = assignment.Member.Name;
1714+
MemberInfo? targetMember = targetType.GetProperty(memberName) ?? (MemberInfo?)targetType.GetField(memberName);
1715+
if (targetMember == null)
1716+
{
1717+
return false;
1718+
}
1719+
1720+
var targetMemberType = targetMember is PropertyInfo targetPropertyInfo
1721+
? targetPropertyInfo.PropertyType
1722+
: ((FieldInfo)targetMember).FieldType;
1723+
1724+
var bindingExpression = assignment.Expression;
1725+
if (bindingExpression is MemberInitExpression nestedMemberInit && bindingExpression.Type != targetMemberType)
1726+
{
1727+
if (!TryRebuildMemberInitExpression(nestedMemberInit, targetMemberType, out var rebuiltNested))
1728+
{
1729+
return false;
1730+
}
1731+
bindingExpression = rebuiltNested;
1732+
}
1733+
else if (bindingExpression.Type != targetMemberType)
1734+
{
1735+
bindingExpression = Expression.Convert(bindingExpression, targetMemberType);
1736+
}
1737+
1738+
newBindings[i] = Expression.Bind(targetMember, bindingExpression);
1739+
}
1740+
1741+
expression = Expression.MemberInit(Expression.New(targetType), newBindings);
1742+
return true;
1743+
}
1744+
16891745
private Expression ParseLambdaInvocation(LambdaExpression lambda)
16901746
{
16911747
int errorPos = _textParser.CurrentToken.Pos;

test/System.Linq.Dynamic.Core.Tests/DynamicExpressionParserTests.cs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2303,6 +2303,68 @@ public void DynamicExpressionParser_ParseLambda_LambdaParameter_SameNameAsDynami
23032303
DynamicExpressionParser.ParseLambda<bool>(new ParsingConfig(), false, "new[]{1,2,3}.Any(z => z > 0)");
23042304
}
23052305

2306+
// https://github.com/zzzprojects/System.Linq.Dynamic.Core/issues/701
2307+
[Fact]
2308+
public void DynamicExpressionParser_ParseLambda_NestedObjectInitialization()
2309+
{
2310+
// Arrange
2311+
var srcType = typeof(CustomerForNestedNewTest);
2312+
2313+
// Act
2314+
var lambda = DynamicExpressionParser.ParseLambda(ParsingConfig.DefaultEFCore21, srcType, srcType, "new (new (3 as Id) as CurrentDepartment)");
2315+
var @delegate = lambda.Compile();
2316+
var result = (CustomerForNestedNewTest)@delegate.DynamicInvoke(new CustomerForNestedNewTest())!;
2317+
2318+
// Assert
2319+
result.Should().NotBeNull();
2320+
result.CurrentDepartment.Should().NotBeNull();
2321+
result.CurrentDepartment!.Id.Should().Be(3);
2322+
}
2323+
2324+
// https://github.com/zzzprojects/System.Linq.Dynamic.Core/issues/701
2325+
[Fact]
2326+
public void DynamicExpressionParser_ParseLambda_NestedObjectInitialization_ThreeLevelsDeep()
2327+
{
2328+
// Arrange — exercises the recursive TryRebuildMemberInitExpression path.
2329+
// The parser propagates _resultType (CustomerForNestedNewTest) into all nested new
2330+
// expressions. The middle "new (new (3 as Id) as Sub)" therefore builds a
2331+
// MIE<Customer>{ Sub = MIE<Department>{Id=3} }. When the outer new binds that to its own
2332+
// "Sub" property (type DepartmentForNestedNewTest), TryRebuildMemberInitExpression is
2333+
// called and encounters the inner MIE<Department>{Id=3} binding — a MemberInitExpression
2334+
// itself — which triggers the recursive call to rebuild it for SubDepartmentForNestedNewTest.
2335+
var srcType = typeof(CustomerForNestedNewTest);
2336+
2337+
// Act
2338+
var lambda = DynamicExpressionParser.ParseLambda(ParsingConfig.DefaultEFCore21, srcType, srcType,
2339+
"new (new (new (3 as Id) as Sub) as Sub)");
2340+
var @delegate = lambda.Compile();
2341+
var result = (CustomerForNestedNewTest)@delegate.DynamicInvoke(new CustomerForNestedNewTest())!;
2342+
2343+
// Assert
2344+
result.Should().NotBeNull();
2345+
result.Sub.Should().NotBeNull();
2346+
result.Sub!.Sub.Should().NotBeNull();
2347+
result.Sub.Sub!.Id.Should().Be(3);
2348+
}
2349+
2350+
public class CustomerForNestedNewTest
2351+
{
2352+
public int Id { get; set; }
2353+
public DepartmentForNestedNewTest? CurrentDepartment { get; set; }
2354+
public DepartmentForNestedNewTest? Sub { get; set; }
2355+
}
2356+
2357+
public class DepartmentForNestedNewTest
2358+
{
2359+
public int Id { get; set; }
2360+
public SubDepartmentForNestedNewTest? Sub { get; set; }
2361+
}
2362+
2363+
public class SubDepartmentForNestedNewTest
2364+
{
2365+
public int Id { get; set; }
2366+
}
2367+
23062368
public class DefaultDynamicLinqCustomTypeProviderForGenericExtensionMethod : DefaultDynamicLinqCustomTypeProvider
23072369
{
23082370
public DefaultDynamicLinqCustomTypeProviderForGenericExtensionMethod() : base(ParsingConfig.Default)

0 commit comments

Comments
 (0)