DynamicMethod in C#: Lightweight Code Generation
The DynamicMethod class in .NET provides a
high-performance mechanism for generating and executing Common
Intermediate Language (CIL) at runtime without the overhead of creating
dynamic assemblies or saving files to disk. Part of the
System.Reflection.Emit namespace, it serves as a
lightweight alternative to full reflection, enabling tasks such as
dynamic mapping, runtime serialization, and fast member invocation. This
guide covers how to instantiate a DynamicMethod, emit
intermediate language instructions, compile the method into a typed
delegate, and execute it efficiently.
Why Use DynamicMethod?
Standard reflection using PropertyInfo.GetValue or
MethodInfo.Invoke incurs significant performance penalties
due to runtime validation, boxing, and dispatch overhead. While
compiling expression trees
(Expression<Func<T>>) offers an alternative,
expression trees introduce their own compilation overhead and do not
support low-level IL instructions. DynamicMethod solves
this by offering:
- Minimal Overhead: Dynamic methods are hosted in lightweight, transient modules and can be garbage-collected when no longer referenced.
- Direct IL Control: You emit exact opcodes, enabling fine-grained control over execution and memory.
- Visibility Bypass: By specifying an associated
module or type, a
DynamicMethodcan bypass standard access checks, allowing direct access toprivateorinternalmembers.
Step-by-Step Implementation
Creating and executing a dynamic method involves four primary steps:
- Instantiate the
DynamicMethod: Define its name, return type, parameter types, and owner context. - Retrieve the
ILGenerator: Use the generator to append IL opcodes. - Emit IL Instructions: Push values onto the evaluation stack, execute operations, and return the result.
- Bake into a Delegate: Convert the generated
bytecode into an invokable delegate using
CreateDelegate.
Example: Generating an Arithmetic Function
The following example builds a dynamic function equivalent to
(int a, int b) => a + b:
using System;
using System.Reflection.Emit;
public class Program
{
public static void Main()
{
// 1. Define the dynamic method: name, return type, parameter types
DynamicMethod addMethod = new DynamicMethod(
name: "AddNumbers",
returnType: typeof(int),
parameterTypes: new Type[] { typeof(int), typeof(int) },
m: typeof(Program).Module // Associated module context
);
// 2. Obtain an ILGenerator
ILGenerator il = addMethod.GetILGenerator();
// 3. Emit CIL instructions
il.Emit(OpCodes.Ldarg_0); // Push first argument (a) onto the stack
il.Emit(OpCodes.Ldarg_1); // Push second argument (b) onto the stack
il.Emit(OpCodes.Add); // Pop both, add them, and push the result
il.Emit(OpCodes.Ret); // Return the top value from the stack
// 4. Create a strongly-typed delegate
Func<int, int, int> addDelegate = (Func<int, int, int>)addMethod.CreateDelegate(typeof(Func<int, int, int>));
// 5. Invoke the delegate
int result = addDelegate(15, 27);
Console.WriteLine($"Result: {result}"); // Output: 42
}
}Accessing Private Members
To access private or protected members, pass a target
Type into the DynamicMethod constructor
instead of a Module. This grants the dynamic method the
same visibility permissions as the target class.
using System;
using System.Reflection;
using System.Reflection.Emit;
public class SecretVault
{
private readonly string _secretCode = "XYZ-12345";
}
public class AccessorExample
{
public static void Main()
{
FieldInfo field = typeof(SecretVault).GetField("_secretCode", BindingFlags.NonPublic | BindingFlags.Instance);
// Associate the DynamicMethod directly with SecretVault to bypass private access checks
DynamicMethod getter = new DynamicMethod(
"GetSecretCode",
typeof(string),
new Type[] { typeof(SecretVault) },
typeof(SecretVault),
skipVisibility: true
);
ILGenerator il = getter.GetILGenerator();
il.Emit(OpCodes.Ldarg_0); // Load the instance argument
il.Emit(OpCodes.Ldfld, field); // Load the value of the private field
il.Emit(OpCodes.Ret); // Return the value
Func<SecretVault, string> readSecret = (Func<SecretVault, string>)getter.CreateDelegate(typeof(Func<SecretVault, string>));
SecretVault vault = new SecretVault();
Console.WriteLine(readSecret(vault)); // Output: XYZ-12345
}
}Best Practices
- Cache Delegates: Emitting IL and compiling
delegates via
CreateDelegatecarries initial latency. Always cache the resulting delegate in a dictionary or static field to ensure subsequent invocations execute at native speed. - Handle the Evaluation Stack Correctly: Intermediate
Language works strictly on an evaluation stack. Every method must exit
with the correct number of items on the stack matching the method's
return type (zero for
void, one for non-void). - Match Types Exactly: Ensure opcodes match data
types (e.g., using
Ldc_I4for 32-bit integers vs.Ldc_I8for 64-bit integers) to avoid runtime verification exceptions.