Programming with Dynamic Objects

The introduction of dynamic objects7 simplified a host of programming scenarios and enabled several new ones previously not available. At its core, programming with dynamic objects enables developers to code operations using a dynamic dispatch mechanism that the runtime will resolve at execution time, rather than having the compiler verify and bind to it at compile time.

Why? Many times, objects are inherently not statically typed. Examples include loading data from an XML/CSV file, a database table, the Internet Explorer DOM, or COM’s IDispatch interface, or calling code in a dynamic language such as an IronPython object. Dynamic object support provides a common solution for talking to runtime environments that don’t necessarily have a compile-time–defined structure. In the initial implementation of dynamic objects, four binding methods are available:

1.
Using reflection against an underlying CLR type
2.
Invoking a custom IDynamicMetaObjectProvider that makes available a DynamicMetaObject
3.
Calling through the IUnknown and IDispatch interfaces of COM
4.
Calling a type defined by dynamic languages such as IronPython

Of these four approaches, we will delve into the first two. The principles underlying them translate seamlessly to the remaining cases—COM interoperability and dynamic language interoperability.

Invoking Reflection Using dynamic

One of the key features of reflection is the ability to dynamically find and invoke a member on a type based on an execution-time identification of the member name or some other quality, such as an attribute (see Listing 18.3). However, dynamic objects8 provide a simpler way of invoking a member by reflection, assuming compile-time knowledge of the member signature. To reiterate, this restriction states that at compile time we need to know the member name along with the signature (the number of parameters and whether the specified parameters will be type-compatible with the signature). Listing 18.26 (with Output 18.10) provides an example.

Listing 18.26: Dynamic Programming Using Reflection
using System;
 
// ...
        dynamic data =
          "Hello!  My name is Inigo Montoya";
        Console.WriteLine(data);
        data = (double)data.Length;
        data = data * 3.5 + 28.6;
        if(data == 2.4 + 112 + 26.2)
            // The distance (in miles) for the swim, bike, and
            // run portions of an Ironman triathlon, respectively
        {
            Console.WriteLine(
                $"{data} makes for a long triathlon.");
        }
        else
        {
            data.NonExistentMethodCallStillCompiles();
        }
        // ...
Output 18.10
Hello!  My name is Inigo Montoya
140.6 makes for a long triathlon.

In this example, there is no explicit code for determining the object type, finding a particular MemberInfo instance, and then invoking it. Instead, data is declared as type dynamic and methods are called against it directly. At compile time, there is no check of whether the members specified are available or even which type underlies the dynamic object. Hence, it is possible at compile time to make any call, so long as the syntax is valid. At compile time, it is irrelevant whether there is really a corresponding member.

However, type safety is not abandoned altogether. For standard CLR types (such as those used in Listing 18.26), the same type checker normally used at compile time for non-dynamic types is instead invoked at execution time for the dynamic type. Therefore, at execution time, if no such member is available, the call will result in a Microsoft.CSharp.RuntimeBinder.RuntimeBinderException.

Note that this capability is not nearly as flexible as the reflection described earlier in the chapter, although the API is undoubtedly simpler. The key difference when using a dynamic object is that it is necessary to identify the signature at compile time rather than determine things such as the member name at runtime (as we did when parsing the command-line arguments).

dynamic Principles and Behaviors

Listing 18.26 and the accompanying text reveal several characteristics of the dynamic data type:

AdVanced Topic
dynamic Uncovered

The CIL disassembler reveals that within the CIL, the dynamic type is actually a System.Object. In fact, without any invocations, declaration of the dynamic type is indistinguishable from System.Object. However, the difference becomes apparent when invoking a member.

To invoke the member, the compiler declares a variable of type System.Runtime.CompilerServices.CallSite<T>. T varies on the basis of the member signature, but something simple, such as the invocation of ToString(), would require instantiation of the type CallSite<Func<CallSite, object, string>>, along with a method call with parameters of CallSite site, object dynamicTarget, and string result. site is the call site itself, dynamicTarget is the object on which the method call is invoked, and result is the underlying return value from the ToString() method call. Rather than instantiate CallSite<Func<CallSite _site, object dynamicTarget, string result>> directly, a Create() factory method is available for instantiating it; this methodtakes a parameter of type Microsoft.CSharp.RuntimeBinder.CSharpConvertBinder. Given an instance of the CallSite<T>, the final step involves a call to CallSite<T>.Target() to invoke the actual member.

Under the covers at execution time, the framework uses reflection to look up members and to verify that the signatures match. Next, the runtime builds an expression tree that represents the dynamic expression as defined by the call site. Once the expression tree is compiled, we have a CIL method body that is similar to what the compiler would have generated had the call not been dynamic. This CIL code is then cached in the call site, and the invocation occurs via a delegate. As the CIL is now cached at the call site, the next invocation doesn’t require all the reflection and compilation overhead again.

Why Dynamic Binding?

In addition to reflection, we can define custom types that we invoke dynamically. We might consider using dynamic invocation to retrieve the values of an XML element, for example. Rather than using the strongly typed syntax of Listing 18.27, using dynamic invocation we could call person.FirstName and person.LastName.

Listing 18.27: Runtime Binding to XML Elements without dynamic
using System;
using System.Linq;
using System.Xml.Linq;
 
// ...
        XElement person = XElement.Parse(
            @"<Person>
                    <FirstName>Inigo</FirstName>
                    <LastName>Montoya</LastName>
                  </Person>");
 
        Console.WriteLine($"{ person.Descendants("FirstName").First().Value }" +
        $"{ person.Descendants("LastName").First().Value }");
        //...

Although the code in Listing 18.27 is not overly complex, compare it to Listing 18.28—an alternative approach that uses a dynamically typed object.

Listing 18.28: Runtime Binding to XML Elements with dynamic
using System;
// ...
        dynamic person = DynamicXml.Parse(
         @"<Person>
                <FirstName>Inigo</FirstName>
                <LastName>Montoya</LastName>
               </Person>");
 
        Console.WriteLine(
            $"{ person.FirstName } { person.LastName }");
        //...

The advantages are clear, but does that mean dynamic programming is preferable to static compilation?

Static Compilation versus Dynamic Programming

In Listing 18.28, we have the same functionality as in Listing 18.27, albeit with one very important difference: Listing 18.27 is entirely statically typed. Thus, at compile time, all types and their member signatures are verified with this approach. Method names are required to match, and all parameters are checked for type compatibility. This is a key feature of C#—and something we have highlighted throughout the book.

In contrast, Listing 18.28 has virtually no statically typed code; the variable person is instead dynamic. As a result, there is no compile-time verification that person has a FirstName or LastName property—or any other members, for that matter. Furthermore, when coding within an IDE, no IntelliSense is available to identify the members on person.

The loss of typing would seem to result in a significant decrease in functionality. Why, then, is such a possibility even available in C#10?

To understand this apparent paradox, let’s reexamine Listing 18.28. Notice the call to retrieve the "FirstName" element:

Element.Descendants("LastName").FirstOrDefault().Value

The listing uses a string ("LastName") to identify the element name, but no compile-time verification occurs to ensure that the string is correct. If the casing was inconsistent with the element name or if there was a space, the compile would still succeed, even though a NullReferenceException would occur with the call to the Value property. Furthermore, the compiler does not attempt to verify that the "FirstName" element even exists; if it doesn’t, we would also get the NullReferenceException message. In other words, in spite of all the type-safety advantages, type safety doesn’t offer many benefits when you’re accessing the dynamic data stored within the XML element.

Listing 18.28 is no better than Listing 18.27 when it comes to compile-time verification of the element retrieval. If a case mismatch occurs or if the FirstName element doesn’t exist, an exception would still be thrown.11 However, compare the call to access the first name in Listing 18.28 (person.FirstName) with the corresponding call in Listing 18.27. The call in the latter listing is undoubtedly significantly simpler.

In summary, in some situations type safety doesn’t—and likely can’t—make certain checks. In those cases, code that makes a dynamic call that is verified only at runtime, rather than also being verified at compile time, is significantly more readable and succinct. Obviously, if compile-time verification is possible, statically typed programming is preferred because readable and succinct APIs can accompany it. However, in the cases where it isn’t effective, dynamic capabilities12 enable programmers to write simpler code rather than emphasizing the purity of type safety.

Implementing a Custom Dynamic Object

Listing 18.28 included a method call to DynamicXml.Parse(...) that was essentially a factory method call for DynamicXml—a custom type rather than one built into the CLR framework. However, DynamicXml doesn’t implement a FirstName or LastName property. To do so would break the dynamic support for retrieving data from the XML file at execution time rather than fostering compile-time–based implementation of the XML elements. In other words, DynamicXml does not use reflection for accessing its members but rather dynamically binds to the values based on the XML content.

The key to defining a custom dynamic type is implementation of the System.Dynamic.IDynamicMetaObjectProvider interface. Rather than implementing the interface from scratch, however, the preferred approach is to derive the custom dynamic type from System.Dynamic.DynamicObject. This provides default implementations for a host of members and allows you to override the ones that don’t fit. Listing 18.29 shows the full implementation.

Listing 18.29: Implementing a Custom Dynamic Object
using System.Dynamic;
using System.Linq;
using System.Xml.Linq;
 
public class DynamicXml : DynamicObject
{
    private XElement Element { getset; }
 
    public DynamicXml(System.Xml.Linq.XElement element)
    {
        Element = element;
    }
 
    public static DynamicXml Parse(string text)
    {
        return new DynamicXml(XElement.Parse(text));
    }
 
    public override bool TryGetMember(
        GetMemberBinder binder, out object? result)
    {
        bool success = false;
        result = null;
        XElement? firstDescendant =
            Element.Descendants(binder.Name).FirstOrDefault();
        if(firstDescendant != null)
        {
            if(firstDescendant.Descendants().Any())
            {
                result = new DynamicXml(firstDescendant);
            }
            else
            {
                result = firstDescendant.Value;
            }
            success = true;
        }
        return success;
    }
 
    public override bool TrySetMember(
        SetMemberBinder binder, objectvalue)
    {
        bool success = false;
        XElement? firstDescendant =
            Element.Descendants(binder.Name).FirstOrDefault();
        if(firstDescendant != null)
        {
            if(value?.GetType() == typeof(XElement))
            {
                firstDescendant.ReplaceWith(value);
            }
            else
            {
                firstDescendant.Value = value?.ToString() ?? string.Empty;
            }
            success = true;
        }
        return success;
    }
}

The key dynamic implementation methods for this use case are TryGetMember() and TrySetMember() (assuming you want to assign the elements as well). Only these two method implementations are necessary to support the invocation of the dynamic getter and setter properties. Furthermore, the implementations are straightforward. First, they examine the contained XElement, looking for an element with the same name as the binder.Name—the name of the member invoked. If a corresponding XML element exists, the value is retrieved (or set). The return value is set to true if the element exists and false if it doesn’t. A return value of false will immediately cause the runtime to throw a Microsoft.CSharp.RuntimeBinder.RuntimeBinderException at the call site of the dynamic member invocation.

System.Dynamic.DynamicObject supports additional virtual methods if more dynamic invocations are required. Listing 18.30 produces a list of all overridable members.

Listing 18.30: Overridable Members on System.Dynamic.DynamicObject
using System.Collections.Generic;
using System.Dynamic;
   
public class DynamicObject : IDynamicMetaObjectProvider
{
    protected DynamicObject();
 
    public virtual IEnumerable<string> GetDynamicMemberNames();
    public virtual DynamicMetaObject GetMetaObject(
        Expression parameter);
    public virtual bool TryBinaryOperation(
        BinaryOperationBinder binder, object arg,
            out object result);
    public virtual bool TryConvert(
        ConvertBinder binder, out object result);
    public virtual bool TryCreateInstance(
        CreateInstanceBinder binder, object[] args,
              out object result);
    public virtual bool TryDeleteIndex(
        DeleteIndexBinder binder, object[] indexes);
    public virtual bool TryDeleteMember(
        DeleteMemberBinder binder);
    public virtual bool TryGetIndex(
        GetIndexBinder binder, object[] indexes,
              out object result);
    public virtual bool TryGetMember(
        GetMemberBinder binder, out object result);
    public virtual bool TryInvoke(
        InvokeBinder binder, object[] argsout object result);
    public virtual bool TryInvokeMember(
        InvokeMemberBinder binder, object[] args,
              out object result);
    public virtual bool TrySetIndex(
        SetIndexBinder binder, object[] indexes, object value);
    public virtual bool TrySetMember(
        SetMemberBinder binder, object value);
    public virtual bool TryUnaryOperation(
        UnaryOperationBinder binder, out object result);
}

As Listing 18.30 shows, there are member implementations for everything—from casts and various operations, to index invocations. In addition, there is a method for retrieving all the possible member names: GetDynamicMemberNames().

________________________________________

7. Introduced in C# 4.0.
8. Added in C# 4.0.
9. Technically, it is restricted to any type that converts to an object—which excludes unsafe pointers, lambdas, and method groups.
10. Functionality added in C# 4.0.
11. You cannot use a space in the FirstName property call—but XML doesn’t support spaces in element names, so let’s just ignore this fact.
12. Starting in C# 4.0.
{{ snackbarMessage }}