Programming with Dynamic Objects

The introduction of dynamic objects8 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 objects 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.29 (with Output 18.10) provides an example.

Listing 18.29: Dynamic Programming Using Reflection
// ...
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.29), 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.29 and the accompanying text reveal several characteristics of the dynamic data type:

dynamic is a directive to the compiler to generate code.
dynamic involves an interception mechanism so that when a dynamic call is encountered by the runtime, it can compile the request to CIL and then invoke the newly compiled call. (See “Advanced Topic: dynamic Uncovered” later in this chapter for more details.)
The principle at work when a type is assigned to dynamic is to conceptually “wrap” the original type so that no compile-time validation occurs. Additionally, when a member is invoked at runtime, the wrapper intercepts the call and dispatches it appropriately (or rejects it). Calling GetType() on the dynamic object reveals the type underlying the dynamic instance—it does not return dynamic as a type.
Any type9 will convert to dynamic.
In Listing 18.29, we successfully cast both a value type (double) and a reference type (string) to dynamic. In fact, all types can successfully be converted into a dynamic object. There is an implicit conversion from any reference type to dynamic, an implicit conversion (a boxing conversion) from a value type to dynamic, and an implicit conversion from dynamic to dynamic. This is perhaps obvious, but with dynamic, this process is more complicated than simply copying the “pointer” (address) from one location to the next.
Successful conversion from dynamic to an alternative type depends on support in the underlying type.
Conversion from a dynamic object to a standard CLR type is an explicit cast (e.g., (double)data.Length). Not surprisingly, if the target type is a value type, an unboxing conversion is required. If the underlying type supports the conversion to the target type, the conversion from dynamic will also succeed.
The type underlying the dynamic type can change from one assignment to the next.
Unlike an implicitly typed variable (var), which cannot be reassigned to a different type, dynamic involves an interception mechanism for compilation before the underlying type’s code is executed. Therefore, it is possible to successfully swap out the underlying type instance to an entirely different type. This will result in another interception call site that will need to be compiled before invocation.
Verification that the specified signature exists on the underlying type doesn’t occur until runtime—but it does occur.
The compiler makes almost no verification of operations on a dynamic type, as the method call to data.NonExistentMethodCallStillCompiles() demonstrates. This step is left entirely to the work of the runtime when the code executes. Moreover, if the code never executes, even though surrounding code does (as with data.NonExistentMethodCallStillCompiles()), no verification and binding to the member will ever occur.
The result of any dynamic member invocation is of compile-time type dynamic.
A call to any member on a dynamic object will return a dynamic object. Therefore, calls such as data.ToString() will return a dynamic object rather than the underlying string type. However, at execution time, when GetType() is called on the dynamic object, an object representing the runtime type is returned.
If the member specified does not exist at runtime, the runtime will throw a Microsoft.CSharp.RuntimeBinder.RuntimeBinderException exception.
If an attempt to invoke a member at execution time does occur, the runtime will verify that the member call is truly valid (e.g., that the signatures are type-compatible in the case of reflection). If the method signatures are not compatible, the runtime will throw a Microsoft.CSharp.RuntimeBinder.RuntimeBinderException.
dynamic with reflection does not support extension methods.
As is the case for reflection using System.Type, reflection using dynamic does not support extension methods. Invocation of extension methods is still available on the implementing type (e.g., System.Linq.Enumerable), just not on the dynamic type directly.
At its core, dynamic is a System.Object.
Given that any object can be successfully converted to dynamic, and that dynamic may be explicitly converted to a different object type, dynamic behaves like System.Object. Like System.Object, it even returns null for its default value (default(dynamic)), indicating it is a reference type. The special dynamic behavior of dynamic that distinguishes it from a System.Object appears only at compile time.
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 method takes 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.30, using dynamic invocation we could call person.FirstName and person.LastName.

Listing 18.30: Runtime Binding to XML Elements without dynamic
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.30 is not overly complex, compare it to Listing 18.31—an alternative approach that uses a dynamically typed object.

Listing 18.31: Runtime Binding to XML Elements with dynamic
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.31, we have the same functionality as in Listing 18.30, albeit with one very important difference: Listing 18.30 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.31 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.31. 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.31 is no better than Listing 18.30 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.31 (person.FirstName) with the corresponding call in Listing 18.30. 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.31 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.32 shows the full implementation.

Listing 18.32: 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.33 produces a list of all overridable members.

Listing 18.33: 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.33 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().

________________________________________

8. Introduced 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 }}
;