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.
<br> .b{fill:none;stroke:#64748b;stroke-width:1.5}.t{fill:currentColor;font-size:13px}.s{fill:#64748b;font-size:11px}<br> .ob{stroke:#6366f1;stroke-width:2}.ar{stroke:#64748b;stroke-width:1.5;marker-end:url(#e)}<br>
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/
MethodInfocapture overGetMethod("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.
Top comments (0)