1. Introduction
C# is a statically-typed language. That means at compile time a variable must have a known type, and you can access class members only according to that type. This is safe, convenient, and friendly to the IDE and compiler.
But sometimes you need to work with objects whose structure might be unknown at compile time — for example, when:
- You're executing code written in another .NET language (for example IronPython or IronRuby), where typing is dynamic.
- Working with COM objects (for example, Microsoft Office via Interop).
- Interacting with dynamic data structures: e.g., loosely-structured JSON objects.
- You don't want to write a bunch of boilerplate and prefer to "trust" the object and try calling the method by name.
With the introduction of the dynamic keyword in C# 4.0 and the DLR platform you can write code that behaves "like Python, but in C#": checks happen at runtime, not at compile time. With this magic word you can make the compiler less strict and defer checking whether a method/property exists until the program actually reaches that spot.
What is the DLR?
The Dynamic Language Runtime (DLR) is an infrastructure extension to the CLR (Common Language Runtime) that enables support for dynamic languages (and dynamic behavior in static languages). The DLR handles operations that the compiler cannot verify: for example, invoking a method by name when the type will only be known at runtime. Thanks to the DLR, C# gets a "dynamic mode".
2. The dynamic keyword: how it works
Difference from object
Some beginners think that dynamic is just a fancier way to write object and cast every time. That's not the case.
- A variable of type object always requires an explicit cast to a concrete type to call methods (and the compiler enforces that).
- A variable of type dynamic tells the compiler: "trust me, I know what the type will be — we'll resolve it at runtime!". If the method doesn't exist, you'll get a runtime exception.
Example:
// Example with object
object obj = "Hello, C#!";
// obj.ToUpper(); // Compile error! The compiler doesn't know obj is a string
string s = ((string)obj).ToUpper(); // Only like this
// Example with dynamic
dynamic dyn = "Hello, C#!";
string upper = dyn.ToUpper(); // The compiler doesn't object, the check happens at runtime
With dynamic you can try to access non-existent properties and methods. If they don't exist you'll get a RuntimeBinderException.
Usage in an example app
Suppose in our mini-app there's a block that works with JSON data whose structure can change on the fly. With dynamic you can access properties as if they always exist:
dynamic user = new System.Dynamic.ExpandoObject();
user.Name = "John";
user.Age = 35;
Console.WriteLine($"Name: {user.Name}, age: {user.Age}");
3. DLR in action: what happens under the hood
When a variable is declared as dynamic, the C# compiler remembers that any accesses to methods, properties, indexers, operators, etc. on that object cannot be checked ahead of time.
Instead, at every dynamic call site the compiler inserts code that asks the runtime "dynamic dispatcher". The DLR checks: does the method or property we optimistically, carelessly, and hopefully access actually exist? If yes — it performs the call; if not — it throws an exception.
Visual diagram
+------------------------+
| Code with dynamic |
+----------+-------------+
|
v
+----------+-------------+
| C# compiler inserts |
| a "request to DLR" |
+----------+-------------+
|
v
+----------+-------------+
| At runtime DLR looks |
| for method/property |
+----------+-------------+
|
/---------\
| |
found not found
| |
v v
invoke exception
method (RuntimeBinderException)
DLR takes care of finding methods and properties on the object — and even caches frequently used dynamic calls!
A bit of history
DLR was created to support languages that are dynamic by nature: IronPython, IronRuby, and to ease integration with dynamic objects in C#. Today it's actively used with ExpandoObject, DynamicObject, as well as with COM and dynamically serialized data.
4. Comparison: static vs dynamic
Let's compare how property and method calls look in static and dynamic paradigms:
| Approach | Declaration | Compile-time check | Behavior on error |
|---|---|---|---|
|
|
Requires cast to the right type | Compile error or InvalidCastException |
|
|
No, everything at runtime | RuntimeBinderException if the method is missing |
| static type | |
Checked by the compiler | Compile error |
Example to illustrate:
// Static typing
MyUser u = new MyUser();
u.PrintInfo(); // Checked by compiler
// dynamic
dynamic du = new MyUser();
du.PrintInfo(); // Compiler doesn't check, method will be found only at runtime
// object
object ou = new MyUser();
// ou.PrintInfo(); // Compile error
((MyUser)ou).PrintInfo(); // Must cast explicitly
Static methods and dynamic
dynamic d = "Hello";
Console.WriteLine(d.Length); // 5 — fine
Console.WriteLine(d.FakeMethod()); // RuntimeBinderException at execution
But be careful! If you typo the method name or the method doesn't exist — you'll only see it at runtime. Classic example of "bug in production".
5. Practical examples: dynamics in real tasks
Working with ExpandoObject
using System.Dynamic;
dynamic person = new ExpandoObject();
person.Name = "John";
person.Greet = (Action)(() => Console.WriteLine($"Hello, {person.Name}!"));
person.Greet(); // Prints: Hello, John!
Here we added the Name property and even a delegate Greet on the fly. Very similar to how objects work in JavaScript.
Binding to JSON structures
using System.Dynamic;
using Newtonsoft.Json;
string json = @"{""name"": ""John"", ""age"": 30}";
dynamic obj = JsonConvert.DeserializeObject<ExpandoObject>(json);
Console.WriteLine(obj.name); // John
Console.WriteLine(obj.age); // 30
Note: if you access obj.surname — you'll get an error at runtime.
COM-Interop (Office Automation)
dynamic excel = Activator.CreateInstance(Type.GetTypeFromProgID("Excel.Application"));
excel.Visible = true; // We "see" the property even though the compiler doesn't know its type
6. Useful nuances
DLR, dynamic and using with other .NET languages
- IronPython/IronRuby: you can instantiate objects from these languages and call their methods from C# via dynamic.
- COM objects: automatic wrapping of methods and properties via dynamic.
- Custom dynamic objects: by implementing the interface IDynamicMetaObjectProvider you can make your class support dynamic calls.
Example of integration with IronPython
using IronPython.Hosting;
using Microsoft.Scripting.Hosting;
ScriptEngine engine = Python.CreateEngine();
dynamic py = engine.Execute("class Test: pass\nTest()");
py.x = 42;
Console.WriteLine(py.x); // 42
How to implement your own dynamic types
It's usually easiest to inherit from DynamicObject.
using System.Dynamic;
class MyDynamic : DynamicObject
{
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
if (binder.Name == "Ping")
{
result = "Pong!";
return true;
}
result = null;
return false;
}
}
dynamic d = new MyDynamic();
Console.WriteLine(d.Ping); // Pong!
Console.WriteLine(d.Miss); // null (doesn't throw)
When dynamic is harmful
- Reduces performance (dynamic calls are slower than static ones).
- Makes debugging and maintenance harder.
- Takes away most IDE goodies: autocompletion, refactoring, find usages.
- Can lead to errors that are discovered only by the user or on the server.
When you should actually use dynamic
- Integration with external libraries where writing wrappers is impossible or impractical (COM, Python, JavaScript).
- Manipulating loosely typed structures (JSON/XML/ExpandoObject).
- For building frameworks, metaprogramming, and flexible APIs that are meant to be dynamic.
In all other cases it's better to use static types and enjoy the compiler's help :-)
7. Common mistakes when working with dynamic and the DLR
Mistake #1: Accessing non-existent members.
Calling a missing method or property on dynamic leads to a RuntimeBinderException at runtime, which is hard to catch during development.
Mistake #2: Ignoring type checks.
Without checking types before calling (for example, if (obj is IDictionary<string, object>)) you can get unexpected runtime errors.
Mistake #3: Using dynamic in public APIs.
Dynamic types complicate code maintenance because IDEs and analyzers can't suggest correct members, reducing readability.
Mistake #4: Abusing dynamic in performance-critical code.
Dynamic calls are slower than static ones and add overhead. Use dynamic only when necessary.
GO TO FULL VERSION