Using the C# Dynamic Keyword Safely
c# dynamic keyword: Learn when and how to use the `dynamic` keyword in C# for runtime binding, interoperability, and reflection scenarios, including performance and sa...
When you write dynamic in C#, you are telling the compiler to defer type checks until runtime. This is useful for scenarios where the shape of data is unknown until execution, such as when working with COM objects, JSON documents, or APIs that use reflection. However, the c# dynamic keyword comes with tradeoffs that affect performance, maintainability, and error handling. This article explains how dynamic behaves under the hood, when it makes sense to use it, and how to avoid common pitfalls.
How the Compiler Treats Variables Declared as dynamic
In C#, dynamic is a static type in the sense that you must declare it explicitly, but the compiler treats any expression of type dynamic as a request for runtime binding. Consider this code:
dynamic value = GetValue(); value.Calculate();
The compiler does not know whether value has a Calculate method. Instead, it generates a call site that attempts to bind Calculate at runtime. If the object does not have that method, a RuntimeBinderException is thrown. The compiler still performs syntax checks, but type checking is deferred.
This is different from using var, which is still fully type-checked at compile time. var simply infers the static type of the expression. dynamic is the only C# type that suspends compile-time type checking for the variable and any operations performed on it.
When Use of the C# Dynamic Keyword Makes Sense
Dynamic binding is primarily useful in three areas:
- Interoperability with dynamic languages – If you are embedding a scripting engine like IronPython, the dynamic keyword maps directly to the language's dynamic behavior.
- Office and COM automation – COM objects, such as those in Microsoft Office, often have member signatures that are not fully known at compile time. Using dynamic avoids the need to cast every call to
objectand allows calls likeexcelWorksheet.Cells[1, 1].Value = "Hello". - Reflection-heavy APIs – When using libraries that return data as
object, such as anIDataReaderor a generic serializer, dynamic can save you from writing repetitive reflection code.
A typical use case is a factory method that returns different types depending on input:
public dynamic CreateInstance(string typeName) { return Activator.CreateInstance(Type.GetType(typeName)); }
Here, the actual type of the returned instance is unknown at compile time, so dynamic is a reasonable return type. However, you should limit the use of dynamic to the boundary of your application. Internally, you should immediately convert the dynamic value to a concrete type or interface so that the rest of your code retains type safety.
Performance and Runtime Cost of Dynamic Binding
The primary cost of dynamic is that the compiler generates a call site that uses the Dynamic Language Runtime (DLR). The first time a call site is reached, the DLR examines the object and caches the binding. Subsequent calls with objects of the same type reuse that cached binding, which is faster than full reflection but still slower than a statically bound call.
The performance impact varies. For a single call, the overhead is negligible. For repeated calls inside a loop, the overhead can become noticeable. In microbenchmarks, dynamic calls are typically an order of magnitude slower than direct calls, though the exact number depends on the interface and the object's structure. The tradeoff is acceptable when the alternative is writing substantial reflection code that must parse attributes and invoke methods via MethodInfo. Dynamic reduces that verbosity, but you should not use it as a general replacement for interfaces or generics.
Another runtime cost is that exceptions may be wrapped. A RuntimeBinderException often includes a message that is not as clear as a compiler error. The stack trace points to where the binder exception was thrown, but the root cause may be a missing member or an incompatible argument type. Good exception handling is essential when working with dynamic.
Error Handling with Dynamic: Catching and Avoiding RuntimeBinderException
Because binding failures only surface at runtime, you need to be careful. The most common failure is invoking a member that does not exist. For example:
dynamic data = GetJObject(); var name = data.Name; // If data does not have a Name, exception here
You can catch RuntimeBinderException, but catching it is fragile because the exception message varies and does not always indicate which member was missing. A better approach is to validate the shape of the data before using dynamic. For JSON, for instance, you can check the document structure before indexing into dynamic properties.
When you control the type, avoid dynamic altogether. If you are consuming an external library, consider using a static wrapper class that encapsulates the dynamic calls, so the rest of your application does not depend on dynamic behavior.
dynamic in Interop and Reflection: Beyond the Basics
The dynamic keyword works with any object that implements IDynamicMetaObjectProvider. Most .NET objects do not implement this interface, so dynamic binding falls back to reflection. This means that calling a method on a plain object through dynamic is essentially reflection with a caching layer. Knowing this helps you predict performance and behavior.
For reflection scenarios, dynamic can be a convenient replacement for calls like:
var method = typeof(MyClass).GetMethod("DoWork"); method.Invoke(instance, null);
You can instead write:
dynamic instance = new MyClass(); instance.DoWork();
But this only works if the method is public and not overloaded in a way that causes ambiguity. Overload resolution happens at runtime and follows the same rules as the compiler would, but it may not always pick the overload you expect. If you have multiple methods with the same name, dynamic can be unpredictable because the binder uses the runtime type of arguments, which can be a static type of object if you are not careful.
Another subtlety is that dynamic does not work with extension methods. The compiler cannot resolve extension methods at runtime because they are static methods discovered at compile time. If you try to call an extension method on a dynamic variable, you will get a RuntimeBinderException. This is a common mistake.
The Interplay Between dynamic and Object: What You Need to Know
dynamic and object are related but not equivalent. object is a static type; any variable typed as object is treated as object at compile time, and you must cast to call specific members. dynamic changes the binding. The following table illustrates the difference:
| Aspect | Statement with object | Statement with dynamic |
|---|---|---|
| Type checking | At compile time (as object) | At runtime (actual type) |
| Member access | Requires cast | Direct call syntax |
| Overload resolution | Based on static type (object) | Based on runtime type |
| Performance | Cast overhead minimal | DLR binding overhead |
| Error detection | Compile-time error for missing members | RuntimeBinderException |
| Intended use | Passing unknown types without binding | Interop or dynamic languages |
When you assign a dynamic variable to a object variable, the static type becomes object. You lose the dynamic behavior. Conversely, assigning an object to a dynamic variable does not automatically make it dynamic; the variable behaves as a dynamic dispatch only when the compile-time type is dynamic and the runtime type is a regular CLR object.
Maintainability Risks and How to Mitigate Them
Dynamic makes code harder to refactor. If you rename a method on a class, the compiler will not catch every call site if those calls are via dynamic. You may discover the break only when that code path executes in production. To mitigate this, keep dynamic usage as close to the integration point as possible, and add unit tests that exercise those paths. The same applies to changing method signatures – the binder may fail silently if the new signature happens to be compatible with the wrong overload.
Another risk is that dynamic can hide null reference issues. If a dynamic expression returns null, trying to call a member on it will throw a RuntimeBinderException or a NullReferenceException depending on the binder implementation. The behavior is not always consistent across .NET versions. Always check for null before invoking members on dynamic values.
A practical pattern is to define an interface that describes the members you need, then cast the dynamic object to that interface if it implements it. This gives you compile-time safety without giving up the flexibility of loading types dynamically.
public interface IReport { void Generate(); } var dynamicReport = LoadReport(); var report = dynamicReport as IReport; if (report != null) { report.Generate(); } else { // Handle unsupported report type }
This approach is safer than relying on dynamic throughout the codebase.
A Closer Look at Dynamic Dispatch: IDynamicMetaObjectProvider and ExpandoObject
The dynamic keyword is most powerful when the object implements IDynamicMetaObjectProvider. This interface allows an object to define its own binding rules. The most common built-in implementation is ExpandoObject, which lets you add properties and methods at runtime as if it were a dictionary.
dynamic expando = new ExpandoObject(); expando.Name = "Sample"; expando.Print = new Action(() => Console.WriteLine("Hello")); expando.Print();
ExpandoObject is useful when you need to build an object dynamically, for example, when creating a JSON response that has variable fields. However, the underlying data structure is a dictionary, so accessing members is essentially a hash lookup. The performance is similar to using a Dictionary<string, object> but with a cleaner syntax. When you need to pass such an object to a method that expects a known type, you must cast it.
DynamicObject is another base class you can derive from to create custom dynamic objects. It lets you override TryGetMember, TrySetMember, TryInvoke, and other methods. This is useful for building a lightweight proxy or a dynamic view over a data source. But it adds complexity, and you should only use it when you truly need custom binding semantics.
A common misuse of ExpandoObject is using it to simulate a typed DTO. For example, you might be tempted to create an ExpandoObject where you know at compile time that the object will always have Id and Name properties. In that case, a simple immutable class is clearer, faster, and catches mistakes early.
When to Avoid the C# Dynamic Keyword Entirely
There are situations where dynamic is heavily discouraged:
- Public API signatures – Returning
dynamicfrom a public method forces every caller to opt into dynamic binding. This removes compile-time safety for all downstream code. Even if you need dynamic internally, you can expose a more concrete type. - Performance-sensitive code paths – If you have a loop that executes many times and calls methods on a dynamic variable, consider whether you can express the operation with an interface or a delegate instead. The DLR caching helps, but it cannot match the direct function call overhead of static dispatch.
- Code that must be refactored frequently – As mentioned, dynamic breaks the safety net of the compiler. If the domain model changes often, using dynamic increases maintenance cost.
- When you need argument type safety – Dynamic does not enforce that the arguments you pass to a method are of the correct type. The binder may perform implicit conversions that you did not expect. For example, passing an
intto a method that expects astringmay work if an implicit conversion exists, leading to subtle bugs.
In addition, dynamic is not available in all contexts. You cannot use dynamic in parameter default values, attribute arguments, or in nameof expressions. Also, extension methods do not work with dynamic arguments as mentioned earlier. These limitations can surprise developers who are used to the flexible syntax.
The Performance Measurement Trap
Developers often try to measure the cost of dynamic and conclude that it is negligible based on a microbenchmark. However, microbenchmarks typically use the same object type for every iteration, which means the DLR cache is warm. In real applications, dynamic values may come from different sources, such as different JSON shapes, causing cache misses and more binder work. Even with warm caches, the overhead per call is not zero. A better approach is to measure the actual end-to-end throughput of the feature that uses dynamic, rather than isolating a single binding operation.
If you are considering dynamic for a section of code that runs frequently, profile the application first. Many developers replace dynamic with a generic method or an interface and see meaningful gains, but this depends on the context. There is no universal threshold for when dynamic becomes a bottleneck. The only reliable method is to measure your specific usage.
Compatibility Considerations with Older and Newer .NET Versions
The behavior of the DLR and the binder has been stable across .NET Framework and .NET Core, but there are minor differences. For example, in some versions of .NET Core, RuntimeBinderException messages have been improved. Additionally, the DynamicObject API has remained largely unchanged. If you are targeting multiple frameworks, you should test dynamic code on each because the binder uses internal helpers that may differ slightly. The good news is that dynamic is fully supported in modern .NET, including .NET 8 and later, so you do not have to worry about it being obsolete.
One area that has changed is the interaction with nullable reference types. When you have dynamic? with nullable annotations, the compiler warns about possible null dereference, but the binding still happens at runtime. This means you need to explicitly check for null before using a dynamic variable that might be null, especially in generic or reflection-based code.
Using Dynamic in a Generic Context
You can use dynamic as a generic type argument, but this can lead to confusion. For instance, List<dynamic> is allowed, but it behaves differently from List<object>. With List<object>, each element is statically typed as object, and you must cast to call members. With List<dynamic>, each element is dynamic, so you can call members directly, but you lose compile-time safety. This is a common pattern in code that deals with heterogeneous data collections.
When you have a generic method that uses dynamic, the compiler will warn about using dynamic in a generic context because it may be interpreted as object. For example:
public void Process<T>(T item) where T : class { dynamic d = item; d.Method(); // works only if item has Method }
This works, but the compiler may produce a warning. It is better to constrain the generic type or use an interface.
A more robust alternative to dynamic is to use System.Text.Json.Nodes.JsonNode for JSON traversal or System.Dynamic.ExpandoObject for building objects. These types have their own APIs and are often clearer than mixing dynamic with reflection-heavy code.
Debugging and Logging of Dynamic Calls
When a dynamic call fails, the exception includes the message that describes the failure, but it does not show the call stack with the exact source line if the call originated from a compiled expression. To debug, you can log the type of the object and the member name. For example:
try { dynamic result = obj.Calculate(); } catch (RuntimeBinderException ex) { Console.WriteLine($"Failed to call Calculate on type {obj.GetType().FullName}: {ex.Message}"); }
This gives you a clearer picture of what went wrong. You should also enable first-chance exception logging in your debugger to catch these exceptions when they are thrown, not when they are caught. In production, consider adding structured logging that includes the operand type.
Another debugging technique is to use the C# interactive window or a small test harness to call the dynamic member with a known type, to verify the binding works before integrating it into the larger system.
Final Thoughts on Adopting Dynamic Responsibly
The c# dynamic keyword is a legitimate tool, but it is easy to overuse. Its primary value is in interop and reflection, not as a general-purpose type system replacement. When you use it, you accept deferred errors and runtime overhead. The key to using it safely is to confine dynamic to the boundaries of your application, wrap it in well-tested functions, and avoid it in hot paths or public APIs.
If you find yourself reaching for dynamic frequently, examine why. If it is because your data is genuinely untyped, such as JSON or COM, that is a valid reason. If it is because you are avoiding type conversions, you are likely causing more problems than you solve. Prefer interfaces, generics, and expression trees where possible, because they offer compile-time safety and better performance. Dynamic is not something to be afraid of, but it should be a deliberate choice backed by an understanding of its runtime behavior and maintenance implications.
When you do use dynamic, validate inputs, check for nulls, and prepare for exceptions. The cost of those precautions is small compared to the cost of debugging a production issue caused by a missing member on an object that only exists at runtime.