Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 59 additions & 3 deletions src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1514,7 +1514,7 @@
{
if (!propertyNames.Add(propName!))
{
throw ParseError(exprPos, Res.DuplicateIdentifier, propName);

Check warning on line 1517 in src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs

View workflow job for this annotation

GitHub Actions / Windows: Build and Tests

Possible null reference argument for parameter 'args' in 'Exception ExpressionParser.ParseError(int pos, string format, params object[] args)'.

Check warning on line 1517 in src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs

View workflow job for this annotation

GitHub Actions / Linux: Build and Tests

Possible null reference argument for parameter 'args' in 'Exception ExpressionParser.ParseError(int pos, string format, params object[] args)'.
}

properties.Add(new DynamicProperty(propName!, expr.Type));
Expand Down Expand Up @@ -1678,14 +1678,70 @@
propertyOrFieldType = fieldInfo.FieldType;
}

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

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

private static bool TryRebuildMemberInitExpression(MemberInitExpression memberInitExpression, Type targetType, [NotNullWhen(true)] out Expression? expression)
{
expression = null;

var defaultConstructor = targetType.GetConstructor(Type.EmptyTypes);
if (defaultConstructor == null)
{
return false;
}

var newBindings = new MemberBinding[memberInitExpression.Bindings.Count];
for (var i = 0; i < memberInitExpression.Bindings.Count; i++)
{
if (memberInitExpression.Bindings[i] is not MemberAssignment assignment)
{
return false;
}

var memberName = assignment.Member.Name;
MemberInfo? targetMember = targetType.GetProperty(memberName) ?? (MemberInfo?)targetType.GetField(memberName);
if (targetMember == null)
{
return false;
}

var targetMemberType = targetMember is PropertyInfo targetPropertyInfo
? targetPropertyInfo.PropertyType
: ((FieldInfo)targetMember).FieldType;

var bindingExpression = assignment.Expression;
if (bindingExpression is MemberInitExpression nestedMemberInit && bindingExpression.Type != targetMemberType)
{
if (!TryRebuildMemberInitExpression(nestedMemberInit, targetMemberType, out var rebuiltNested))
{
return false;
}
bindingExpression = rebuiltNested;
}
else if (bindingExpression.Type != targetMemberType)
{
bindingExpression = Expression.Convert(bindingExpression, targetMemberType);
}

newBindings[i] = Expression.Bind(targetMember, bindingExpression);
}

expression = Expression.MemberInit(Expression.New(targetType), newBindings);
return true;
}

private Expression ParseLambdaInvocation(LambdaExpression lambda)
{
int errorPos = _textParser.CurrentToken.Pos;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2303,6 +2303,68 @@ public void DynamicExpressionParser_ParseLambda_LambdaParameter_SameNameAsDynami
DynamicExpressionParser.ParseLambda<bool>(new ParsingConfig(), false, "new[]{1,2,3}.Any(z => z > 0)");
}

// https://github.com/zzzprojects/System.Linq.Dynamic.Core/issues/701
[Fact]
public void DynamicExpressionParser_ParseLambda_NestedObjectInitialization()
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a unit test which goes 1 level deeper so that the code

if (bindingExpression is MemberInitExpression nestedMemberInit && bindingExpression.Type != targetMemberType)
{
    if (!TryRebuildMemberInitExpression(nestedMemberInit, targetMemberType, out var rebuiltNested))
    {
        return false;
    }
    bindingExpression = rebuiltNested;
}
else if (bindingExpression.Type != targetMemberType)
{
    bindingExpression = Expression.Convert(bindingExpression, targetMemberType);
}

is also tested

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in ddb00b9. The new test DynamicExpressionParser_ParseLambda_NestedObjectInitialization_ThreeLevelsDeep uses expression new (new (new (3 as Id) as Sub) as Sub) with models that have a shared Sub property name at each level (CustomerForNestedNewTest.Sub → DepartmentForNestedNewTest, DepartmentForNestedNewTest.Sub → SubDepartmentForNestedNewTest). This causes the outer rebuild to encounter an inner MemberInitExpression binding, triggering the recursive call path.

{
// Arrange
var srcType = typeof(CustomerForNestedNewTest);

// Act
var lambda = DynamicExpressionParser.ParseLambda(ParsingConfig.DefaultEFCore21, srcType, srcType, "new (new (3 as Id) as CurrentDepartment)");
var @delegate = lambda.Compile();
var result = (CustomerForNestedNewTest)@delegate.DynamicInvoke(new CustomerForNestedNewTest())!;

// Assert
result.Should().NotBeNull();
result.CurrentDepartment.Should().NotBeNull();
result.CurrentDepartment!.Id.Should().Be(3);
}

// https://github.com/zzzprojects/System.Linq.Dynamic.Core/issues/701
[Fact]
public void DynamicExpressionParser_ParseLambda_NestedObjectInitialization_ThreeLevelsDeep()
{
// Arrange — exercises the recursive TryRebuildMemberInitExpression path.
// The parser propagates _resultType (CustomerForNestedNewTest) into all nested new
// expressions. The middle "new (new (3 as Id) as Sub)" therefore builds a
// MIE<Customer>{ Sub = MIE<Department>{Id=3} }. When the outer new binds that to its own
// "Sub" property (type DepartmentForNestedNewTest), TryRebuildMemberInitExpression is
// called and encounters the inner MIE<Department>{Id=3} binding — a MemberInitExpression
// itself — which triggers the recursive call to rebuild it for SubDepartmentForNestedNewTest.
var srcType = typeof(CustomerForNestedNewTest);

// Act
var lambda = DynamicExpressionParser.ParseLambda(ParsingConfig.DefaultEFCore21, srcType, srcType,
"new (new (new (3 as Id) as Sub) as Sub)");
var @delegate = lambda.Compile();
var result = (CustomerForNestedNewTest)@delegate.DynamicInvoke(new CustomerForNestedNewTest())!;

// Assert
result.Should().NotBeNull();
result.Sub.Should().NotBeNull();
result.Sub!.Sub.Should().NotBeNull();
result.Sub.Sub!.Id.Should().Be(3);
}

public class CustomerForNestedNewTest
{
public int Id { get; set; }
public DepartmentForNestedNewTest? CurrentDepartment { get; set; }
public DepartmentForNestedNewTest? Sub { get; set; }
}

public class DepartmentForNestedNewTest
{
public int Id { get; set; }
public SubDepartmentForNestedNewTest? Sub { get; set; }
}

public class SubDepartmentForNestedNewTest
{
public int Id { get; set; }
}

public class DefaultDynamicLinqCustomTypeProviderForGenericExtensionMethod : DefaultDynamicLinqCustomTypeProvider
{
public DefaultDynamicLinqCustomTypeProviderForGenericExtensionMethod() : base(ParsingConfig.Default)
Expand Down
Loading