Skip to content

Commit 5656777

Browse files
authored
Add support of dynamic property and index assignments (#353)
1 parent aa23c98 commit 5656777

3 files changed

Lines changed: 117 additions & 7 deletions

File tree

src/DynamicExpresso.Core/Parsing/Parser.cs

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -221,8 +221,8 @@ private Expression ParseAssignment()
221221
if (!_arguments.Settings.AssignmentOperators.HasFlag(AssignmentOperators.AssignmentEqual))
222222
throw new AssignmentOperatorDisabledException("=", _token.pos);
223223

224-
225-
if (!IsWritable(left))
224+
Func<Expression, Expression> assignLeftDynamic = null;
225+
if (!IsWritable(left) && !IsDynamicWritable(left, out assignLeftDynamic))
226226
throw ParseException.Create(_token.pos, ErrorMessages.ExpressionMustBeWritable);
227227

228228
NextToken();
@@ -233,7 +233,9 @@ private Expression ParseAssignment()
233233
throw ParseException.Create(_token.pos, ErrorMessages.CannotConvertValue,
234234
TypeUtils.GetTypeName(right.Type), TypeUtils.GetTypeName(left.Type));
235235

236-
left = Expression.Assign(left, promoted);
236+
left = assignLeftDynamic == null
237+
? Expression.Assign(left, promoted)
238+
: assignLeftDynamic(promoted);
237239
}
238240
return left;
239241
}
@@ -1813,7 +1815,7 @@ private static Expression ParseDynamicIndex(Type type, Expression instance, Expr
18131815
{
18141816
var argsDynamic = args.ToList();
18151817
argsDynamic.Insert(0, instance);
1816-
return Expression.Dynamic(new LateInvokeIndexCallSiteBinder(), typeof(object), argsDynamic);
1818+
return Expression.Dynamic(new LateGetIndexCallSiteBinder(), typeof(object), argsDynamic);
18171819
}
18181820

18191821
private Expression[] ParseArgumentList(TokenId openToken, string missingOpenTokenMsg,
@@ -1948,6 +1950,20 @@ private static bool IsWritable(Expression expression)
19481950
return false;
19491951
}
19501952

1953+
private static bool IsDynamicWritable(Expression expression, out Func<Expression, Expression> toWritableExpression)
1954+
{
1955+
toWritableExpression = null;
1956+
if (expression.NodeType != ExpressionType.Dynamic)
1957+
return false;
1958+
1959+
var dynamicExpression = (DynamicExpression)expression;
1960+
if (!(dynamicExpression.Binder is IConvertibleToWritableBinder convertibleBinder))
1961+
return false;
1962+
1963+
toWritableExpression = value => Expression.Dynamic(convertibleBinder.ToWritableBinder(), typeof(object), dynamicExpression.Arguments.Concat(new[] { value }));
1964+
return true;
1965+
}
1966+
19511967
private Expression GenerateEqual(Expression left, Expression right)
19521968
{
19531969
return GenerateBinary(ExpressionType.Equal, left, right);

src/DynamicExpresso.Core/Resolution/LateBinders.cs

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@
88

99
namespace DynamicExpresso.Resolution
1010
{
11-
internal class LateGetMemberCallSiteBinder : CallSiteBinder
11+
internal interface IConvertibleToWritableBinder
12+
{
13+
CallSiteBinder ToWritableBinder();
14+
}
15+
16+
internal class LateGetMemberCallSiteBinder : CallSiteBinder, IConvertibleToWritableBinder
1217
{
1318
private readonly string _propertyOrFieldName;
1419

@@ -19,11 +24,39 @@ public LateGetMemberCallSiteBinder(string propertyOrFieldName)
1924

2025
public override Expression Bind(object[] args, ReadOnlyCollection<ParameterExpression> parameters, LabelTarget returnLabel)
2126
{
27+
// there's only one argument: the instance on which the member is accessed
2228
var binder = Binder.GetMember(
2329
CSharpBinderFlags.None,
2430
_propertyOrFieldName,
2531
TypeUtils.RemoveArrayType(args[0]?.GetType()),
26-
new[] { CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null) }
32+
parameters.Select(x => CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null))
33+
);
34+
return binder.Bind(args, parameters, returnLabel);
35+
}
36+
37+
public CallSiteBinder ToWritableBinder()
38+
{
39+
return new LateSetMemberCallSiteBinder(_propertyOrFieldName);
40+
}
41+
}
42+
43+
internal class LateSetMemberCallSiteBinder : CallSiteBinder
44+
{
45+
private readonly string _propertyOrFieldName;
46+
47+
public LateSetMemberCallSiteBinder(string propertyOrFieldName)
48+
{
49+
_propertyOrFieldName = propertyOrFieldName;
50+
}
51+
52+
public override Expression Bind(object[] args, ReadOnlyCollection<ParameterExpression> parameters, LabelTarget returnLabel)
53+
{
54+
// there are two arguments: the instance on which the member is set and the value to set
55+
var binder = Binder.SetMember(
56+
CSharpBinderFlags.None,
57+
_propertyOrFieldName,
58+
TypeUtils.RemoveArrayType(args[0]?.GetType()),
59+
parameters.Select(x => CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null))
2760
);
2861
return binder.Bind(args, parameters, returnLabel);
2962
}
@@ -96,16 +129,36 @@ public override Expression Bind(object[] args, ReadOnlyCollection<ParameterExpre
96129
/// <summary>
97130
/// Binds to an items invocation of an instance as late as possible. This allows the use of anonymous types on dynamic values.
98131
/// </summary>
99-
internal class LateInvokeIndexCallSiteBinder : CallSiteBinder
132+
internal class LateGetIndexCallSiteBinder : CallSiteBinder, IConvertibleToWritableBinder
100133
{
101134
public override Expression Bind(object[] args, ReadOnlyCollection<ParameterExpression> parameters, LabelTarget returnLabel)
102135
{
136+
// there are two arguments: the instance on which the member is set and the value of the indexer
103137
var binder = Binder.GetIndex(
104138
CSharpBinderFlags.None,
105139
TypeUtils.RemoveArrayType(args[0]?.GetType()),
106140
parameters.Select(x => CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null))
107141
);
108142
return binder.Bind(args, parameters, returnLabel);
109143
}
144+
145+
public CallSiteBinder ToWritableBinder()
146+
{
147+
return new LateSetIndexCallSiteBinder();
148+
}
149+
}
150+
151+
internal class LateSetIndexCallSiteBinder : CallSiteBinder
152+
{
153+
public override Expression Bind(object[] args, ReadOnlyCollection<ParameterExpression> parameters, LabelTarget returnLabel)
154+
{
155+
// there are three arguments: the instance on which the member is set, the value of the indexer, and the value to set
156+
var binder = Binder.SetIndex(
157+
CSharpBinderFlags.None,
158+
TypeUtils.RemoveArrayType(args[0]?.GetType()),
159+
parameters.Select(x => CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null))
160+
);
161+
return binder.Bind(args, parameters, returnLabel);
162+
}
110163
}
111164
}

test/DynamicExpresso.UnitTest/DynamicTest.cs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,18 @@ public void Get_Property_of_an_ExpandoObject()
2323
Assert.That(interpreter.Eval("dyn.Foo"), Is.EqualTo(dyn.Foo));
2424
}
2525

26+
[Test]
27+
public void Set_Property_of_an_ExpandoObject()
28+
{
29+
dynamic dyn = new ExpandoObject();
30+
var interpreter = new Interpreter(InterpreterOptions.Default | InterpreterOptions.LateBindObject)
31+
.SetVariable("dyn", (object)dyn);
32+
33+
interpreter.Eval("dyn.Foo = 10");
34+
35+
Assert.That(dyn.Foo, Is.EqualTo(10));
36+
}
37+
2638
[Test]
2739
public void Get_Property_of_a_nested_anonymous()
2840
{
@@ -32,6 +44,20 @@ public void Get_Property_of_a_nested_anonymous()
3244
Assert.That(interpreter.Eval("dyn.Sub.Foo.Bar.Foo2"), Is.EqualTo(dyn.Sub.Foo.Bar.Foo2));
3345
}
3446

47+
[Test]
48+
public void Set_Property_of_a_nested_ExpandoObject()
49+
{
50+
dynamic dyn = new ExpandoObject();
51+
dyn.Sub = new ExpandoObject();
52+
dyn.Sub.Foo = "bar";
53+
54+
var interpreter = new Interpreter().SetVariable("dyn", (object)dyn);
55+
56+
interpreter.Eval("dyn.Sub.Foo = \"foobar\"");
57+
58+
Assert.That(dyn.Sub.Foo, Is.EqualTo("foobar"));
59+
}
60+
3561
[Test]
3662
public void Get_Property_of_a_nested_ExpandoObject()
3763
{
@@ -183,6 +209,21 @@ public void Get_value_of_an_array_of_anonymous_type()
183209
Assert.That(interpreter.Eval("dyn.Sub.ObjArr[1].Foo"), Is.SameAs(dyn.Sub.ObjArr[1].Foo));
184210
}
185211

212+
[Test]
213+
public void Set_value_of_a_dynamic_object()
214+
{
215+
dynamic dyn = new ExpandoObject();
216+
dyn.Foo = new int[5];
217+
dyn.Bar = new Dictionary<string, int>();
218+
219+
var interpreter = new Interpreter().SetVariable("dyn", (object)dyn);
220+
interpreter.Eval("dyn.Foo[2] = 5");
221+
interpreter.Eval("dyn.Bar[\"foo\"] = 50");
222+
223+
Assert.That(dyn.Foo[2], Is.EqualTo(5));
224+
Assert.That(dyn.Bar["foo"], Is.EqualTo(50));
225+
}
226+
186227
[Test]
187228
public void Get_value_of_a_nested_array_error()
188229
{

0 commit comments

Comments
 (0)