Understanding dynamic and DynamicObject in C

In C#, the dynamic keyword bypasses compile-time type checking, deferring member resolution to runtime via the Dynamic Language Runtime (DLR). When combined with the DynamicObject class, developers can intercept runtime operations such as property access, method calls, and indexers to define custom dynamic behaviors. This article explains how the dynamic static type works under the hood, how the DLR resolves calls at runtime, and how to create custom dynamic entities using DynamicObject.

The dynamic Keyword

Introduced in C# 4.0, the dynamic keyword informs the compiler to ignore static type safety for the variable it annotates. Instead of resolving members at compile time, the compiler generates instructions that delegate member resolution to the Dynamic Language Runtime.

Under the hood, the compiler converts dynamic variables to the System.Object type, annotating them with attributes like DynamicAttribute. While code using dynamic feels dynamically typed, C# remains a statically typed language. The static type of the variable is simply treated as dynamic, preventing compile-time errors when accessing properties or methods that the compiler cannot verify.

dynamic value = "Hello, World!";
Console.WriteLine(value.Length); // Resolved at runtime

value = 10;
Console.WriteLine(value + 5);     // Resolved at runtime

If an invoked member does not exist at runtime, the DLR throws a RuntimeBinderException.

How the Dynamic Language Runtime (DLR) Works

The Dynamic Language Runtime sits on top of the Common Language Runtime (CLR) and provides the infrastructure for dynamic programming in .NET. It accomplishes this through three primary components:

  1. Expression Trees: Represent runtime operations as code data structures.
  2. Call Sites: The actual locations in code where dynamic operations occur. Call sites maintain a polymorphic cache of previous invocation targets to ensure dynamic calls perform similarly to static calls after the initial dispatch.
  3. Binders: Language-specific components that translate dynamic calls into concrete operations. The C# binder enforces standard C# rules (such as overload resolution and accessibility) at runtime.

When a dynamic call is invoked, the call site checks its cache. If the runtime types match a previous invocation, the cached delegate executes directly. If not, the binder determines the appropriate member on the object, creates a new execution rule, updates the cache, and executes the operation.

Custom Late Binding with DynamicObject

While standard objects bound to a dynamic variable rely on the DLR inspecting their static CLR types via reflection, custom dynamic behavior is achieved through the IDynamicMetaObjectProvider interface.

Implementing IDynamicMetaObjectProvider directly is complex. To simplify this, .NET provides System.Dynamic.DynamicObject, an abstract base class that implements this interface and exposes virtual methods designed to intercept dynamic operations.

By inheriting from DynamicObject, you can override specific runtime hooks:

  • TryGetMember: Intercepts dynamic property reads.
  • TrySetMember: Intercepts dynamic property assignments.
  • TryInvokeMember: Intercepts dynamic method calls.
  • TryGetIndex / TrySetIndex: Intercepts dynamic indexer access.

Implementing a Dynamic Dictionary

A common use case for DynamicObject is creating a dynamic property bag that allows properties to be added and read on the fly without defining a rigid class structure.

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

public class DynamicDictionary : DynamicObject
{
    private readonly Dictionary<string, object> _properties = new();

    public override bool TryGetMember(GetMemberBinder binder, out object? result)
    {
        return _properties.TryGetValue(binder.Name, out result);
    }

    public override bool TrySetMember(SetMemberBinder binder, object? value)
    {
        _properties[binder.Name] = value!;
        return true;
    }

    public override bool TryInvokeMember(InvokeMemberBinder binder, object?[]? args, out object? result)
    {
        if (binder.Name == "Print" && args?.Length > 0)
        {
            Console.WriteLine($"{args[0]}: {_properties.GetValueOrDefault(args[0]?.ToString() ?? string.Empty)}");
            result = true;
            return true;
        }

        result = null;
        return false;
    }
}

This class can then be consumed via the dynamic keyword:

dynamic person = new DynamicDictionary();

// TrySetMember is invoked
person.FirstName = "Jane";
person.Age = 30;

// TryGetMember is invoked
Console.WriteLine($"{person.FirstName} is {person.Age} years old.");

// TryInvokeMember is invoked
person.Print("FirstName");

Returning true from these methods signals to the DLR that the dynamic operation succeeded. If an override returns false, the runtime binder defaults to standard member resolution and throws a RuntimeBinderException if the static member does not exist.

When to Use dynamic and DynamicObject

Dynamic typing introduces flexibility at the expense of compile-time guarantees, tooling support, and raw execution speed.

It is best suited for:

  • Interacting with non-static data structures: Parsing unstructured JSON, XML, or YAML configurations.
  • COM Interop: Interacting with Office automation APIs where types are often loosely bound.
  • Interoperability with dynamic languages: Working with IronPython or JavaScript engines running on the CLR.
  • Creating Domain-Specific Languages (DSLs): Creating fluent dynamic interfaces or wrappers.

Avoid dynamic dispatch in high-throughput, performance-critical code where static typing provides better execution speed, memory efficiency, and compile-time validation.