Partager via


On Dynamic Objects and DynamicObject

As you know if you read my blog, C# 4.0 introduced some language features that help you consume dynamic objects. Although it’s not part of the language, most of the writing about dynamic in C# that I have seen, including my own, also contains some point about how you create dynamic objects. And they usually make it clear that the easiest way to do that is to extend System.Dynamic.DynamicObject. For instance, you might see an example such as the following, which is pretty straight-forward:

 using System;
using System.Dynamic;

class MyDynamicObject : DynamicObject
{
    public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
    {
        Console.WriteLine("You called method {0}!", binder.Name);
        Console.WriteLine("... and passed {0} args.", args.Length);
        result = null;
        return true;
    }
}

class Program
{
    static void Main(string[] args)
    {
        dynamic d = new MyDynamicObject();
        d.Foo(d, 1, "bar");
    }
}

... that prints:

 You called method Foo!
... and passed 3 args.

That’s all well and good. But I want to tell you that although this works and is easy, DynamicObject is not a type that is specially known to the DLR, and that this is not the primary mechanism to define dynamic objects. The primary mechanism is to implement the interface IDynamicMetaObjectProvider, and to provide your own specialization of DynamicMetaObject. These two types are specially known to the DLR and they are the mechanism by which dynamic (including DynamicObject) really works. DynamicObject is a tool of convenience. You could write it yourself, if you wanted to. The design of DynamicObject deliberately introduces some trade-offs in the name of ease-of-use, and if your code or library is not OK with those trade-offs, then you should consider your alternatives.

Binding vs. executing

One thing that DynamicObject does to your dynamic objects is that it confuses two separate “phases” of the execution of a dynamic operation. There is a lot of machinery in the DLR that is designed to make your dynamic operations fast, and there are concepts called “call sites” and “rules” and “binders,” etc., and you can dig into this yourself if it’s important, but the upshot is: when a dynamic operation is bound at runtime, the binding is cached using something called a “rule” that indicates the circumstances under which to execute some code, as well as the code to execute. Subsequent to the binding, the code the binding produced is simply run when needed.

If you implement your dynamic object using DynamicObject, these things still happen. However, they ignore the code that you put in your DynamicObject and execute it every time through a true dynamic operation. Let me give you an example to make this a little clearer.

Suppose you wanted to write a dynamic type that queried some external data somewhere. With DynamicObject, you might do something like the following:

 class MyDynamicData : DynamicObject
{
    public override bool TryGetMember(GetMemberBinder binder, out object result)
    {
        string propName = binder.Name;

        if (this.externalData.HasColumn(propName))
        {
            result = this.externalData.GetData(this.currentRow, propName);
            return true;
        }
        else
        {
            return base.TryGetMember(binder, out result);
        }
    }
    // ...
}

In this code, we're trying to get a property value. So we go to the external data to see if the property name makes sense, and then we retrieve the relevant data to return to the user. Remember I said that dynamic bindings use “rules?” If the user types “d.FooBaz”, where d holds one of these MyDynamicData objects, then “rule” that is generated is as follows. First, the test part is this:

 ($arg0 TypeEqual MyDynamicData)

And the execution part is this:

 .Block(System.Object $var1) {
    .If (
        .Call ((System.Dynamic.DynamicObject)$$arg0).TryGetMember(
            .Constant<System.Dynamic.GetMemberBinder>(Microsoft.CSharp.RuntimeBinder.CSharpGetMemberBinder),
            $var1)
    ) {
        $var1
    } .Else {
        .Throw .New Microsoft.CSharp.RuntimeBinder.RuntimeBinderException("'MyDynamicData' does not contain a definition for 'FooBaz'")
    }
}

If that syntax doesn’t make much sense to you, let me explain. The DLR encodes these things using LINQ expression trees, and the strings above are simply a debug view of those things. They’re not complicated. Basically, the rule says that if the actual runtime type of the object is a MyDynamicData, then what the DLR should execute is the code below, which calls your TryGetMember, and if that returns true then return your result. Otherwise, it should throw an exception that says there is no property FooBaz. That exception, incidentally, came from the C# binder. The whole dance that goes on to determine what the rule is is a little bit complicated, but the important part is that the TryGetMember you defined in your MyDynamicData is not a part of it. Instead, every single time that user’s line of code that contains “d.FooBaz” runs, your TryGetMember is run. Every time.

You could imagine an alternative, where instead, of TryGetMember running every time, you write some code that looks at the name of the property and makes a determination about what it should do, and then the rule contains code that simply does that thing. In our case, the rule would just have to run the “this.externalData.GetData(this.currentRow, propName)” part. Or just throw if the property name was bogus. Either way, you’re doing less every time through. Of course, this alternative exists, and it requires you not to use DynamicObject. If you implemented your own IDynamicMetaObjectProvider, you could slim up your rules as much as possible by doing your analysis once, not on every execution.

Object is king--except DynamicObject

There is another snag in DynamicObject that is in place in the interest of speed and ease-of-use, and it is the following: when the call site’s underlying language provides some binding for any operation, that binding overrules any dynamic bindings that might exist. This is in contrast to the ordinary behavior in the DLR, which is that the object gets to pick what it wants to do first. That strategy is sometimes summarized by the DLR’s designers as “object is king.” And, well, DynamicObject in some sense adheres to this principle, except that what it decides to do is defer to the language whenever it can.

This has some interesting consequences in C#. Check out the following program.

 using System;
using System.Collections;
using System.Dynamic;

class MyDynamicObject : DynamicObject
{
    public override bool TryBinaryOperation(BinaryOperationBinder binder, object arg, out object result)
    {
        Console.WriteLine("TryBinaryOperation for {0}", binder.Operation);
        result = "dynamically returned result";
        return true;
    }

    public override bool TryConvert(ConvertBinder binder, out object result)
    {
        Console.WriteLine("TryConvert for {0}", binder.Type.Name);
        result = new string[] { "dynamically", "returned", "result" };
        return true;
    }
}

class QuestionableProgram
{
    static void Main(string[] args)
    {
        dynamic d = new MyDynamicObject();
        Console.WriteLine(d == null);
        Console.WriteLine((IEnumerable)d);
    }
}

What does this do? You might like to think that it prints the strings in the DynamicObject method overrides. But it does not. It prints “False” and then throws. Why? Because when the DynamicObject was asked to bind itself, it first asked the C# binder to bind the operations. In the first case, “d == null”, the C# binder said “yes! I can bind this! here you go!” and returned a normal object identity comparison. In the second case, the C# binder said “yes! I can bind this! here you go!” and returned a normal interface reference coercion. For that last one, recall that there is always an explicit conversion from most class types to any interface type in C# (6.2.4, bullet 3), though they can fail. And fail this one does, since MyDynamicObject does not statically implement IEnumerable.

Just to expand on the interface conversion example a bit, it’s especially weird since if the conversion were implicit (say, try assigning to a local), then the dynamic conversion would have worked. Why? Because the C# binder would have said, “nope! no implicit conversion to IEnumerable,” and then the DynamicObject implementation would have let TryConvert do its thing.

And these are just some of the examples. This behavior means that you cannot dynamically do lots of things, include define a ToString or Equals method, or even hide such members as TryInvokeMember. In other words, if I have a DynamicObject in hand and concealed behind the dynamic type, all of the overrideable members are always directly (though dynamically) invocable. Ugh.

Again, if this behavior does not suit you, then you need to roll your own.

But there’s a reason

I don’t mean to disparage DynamicObject. It really is a convenient and easy-to-use type, and if you’re not exposing dynamic objects in a library then it may be perfectly suitable to use.

The design decisions that led us here are deliberate. First, for the intermingling of binding and execution code, well, that whole protocol is very confusing. DynamicObject presents a much more easy to understand model. It doesn’t even require you to understand LINQ expression trees. But it gives up performance. Then, binding statically when possible gets a lot of that performance back. To give an example, although you cannot directly dynamically expose all the things I mentioned in the last bit (==, interface conversions, etc), you can just as easily introduce them statically and they’ll get called with no further help from you, and more quickly too.

ExpandoObject, by the way, is the other convenience type for users of static languages such as C# to create dynamic objects. You can read about both of these types, if you are interested, in the DLR’s excellent documentation at https://dlr.codeplex.com/.

Just remember that when someone says that “DynamicObject is the way to generate dynamic types in C#,” what they should really have said is that DynamicObject is a way to generate dynamic types in C#. Other ways include directly with IDynamicMetaObjectProvider, or by stepping outside of C# and defining some types in Python or Ruby or any other DLR language. I intended to present some of these alternatives in the future.