Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding fuller support for resolving anonymous type data on a dynamic object #258

Merged
merged 6 commits into from
Sep 26, 2022
Merged
Show file tree
Hide file tree
Changes from 3 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
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
110 changes: 87 additions & 23 deletions src/DynamicExpresso.Core/Parsing/Parser.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Dynamic;
using System.Globalization;
using System.Linq;
Expand Down Expand Up @@ -1676,43 +1677,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;
metoule marked this conversation as resolved.
Show resolved Hide resolved
}
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 @@ -3340,5 +3336,73 @@ 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 m_propertyOrFieldName;
metoule marked this conversation as resolved.
Show resolved Hide resolved

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

public override Expression Bind(object[] args, ReadOnlyCollection<ParameterExpression> parameters, LabelTarget returnLabel)
{
var binder =
Microsoft.CSharp.RuntimeBinder.Binder.GetMember(
metoule marked this conversation as resolved.
Show resolved Hide resolved
Microsoft.CSharp.RuntimeBinder.CSharpBinderFlags.None,
m_propertyOrFieldName,
RemoveArrayType(args[0]?.GetType()),
new[] { Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo.Create(Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfoFlags.None, null) }
);
var e = binder.Bind(args, parameters, returnLabel);
return e;
metoule marked this conversation as resolved.
Show resolved Hide resolved
}
}

/// <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);
}
}

}
}
55 changes: 54 additions & 1 deletion test/DynamicExpresso.UnitTest/DynamicTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,20 @@ public void Get_Property_of_an_ExpandoObject()
var interpreter = new Interpreter()
.SetVariable("dyn", (object)dyn);

Assert.AreEqual(dyn.Foo, interpreter.Eval("dyn.Foo"));
var l = interpreter.Parse("dyn.Foo");
l.Invoke();
l.Invoke();

//Assert.AreEqual(dyn.Foo, interpreter.Eval("dyn.Foo"));
metoule marked this conversation as resolved.
Show resolved Hide resolved
}

[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]
Expand Down Expand Up @@ -86,6 +99,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 +142,33 @@ 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 };
metoule marked this conversation as resolved.
Show resolved Hide resolved
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"));
}

[Test]
public void Get_value_of_a_nested_array_error()
{
Expand Down