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 runtimeIf 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:
- Expression Trees: Represent runtime operations as code data structures.
- 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.
- 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.