Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using DynamicObject (IDynamicMetaObjectProvider) as a component of a static type leads to infinite loop

Tags:

dynamic

c#-4.0

I'm trying to create a dynamic object that can be used as a component of a static object. Here is a contrived example of what I'm trying to accomplish.

Here is the dynamic component:

public class DynamicComponent : DynamicObject
{
    public override bool TryInvokeMember(
        InvokeMemberBinder binder, 
        object[] args, 
        out object result)
    {
        result = "hello";
        return true;
    }
}

And here is a class where inheriting from DynamicObject isn't an option...assume that there is some third party class that I'm forced to inherit from.

public class AStaticComponent : VendorLibraryClass, IDynamicMetaObjectProvider
{
    IDynamicMetaObjectProvider component = new DynamicComponent();

    public DynamicMetaObject GetMetaObject(Expression parameter)
    {
        var result = component.GetMetaObject(parameter);

        return result;
    }
}

The direct usage of DynamicComponent works:

dynamic dynamicComponent = new DynamicComponent();
Assert.AreEqual(dynamicComponent.AMethod(), "hello");

However, forwarding the GetMetaObject through AStaticComponent causes some form of an infinite loop.

dynamic dynamicComponent = new AStaticComponent();
Assert.AreEqual(dynamicComponent.AMethod(), "hello"); //causes an infinite loop

Anyone know why this occurs?

And if it's some baked in behavior of DynamicObject that I cannot change, could someone provide some help on how to create a IDynamicMetaObjectProvider from scratch to accomplish a component based dynamic object (just something to get things started)?

like image 993
Amir Avatar asked Jul 11 '13 18:07

Amir


2 Answers

I think the problem is that the Expression parameter passed to GetMetaObject represents the target of the dynamic invocation (i.e. the current object). You are passing the outer object to the call on component.GetMetaObject, so the returned meta object is trying to resolve the call to AMethod on the outer object instead of itself, hence the infinite loop.

You can create your own meta object which delegates to the inner component when binding member invocations:

public class AStaticComponent : VendorLibraryClass, IDynamicMetaObjectProvider
{
    IDynamicMetaObjectProvider component = new DynamicComponent();

    public DynamicMetaObject GetMetaObject(Expression parameter)
    {
        return new DelegatingMetaObject(component, parameter, BindingRestrictions.GetTypeRestriction(parameter, this.GetType()), this);
    }

    private class DelegatingMetaObject : DynamicMetaObject
    {
        private readonly IDynamicMetaObjectProvider innerProvider;

        public DelegatingMetaObject(IDynamicMetaObjectProvider innerProvider, Expression expr, BindingRestrictions restrictions)
            : base(expr, restrictions)
        {
            this.innerProvider = innerProvider;
        }

        public DelegatingMetaObject(IDynamicMetaObjectProvider innerProvider, Expression expr, BindingRestrictions restrictions, object value)
            : base(expr, restrictions, value)
        {
            this.innerProvider = innerProvider;
        }

        public override DynamicMetaObject BindInvokeMember(InvokeMemberBinder binder, DynamicMetaObject[] args)
        {
            var innerMetaObject = innerProvider.GetMetaObject(Expression.Constant(innerProvider));
            return innerMetaObject.BindInvokeMember(binder, args);
        }
    }
}
like image 85
Lee Avatar answered Oct 24 '22 06:10

Lee


@Lee's answer is really useful, I wouldn't have known where to get started without it. But from using it in production code, I believe it has a subtle bug.

Dynamic calls are cached at the call site, and Lee's code produces a DynamicMetaObject which effectively states that the inner handling object is a constant. If you have a place in your code where you call a dynamic method on an instance of AStaticObject, and later the same point in the code calls the same method on a different instance of AStaticObject (i.e. because the variable of type AStaticObject now has a different value) then the code will make the wrong call, always calling methods on the handling object from the first instance encountered at that place in the code during that run of the code.

This is a like-for-like replacement, the key difference being the use of Expression.Field to tell the dynamic call caching system that the handling object depends on the parent object:

public class AStaticComponent : VendorLibraryClass, IDynamicMetaObjectProvider
{
    IDynamicMetaObjectProvider component = new DynamicComponent();

    public DynamicMetaObject GetMetaObject(Expression parameter)
    {
        return new DelegatingMetaObject(parameter, this, nameof(component));
    }

    private class DelegatingMetaObject : DynamicMetaObject
    {
        private readonly DynamicMetaObject innerMetaObject;

        public DelegatingMetaObject(Expression expression, object outerObject, string innerFieldName)
            : base(expression, BindingRestrictions.Empty, outerObject)
        {
            FieldInfo innerField = outerObject.GetType().GetField(innerFieldName, BindingFlags.Instance | BindingFlags.NonPublic);
            var innerObject = innerField.GetValue(outerObject);
            var innerDynamicProvider = innerObject as IDynamicMetaObjectProvider;
            innerMetaObject = innerDynamicProvider.GetMetaObject(Expression.Field(Expression.Convert(Expression, LimitType), innerField));
        }

        public override DynamicMetaObject BindInvokeMember(InvokeMemberBinder binder, DynamicMetaObject[] args)
        {
            return binder.FallbackInvokeMember(this, args, innerMetaObject.BindInvokeMember(binder, args));
        }
    }
}
like image 45
MikeBeaton Avatar answered Oct 24 '22 04:10

MikeBeaton