Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

IDynamicMetaObjectProvider.GetMetaObject is not always called

Tags:

c#

dynamic

I am struggling with a very strange problem around a class that implements IDynamicMetaObjectProvider interface. According to the documentation, each time there is an attempt to do a dynamic binding on an instance of such class, GetMetaObject is called to resolve the dynamically bound value.

But what I am experiencing is a kind of mystery. Just look at this code:

public class DataEntry : Dictionary<string, object>
{
    public DataEntry(IDictionary<string, object> entry)
        : base(entry)
    {
    }
}

public class DynamicDataEntry : DataEntry, IDynamicMetaObjectProvider
{
    internal DynamicDataEntry()
        : base(new Dictionary<string, object>())
    {
    }

    public DynamicDataEntry(IDictionary<string, object> entry)
        : base(entry)
    {
    }

    public DynamicMetaObject GetMetaObject(Expression parameter)
    {
        return new DynamicEntryMetaObject(parameter, this);
    }

    private class DynamicEntryMetaObject : DynamicMetaObject
    {
        internal DynamicEntryMetaObject(
            Expression parameter,
            DynamicDataEntry value)
            : base(parameter, BindingRestrictions.Empty, value)
        {
        }

        public override DynamicMetaObject BindGetMember(GetMemberBinder binder)
        {
            var methodInfo = this.GetType().GetMethod("GetEntryValue", BindingFlags.Instance | BindingFlags.NonPublic);
            var arguments = new Expression[]
            {
                Expression.Convert(Expression.Constant(base.Value), typeof (DynamicDataEntry)),
                Expression.Constant(binder.Name)
            };
            Expression objectExpression = Expression.Call(Expression.Constant(this), methodInfo, arguments);

            return new DynamicMetaObject(
                objectExpression,
                BindingRestrictions.GetTypeRestriction(Expression, this.RuntimeType));
        }

        private object GetEntryValue(DynamicDataEntry entry, string propertyName)
        {
            return entry[propertyName];
        }
    }
}

// And here is the test:

    [Test]
    public void Test()
    {
        var dict = new[]
        {
            new Dictionary<string, object>() {{"StringProperty", "a"}, {"IntProperty", 1}},
            new Dictionary<string, object>() {{"StringProperty", "b"}, {"IntProperty", 2}},
        };

        var values = (dict.Select(x => new DynamicDataEntry(x)) as IEnumerable<dynamic>).ToArray();
        for (int index = 0; index < values.Count(); index++)
        {
            // GetMetaObject is called only first time for the line below, so it is "a" for both iterations! WHY?!!
            var s = values[index].StringProperty;

            switch (index)
            {
                case 0:
                    Assert.AreEqual("a", values[index].StringProperty);
                    Assert.AreEqual("a", s);
                    break;
                case 1:
                    Assert.AreEqual("b", values[index].StringProperty);
                    Assert.AreEqual("b", s);
                    break;
            }
        }
    }

When I debug the code, I can see that GetMetaObject on StringProperty for the first line in the loop is always called for the first iteration, but on the next iteration GetMetaObject is not called - instead DLR executes an expression for the values[index] from the previous iteration, thus evaluating StringProperty as "a". But Assert.AreEqual call triggers GetMetaObject execution and StringProperty is correctly evaluated as "b".

This behavior is driving me nuts, I can't understand what may cause it. Does anyone have any ideas?

UPDATE I received a suggestion to derive my class from DynamicObject instead of IDynamicMetaObjectProvider. To make the long story short: I am aware of DynamicObject but it's not suitable in my case. I only posted a simple example to illustrate what's going on. The real implementation needs to be derived from another class than DataEntry, and such derivation is essential, so I have to implement IDynamicMetaObjectProvider even though it's more work.

like image 467
Vagif Abilov Avatar asked Dec 23 '13 14:12

Vagif Abilov


1 Answers

I've found a workaround for the problem by rewriting an expression used in BindGetMember. Below is the code that works.

The important different is that the previous code used Expression.Constant(this) to refer to an instance of the class derived from DynamicMetaObject. I've found samples that used rather cryptic expression Expression.Convert(Expression, LimitType) instead. The updated version works fine with all tests.

I must say that the stuff around IDynamicMetaObjectProvider is poorly (or non-) documented, and I still have no explanation why my original code didn't work in combination with traversing IEnumerable. Thanks to some blog posts, I managed to rewrite it in a way functioning in all my scenarios.

public class DataEntry : Dictionary<string, object>
{
    public DataEntry(IDictionary<string, object> entry)
        : base(entry)
    {
    }

    private object GetEntryValue(string propertyName)
    {
        return base[propertyName];
    }
}

public class DynamicDataEntry : DataEntry, IDynamicMetaObjectProvider
{
    internal DynamicDataEntry()
        : base(new Dictionary<string, object>())
    {
    }

    public DynamicDataEntry(IDictionary<string, object> entry)
        : base(entry)
    {
    }

    public DynamicMetaObject GetMetaObject(Expression parameter)
    {
        return new DynamicEntryMetaObject(parameter, this);
    }

    private class DynamicEntryMetaObject : DynamicMetaObject
    {
        internal DynamicEntryMetaObject(
            Expression parameter,
            DynamicDataEntry value)
            : base(parameter, BindingRestrictions.Empty, value)
        {
        }

        public override DynamicMetaObject BindGetMember(GetMemberBinder binder)
        {
            var methodInfo = typeof(DataEntry).GetMethod("GetEntryValue", BindingFlags.Instance | BindingFlags.NonPublic);
            var arguments = new Expression[]
        {
            Expression.Constant(binder.Name)
        };
            Expression objectExpression = Expression.Call(
                Expression.Convert(Expression, LimitType), 
                methodInfo, arguments);

            return new DynamicMetaObject(
                objectExpression,
                BindingRestrictions.GetTypeRestriction(Expression, this.RuntimeType));
        }
    }
}
like image 51
Vagif Abilov Avatar answered Oct 13 '22 11:10

Vagif Abilov