Skip to content

Commit

Permalink
Adding fuller support for resolving anonymous type data on a dynamic …
Browse files Browse the repository at this point in the history
…object (#258)

* First attempt at accessing anonymous types in dynamic.

* Updated comments for dynamic binding.

* Added additional unit test to make sure we handle null values in the CallSiteBinding the same way as compiled code.

* Incorporated Code Review Comments and expanded a couple of unit tests.

Co-authored-by: Holden Mai <[email protected]>
Co-authored-by: Christophe PLAT <[email protected]>
  • Loading branch information
3 people authored Sep 26, 2022
1 parent 2b0661f commit 115ffa4
Show file tree
Hide file tree
Showing 3 changed files with 153 additions and 23 deletions.
1 change: 1 addition & 0 deletions src/DynamicExpresso.Core/Interpreter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Linq;
using System.Linq.Expressions;
using DynamicExpresso.Exceptions;
using System.Dynamic;

namespace DynamicExpresso
{
Expand Down
108 changes: 85 additions & 23 deletions src/DynamicExpresso.Core/Parsing/Parser.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Dynamic;
using System.Globalization;
Expand Down Expand Up @@ -1792,43 +1793,38 @@ private Expression ParseNormalMethodInvocation(Type type, Expression instance, i
// return Expression.Call(typeof(Enumerable), signature.Name, typeArgs, args);
//}

private static Expression ParseDynamicProperty(Type type, Expression instance, string propertyOrFieldName)
/// <summary>
/// Returns null if <paramref name="t"/> is an Array type. Needed because the <seealso cref="Microsoft.CSharp.RuntimeBinder.Binder"/> lookup methods fail with a <seealso cref="InvalidCastException"/> if the array type is used.
/// Everything still miraculously works on the array if null is given for the type.
/// </summary>
/// <param name="t"></param>
/// <returns></returns>
private static Type RemoveArrayType(Type t)
{
var binder = Microsoft.CSharp.RuntimeBinder.Binder.GetMember(
Microsoft.CSharp.RuntimeBinder.CSharpBinderFlags.None,
propertyOrFieldName,
type,
new[] { Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo.Create(Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfoFlags.None, null) }
);
if (t == null || t.IsArray)
{
return null;
}
return t;
}

return Expression.Dynamic(binder, typeof(object), instance);
private static Expression ParseDynamicProperty(Type type, Expression instance, string propertyOrFieldName)
{
return Expression.Dynamic(new LateGetMemberCallSiteBinder(propertyOrFieldName), typeof(object), instance);
}

private static Expression ParseDynamicMethodInvocation(Type type, Expression instance, string methodName, Expression[] args)
{
var argsDynamic = args.ToList();
argsDynamic.Insert(0, instance);
var binderM = Microsoft.CSharp.RuntimeBinder.Binder.InvokeMember(
Microsoft.CSharp.RuntimeBinder.CSharpBinderFlags.None,
methodName,
null,
type,
argsDynamic.Select(x => Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo.Create(Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfoFlags.None, null))
);

return Expression.Dynamic(binderM, typeof(object), argsDynamic);
return Expression.Dynamic(new LateInvokeMethodCallSiteBinder(methodName), typeof(object), argsDynamic);
}

private static Expression ParseDynamicIndex(Type type, Expression instance, Expression[] args)
{
var argsDynamic = args.ToList();
argsDynamic.Insert(0, instance);
var binder = Microsoft.CSharp.RuntimeBinder.Binder.GetIndex(
Microsoft.CSharp.RuntimeBinder.CSharpBinderFlags.None,
type,
argsDynamic.Select(x => Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo.Create(Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfoFlags.None, null))
);
return Expression.Dynamic(binder, typeof(object), argsDynamic);
return Expression.Dynamic(new LateInvokeIndexCallSiteBinder(), typeof(object), argsDynamic);
}

private Expression[] ParseArgumentList(TokenId openToken, string missingOpenTokenMsg,
Expand Down Expand Up @@ -3468,5 +3464,71 @@ private static T[] RemoveLast<T>(T[] array)
return result;
}
}

/// <summary>
/// Binds to a member access of an instance as late as possible. This allows the use of anonymous types on dynamic values.
/// </summary>
private class LateGetMemberCallSiteBinder : CallSiteBinder
{
private readonly string _propertyOrFieldName;

public LateGetMemberCallSiteBinder(string propertyOrFieldName)
{
_propertyOrFieldName = propertyOrFieldName;
}

public override Expression Bind(object[] args, ReadOnlyCollection<ParameterExpression> parameters, LabelTarget returnLabel)
{
var binder = Microsoft.CSharp.RuntimeBinder.Binder.GetMember(
Microsoft.CSharp.RuntimeBinder.CSharpBinderFlags.None,
_propertyOrFieldName,
RemoveArrayType(args[0]?.GetType()),
new[] { Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo.Create(Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfoFlags.None, null) }
);
return binder.Bind(args, parameters, returnLabel);
}
}

/// <summary>
/// Binds to a method invocation of an instance as late as possible. This allows the use of anonymous types on dynamic values.
/// </summary>
private class LateInvokeMethodCallSiteBinder : CallSiteBinder
{
private readonly string _methodName;

public LateInvokeMethodCallSiteBinder(string methodName)
{
_methodName = methodName;
}

public override Expression Bind(object[] args, ReadOnlyCollection<ParameterExpression> parameters, LabelTarget returnLabel)
{
var binderM = Microsoft.CSharp.RuntimeBinder.Binder.InvokeMember(
Microsoft.CSharp.RuntimeBinder.CSharpBinderFlags.None,
_methodName,
null,
RemoveArrayType(args[0]?.GetType()),
parameters.Select(x => Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo.Create(Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfoFlags.None, null))
);
return binderM.Bind(args, parameters, returnLabel);
}
}

/// <summary>
/// Binds to an items invocation of an instance as late as possible. This allows the use of anonymous types on dynamic values.
/// </summary>
private class LateInvokeIndexCallSiteBinder : CallSiteBinder
{
public override Expression Bind(object[] args, ReadOnlyCollection<ParameterExpression> parameters, LabelTarget returnLabel)
{
var binder = Microsoft.CSharp.RuntimeBinder.Binder.GetIndex(
Microsoft.CSharp.RuntimeBinder.CSharpBinderFlags.None,
RemoveArrayType(args[0]?.GetType()),
parameters.Select(x => Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo.Create(Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfoFlags.None, null))
);
return binder.Bind(args, parameters, returnLabel);
}
}

}
}
67 changes: 67 additions & 0 deletions test/DynamicExpresso.UnitTest/DynamicTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,15 @@ public void Get_Property_of_an_ExpandoObject()
Assert.AreEqual(dyn.Foo, interpreter.Eval("dyn.Foo"));
}

[Test]
public void Get_Property_of_a_nested_anonymous()
{
dynamic dyn = new ExpandoObject();
dyn.Sub = new { Foo = new { Bar = new { Foo2 = "bar" } } };
var interpreter = new Interpreter().SetVariable("dyn", (object)dyn);
Assert.AreEqual(dyn.Sub.Foo.Bar.Foo2, interpreter.Eval("dyn.Sub.Foo.Bar.Foo2"));
}

[Test]
public void Get_Property_of_a_nested_ExpandoObject()
{
Expand Down Expand Up @@ -86,6 +95,19 @@ public void Invoke_Method_of_a_nested_ExpandoObject()
Assert.AreEqual(dyn.Sub.Foo(), interpreter.Eval("dyn.Sub.Foo()"));
}

[Test]
public void Invoke_Method_of_a_nested_ExpandoObject_WithAnonymousType()
{
dynamic dyn = new ExpandoObject();
dyn.Sub = new ExpandoObject();
dyn.Sub.Foo = new { Func = new Func<string>(() => "bar") };

var interpreter = new Interpreter()
.SetVariable("dyn", dyn);

Assert.AreEqual(dyn.Sub.Foo.Func(), interpreter.Eval("dyn.Sub.Foo.Func()"));
}

[Test]
public void Standard_methods_have_precedence_over_dynamic_methods()
{
Expand Down Expand Up @@ -116,6 +138,51 @@ public void Get_value_of_a_nested_array()
Assert.AreEqual(dyn.Sub[0], interpreter.Eval("dyn.Sub[0]"));
}

[Test]
public void Get_value_of_a_nested_array_from_anonymous_type()
{
dynamic dyn = new ExpandoObject();
dyn.Sub = new { Foo = new int[] { 42 }, Bar = new { Sub = new int[] { 43 } } };
var interpreter = new Interpreter().SetVariable("dyn", (object)dyn);
Assert.AreEqual(dyn.Sub.Foo[0], interpreter.Eval("dyn.Sub.Foo[0]"));
Assert.AreEqual(dyn.Sub.Bar.Sub[0], interpreter.Eval("dyn.Sub.Bar.Sub[0]"));
Assert.AreEqual(dyn.Sub.Bar.Sub.Length, interpreter.Eval("dyn.Sub.Bar.Sub.Length"));
}

[Test]
public void Get_value_of_an_array_of_anonymous_type()
{
dynamic dyn = new ExpandoObject();
var anonType1 = new { Foo = string.Empty };
var anonType2 = new { Foo = "string.Empty" };
var nullAnonType = anonType1;
nullAnonType = null;
dyn.Sub = new
{
Arg1 = anonType1,
Arg2 = anonType2,
Arg3 = nullAnonType,
Arr = new[] { anonType1, anonType2, nullAnonType },
ObjArr = new object[] { "Test", anonType1 }
};
var interpreter = new Interpreter().SetVariable("dyn", (object)dyn);
Assert.AreSame(dyn.Sub.Arg1.Foo, interpreter.Eval("dyn.Sub.Arg1.Foo"));
Assert.AreSame(dyn.Sub.Arg2.Foo, interpreter.Eval("dyn.Sub.Arg2.Foo"));
Assert.Throws<RuntimeBinderException>(() => Console.WriteLine(dyn.Sub.Arg3.Foo));
Assert.Throws<RuntimeBinderException>(() => interpreter.Eval("dyn.Sub.Arg3.Foo"));
Assert.AreSame(dyn.Sub.Arr[0].Foo, interpreter.Eval("dyn.Sub.Arr[0].Foo"));
Assert.AreSame(dyn.Sub.Arr[1].Foo, interpreter.Eval("dyn.Sub.Arr[1].Foo"));

Assert.Throws<RuntimeBinderException>(() => Console.WriteLine(dyn.Sub.Arr[2].Foo));
Assert.Throws<RuntimeBinderException>(() => interpreter.Eval("dyn.Sub.Arr[2].Foo"));

Assert.AreSame(dyn.Sub.ObjArr[0], interpreter.Eval("dyn.Sub.ObjArr[0]"));
Assert.AreEqual(dyn.Sub.ObjArr[0].Length, interpreter.Eval("dyn.Sub.ObjArr[0].Length"));

Assert.AreSame(dyn.Sub.ObjArr[1], interpreter.Eval("dyn.Sub.ObjArr[1]"));
Assert.AreSame(dyn.Sub.ObjArr[1].Foo, interpreter.Eval("dyn.Sub.ObjArr[1].Foo"));
}

[Test]
public void Get_value_of_a_nested_array_error()
{
Expand Down

0 comments on commit 115ffa4

Please sign in to comment.