Reflection-Safe Obfuscation in .NET: Keeping Dynamic Code Alive After Protection
Obfuscation is not about renaming everything. It is about renaming what you can prove is safe to rename. This guide shows how to draw that boundary for apps that rely on reflection, DI containers, serializers, and plugins - and how to keep strong protection active on the rest with Skater.
1 The Mental Model: Static Binding vs Dynamic Binding
Direct C# calls use metadata tokens. When you write processor.Process(), the compiler emits callvirt with a token. If an obfuscator renames the method, it rewrites both the declaration and every token reference - safe.
Reflection does not use tokens. It uses strings at runtime:
Type.GetType("Billing.InvoiceService")GetMethod("Calculate") / GetProperty("Total")Activator.CreateInstance(name)If you rename Billing.InvoiceService to a.b, reflection still asks for "Billing.InvoiceService". The CLR finds nothing. That is the entire failure mode - a string that outlives its target.
2 Where Reflection Actually Breaks in Production
Not all reflection is equal. In practice, four buckets cause 90% of post-obfuscation crashes:
| Bucket | Typical API | Symptom if renamed |
|---|---|---|
| Serialization contracts | System.Text.Json, Newtonsoft.Json, XmlSerializer | Properties deserialize as default/null |
| DI / IoC | Microsoft.Extensions.DependencyInjection, Autofac | Service not registered / Unable to resolve |
| Plugin / Factory | Assembly.GetType, CreateInstance, MEF | TypeLoadException, null returns |
| UI Binding | WPF BAML, INotifyPropertyChanged string names | XamlParseException, silent binding failure |
3 Designing an Exclusion Policy Before You Click Anything
Do not start in the obfuscator UI. Start in code. The question to answer for each type is: Is its name part of a contract observed outside its own assembly?
- Keep name if: it is a DTO, API model, EF entity mapped by convention, ViewModel bound by name, plugin contract, or service interface resolved by string.
- Rename aggressively if: it is internal logic, helper, algorithm, licensing, or proprietary business rule - where callers use token references, not strings.
- Hybrid: keep the type and its public contract properties/methods, but let Skater obfuscate private fields and method bodies.
If you could delete the type's name from source and the compiler would still find all usages via typeof or direct references, it is safe to rename. If usage is only discoverable via grep for a string literal, preserve it.
4 Applying the Policy in Skater .NET Obfuscator
Skater lets you apply the policy in two complementary ways. Use both - attributes for boundaries you own, exclusion rules for boundaries you don't.
A. Declarative - ObfuscationAttribute
Ideal for your own domain models. Skater honors standard .NET attributes natively.
using System.Reflection;
[Obfuscation(Exclude = true, ApplyToMembers = false)]
public class PaymentDto
{
[Obfuscation(Exclude = false)] // allow body obfuscation
public decimal Amount { get; set; }
public void Validate() { /* logic still protected */ }
}
B. Configurational - Skater Tree Exclusions
- Open assembly in Skater. It builds a full metadata tree.
- Navigate to namespace
Billing→InvoiceService. - Uncheck Rename Type for contracts; leave Obfuscate Control Flow and Encrypt Strings checked.
- For properties used by JSON: right-click → Exclude from renaming but keep body protection.
- Save profile as
reflection-safe.skprojand use in CI:SkaterCLI.exe --project reflection-safe.skproj
InvoiceService but not how Calculate() works.5 Real-World Scenarios and Fixes
Scenario 1: System.Text.Json DTOs
Fix: either preserve property names or make names explicit with attributes. Preferred for new code is source generation.
[JsonSerializable(typeof(PaymentDto))] // generates metadata, no reflection lookup
public partial class AppJsonContext : JsonSerializerContext {}
Scenario 2: Microsoft.Extensions.DependencyInjection
If you register via services.AddScoped<IInvoiceService, InvoiceService>(), token-based, safe to rename implementation. If you register via Assembly.Scan or string names, exclude service contracts.
Scenario 3: WPF / MVVM
Skater rewrites BAML references automatically. For bindings like {Binding TotalAmount}, keep ViewModel public properties. Alternatively, switch to x:Bind or compiled bindings where possible.
Scenario 4: Plugin Architecture
Define a stable contract assembly that is never obfuscated. Obfuscate plugin implementations heavily, but keep contract interface names and discovery attributes (e.g., [Export]) intact.
6 Verification: How to Prove It Did Not Break
Never trust a green build. Run a post-obfuscation reflection smoke suite.
- Deserialize every DTO from sample JSON
- Resolve every DI service from provider
- Load each plugin via factory path
- Open every WPF window / user control
- Run
dotnet testagainst obfuscated binaries
GetMethod returns null → method name renamed
JsonException: property not found → DTO property renamed
XamlParseException → BAML not synchronized
7 Hardening What You Did Not Exclude
Once contracts are stable, turn intensity up on the rest:
- String Encryption - Always On: Hides connection strings, endpoints, feature flags inside even excluded types' method bodies.
- Control Flow - High: Skater's virtualization and opaque predicates make decompiled logic unreadable, even if type name is visible.
- Anti-Tamper / Anti-Debug: Keep enabled; it protects the whole assembly, not just renamed parts.
- Watermarking: Add assembly watermark to trace leaks without affecting reflection.