Skip to content
← All posts
· Delta1 Labs Obfuscation.NETGuide

Obfuscating Code That Generates Code: Reflection.Emit, DynamicMethod, and Expression Trees

When your app emits IL at runtime — DynamicMethod, TypeBuilder, Expression.Compile — renaming can break it in ways the compiler never warns about. Here is exactly which runtime-codegen patterns survive obfuscation and which need keep-rules.

Serializers, ORMs, DI containers, mappers, and rule engines all do the same trick under the hood: they write IL at runtime instead of calling your methods directly, because a generated, specialised method is far faster than reflection on every call. .NET gives you three main ways to do it — DynamicMethod + ILGenerator, TypeBuilder via Reflection.Emit, and Expression.Compile over LINQ expression trees. All three interact with obfuscation in a way the compiler cannot warn you about, because the breakage happens at runtime, in code that did not exist at build time. Here is the precise model of what is safe and what is not.

The key distinction: the generator ships, the generated code does not

Two different pieces of code are in play, and conflating them is where the confusion starts.

The generated code — the DynamicMethod body, the TypeBuilder type, the compiled expression delegate — is created in memory at runtime and never written to your assembly. The obfuscator never sees it, so it is neither protected nor harmed. (If protecting it mattered you could not, anyway; it is ephemeral.)

The generator — the method full of il.Emit(...) calls, or the expression-building code — is in your assembly. It is plain IL and gets renamed, control-flow-flattened, and string-encrypted like everything else. That is fine, with one catch: the generator usually references your types and members, and how it references them decides whether obfuscation breaks it.

The generator is obfuscated; the generated code is runtime-only In your assembly (obfuscated): Generator methodILGenerator.Emit / Expression tree emits at runtime Runtime-only (never on disk): DynamicMethod bodydiscarded at process exit Risk is only in the arrows the generator uses to name YOUR members: string lookup → breaks on rename · typeof/MethodInfo token → survives (rewritten with the rename).

Where it breaks: string-based member lookup

A generator that resolves the members it emits calls to by string name is the one that breaks. Consider a DynamicMethod that calls a getter it found by name:

MethodInfo getter = typeof(Order).GetMethod("get_Total")!; // string lookup
var dm = new DynamicMethod("ReadTotal", typeof(decimal), new[] { typeof(Order) });
var il = dm.GetILGenerator();
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Callvirt, getter);   // emits a call to whatever `getter` resolved to
il.Emit(OpCodes.Ret);

After obfuscation renames Order.Total (and its get_Total accessor), the string "get_Total" no longer matches anything, so GetMethod returns null and the next line throws NullReferenceException / ArgumentNullException — at runtime, with no compile-time hint. Same failure for TypeBuilder code that defines overrides by name, and for Expression.Property(p, "Total").

The fix is one of two things. Keep the name of the specific members the generator looks up by string (a targeted keep-rule, not a blanket exclusion). Or, better, stop using strings: capture the member through something the obfuscator can rewrite.

What survives: references the obfuscator can see

When the generator captures a member as a metadata reference rather than a string, the obfuscator rewrites that reference as part of the rename, and everything keeps working:

// Survives renaming — MethodInfo captured from a delegate, not a string.
MethodInfo getter = ((Func<Order, decimal>)(o => o.Total)).Method; // won't help: that's a lambda's Method
// Better: read the MemberInfo out of an expression tree
Expression<Func<Order, decimal>> e = o => o.Total;
var prop = (PropertyInfo)((MemberExpression)e.Body).Member;          // real PropertyInfo, token-based

Compiled expression trees are the friendly case: o => o.Total carries a PropertyInfo reference that the obfuscator renames along with the property, so Expression.Compile() binds to the right member afterwards. The rule of thumb: expression trees and typeof(T).GetMethod(someMethodInfo-derived-reference) are rewrite-safe; any "string" member name in a generator is not.

A practical checklist

  • Audit your runtime-codegen sites for string literals naming your own members; those are the breakage points.
  • Prefer expression trees or delegate/MethodInfo capture over GetMethod("Name") where you can.
  • For the unavoidable string lookups, add targeted keep-rules for exactly those members — and remember their bodies are still obfuscated; only the name survives.
  • Test the obfuscated build’s codegen paths explicitly, because the compiler and even a smoke test that skips the generated path will not catch a broken emit.

Doing this with Nebula

Nebula renames, flattens, and string-encrypts your generators like any other code, and its keep-rule model is precise enough to preserve exactly the members your Reflection.Emit or expression-building code resolves by name, while still obfuscating their bodies. Pair it with the guidance in reflection and serialization pitfalls and you can ship obfuscated code that generates code without a single runtime surprise.

Start with the free edition, or see the plans.

Try Nebula.NET

Harden your .NET code in minutes — start with the free edition.