Dynamic Expression not working on dynamic objects

Viewed 2108

I want to dynamically apply a predicates to a list of dynamic object. My solution is working well when I use actual objects but it does not work on dynamic objects and I can't figure out what is the problem.

Note: I searched Stackoverflow none of similar questions are using list of dynamic objects.

I have a list of dynamic objects like the following code. The list contains two dynamic object that have two properties (Name,CreateDate). I used JsonConvert class to create dynamic objects :

var lst = new List<dynamic>();

Dictionary<string, object> dict = new Dictionary<string, object>();
dict.Add("Name", "John");
dict.Add("CreateDate", DateTime.Now);
lst.Add(JsonConvert.DeserializeObject<dynamic>(JsonConvert.SerializeObject(dict)));
dict.Clear();

dict.Add("Name", "sara");
dict.Add("CreateDate", DateTime.Now);
lst.Add(JsonConvert.DeserializeObject<dynamic>(JsonConvert.SerializeObject(dict)));
dict.Clear();

As you see lst is a list of dynamic objects and have 2 items in it. Now I want to filter list to get the item with the name Jonh (p=> p.Name == "john")

To do this I had the following approach:

ParameterExpression pe = Expression.Parameter(typeof(object), "p");
CallSiteBinder name = Binder.GetMember(CSharpBinderFlags.None, "Name", typeof(object),
                new CSharpArgumentInfo[] { CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null) });

    var pname = Expression.Dynamic(name, typeof(object), pe);


    var right = Expression.Constant("John");
    Expression e2 = Expression.Equal(pname, right);
    var qu = Expression.Lambda<Func<dynamic, bool>>(e2, pe);


    var lst2 = lst.AsQueryable().Where(qu).ToList();// Count()==0 !

The lst2 should contain 1 item but it contains 0 items. But if I change the original list(lst) to a type that has a Name property (let's say List<Person>) it lst2 correctly have 1 item.

UPDATE: Even when I use ExpandoObject to create dynamic objects it still won't work :

        dynamic obj = new ExpandoObject();
        var dictionary = (IDictionary<string, object>)obj;
        dictionary.Add("Name", "John");
        dictionary.Add("CreateDate", DateTime.Now);

UPDATE 2: As pionted out in the comments ExpandoObject actually works and the problem is with SqlDataReader. Here are what I have tried (see Not working comments in the following code) :

            ...
List<dynamic> result = new List<dynamic>();
While(dr.Read()){
            dynamic obj = new ExpandoObject();
            var dictionary = (IDictionary<string, object>)obj;
            dictionary.Add("Name","John"); // <= this works fine
            // dictionary.Add("Name",dr["Name"]); // <= Not working
            // dictionary.Add("Name",dr["Name"].ToItsType()); // <= Not working
            // dictionary.Add("Name",dr["Name"].ToString()); // <= Not working
            dictionary.Add("CreateDate", DateTime.Now);
            result.Add(obj);
            }
            ...
3 Answers

I was able to reproduce the issue (after your UPDATE 2 which gave me the idea) by changing the ExpandoObject example code

dictionary.Add("Name", "John");

to

dictionary.Add("Name", new string("John".ToCharArray()));

to avoid constant string interning, which lead us to the issue in the dynamic expression code.

The dynamic expression type is object, hence Expression.Equal resolves to object operator ==, i.e. ReferenceEquals. That's why the example is working with constant strings and not with runtime created strings.

What you need here is to use actual property type. So simply cast (Expression.Convert) the result of the dynamic property accessor to the expected type:

var pname = Expression.Convert(Expression.Dynamic(name, typeof(object), pe), typeof(string));

Now the expressions which refer to pname expression will resolve with the correct type (in this particular case, Equal will resolve to the overloaded string == operator which correctly compares strings by value. Same for value types like int, DateTime etc.).

    dynamic obj = new ExpandoObject();        
    dictionary.Add("Name", "John");
    dictionary.Add("CreateDate", DateTime.Now);

try the above code. Conversion is not required and ExpandoObject should allow to add or remove dynamic objects.

Why not just use dynamic objects instead of dictionary. Following code works like charm:

var lst = new List<dynamic>();

dynamic obj = new ExpandoObject();
obj.Name = "John";
obj.CreateDate = DateTime.Now;
lst.Add(obj);

obj = new ExpandoObject(); // re-instantiate the obj if you want to differentiate from the List itself
obj.Name = "Sara";
obj.CreateDate = DateTime.Now.AddMonths(-10);
lst.Add(obj);

foreach (var item in lst)
{
    Console.WriteLine($"{item.Name} - {item.CreateDate}");
}

You can even filter the list dynamically

Console.WriteLine(lst.Find(i=>i.Name == "John").Name);

Hope it helps.

EDIT

You need to re-instantiate your dynamic obj on each adding. If you dont, your list will have nothing but 2 "Sara"s.

Update

Well, with a little bit work on this this solution got worked for me.

I used JsonConvert.DeserializeObject<ExpandoObject>(...) instead of dynamic. Then wrote a LookUp method for inspecting the element. I think first problem with your code is deserializing your serialized object as dynamic instead of ExpandoObject. After that correction, it was not that hard for the casting dictinaries and getting key-value oriented values.

Here is my code:

var lst = new List<dynamic>();

Dictionary<string, object> dict = new Dictionary<string, object>();
dict.Add("Name", "John");
dict.Add("CreateDate", DateTime.Now);
lst.Add(JsonConvert.DeserializeObject<ExpandoObject>(JsonConvert.SerializeObject(dict)));
dict.Clear();

dict.Add("Name", "Sara");
dict.Add("CreateDate", DateTime.Now);
lst.Add(JsonConvert.DeserializeObject<ExpandoObject>(JsonConvert.SerializeObject(dict)));
dict.Clear();

var res = LookUp(lst, "Name", "Sara");

And after that LookUp method

public static object LookUp(List<dynamic> lst, string propName, object value)
{
    return lst.FindAll(i =>
    {
        var dic = i as IDictionary<string, object>;
        return dic.Keys.Any(key => dic[key].ToString().Contains(value.ToString()));
    });
}

Also if you dont want to cast it to dictionary here is an alternative method for it:

private static object GetProperty(dynamic target, string name)
{
    var site =
        CallSite<Func<CallSite, dynamic, object>>  
          .Create(Microsoft.CSharp.RuntimeBinder.Binder.GetMember(CSharpBinderFlags.None, name, target.GetType(),
                new[] {CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null)}));
    return site.Target(site, target);
}

public static object LookUpAlt(List<dynamic> lst, string propName, object value)
{
    return lst.FindAll(i => GetProperty(i, propName).Equals(value));
}
Related