How to Obfuscate a .NET Application
Without Breaking Reflection
Step-by-step instructions for protecting C# and VB.NET code with dynamic Type.GetType() and GetMethod() calls using Opaquer .NET Obfuscator.
Direct Answer
Obfuscating reflection-dependent .NET applications requires selective exclusion of runtime-resolved type and member names from identifier renaming while retaining protection for the underlying logic. In Opaquer .NET Obfuscator, you achieve this by selecting reflection-targeted types and methods in the assembly tree and disabling symbol renaming for those specific identifiers. This ensures metadata reflection APIs like Type.GetType() and GetMethod() succeed at runtime while string encryption and control-flow obfuscation continue to secure sensitive business logic.
1 The Problem
When a .NET obfuscator scrambles metadata identifiers, it replaces human-readable class, method, property, and field names (such as MyApp.Processor or ProcessData) with non-printable unicode characters, random string patterns, or encrypted metadata tokens.
Standard C# compilation bakes metadata tokens directly into method calls (e.g., IL call instance void MyApp.Processor::ProcessData()). Renaming these tokens in Intermediate Language (IL) changes both the declaration and the static call site in tandem, allowing direct method calls to execute without issue.
Dynamic Reflection Mechanics
Reflection operates dynamically via string literals or runtime metadata queries:
Type.GetType("MyApp.SomeClass")passes a hardcoded string to the runtime type resolver.typeof(MyClass).GetMethod("ProcessData")queries assembly metadata for a method named"ProcessData".Activator.CreateInstance("MyApp", "MyApp.SomeClass")dynamically locates and instantiates a type by string.
If an obfuscator renames MyApp.SomeClass to a.a or \u0001, the runtime reflection lookup searches for the literal string "MyApp.SomeClass", fails to find it in the obfuscated metadata table, and throws a runtime exception.
Runtime Reflection Lookup Comparison
2 Reflection-Dependent Code Example
Below is a practical implementation featuring dynamic type loading and method invocation commonly found in factory patterns, dependency injection engines, and plugin architectures.
using System;
using System.Reflection;
namespace MyApp
{
public interface IProcessor
{
void ProcessData(string payload);
}
public class DataProcessor : IProcessor
{
public void ProcessData(string payload)
{
Console.WriteLine($"[Processing Payload]: {payload}");
}
}
public class ReflectionFactory
{
public static void ExecuteDynamicCall()
{
// 1. Dynamic type resolution via literal string
Type targetType = Type.GetType("MyApp.DataProcessor");
if (targetType == null)
{
throw new InvalidOperationException("Failed to locate target type via reflection.");
}
// 2. Dynamic instantiation
object instance = Activator.CreateInstance(targetType);
// 3. Dynamic method resolution
MethodInfo method = targetType.GetMethod("ProcessData");
if (method == null)
{
throw new MissingMethodException("MyApp.DataProcessor", "ProcessData");
}
// 4. Dynamic invocation
method.Invoke(instance, new object[] { "Encrypted_Order_Payload_9921" });
}
}
}
Imports System.Reflection
Namespace MyApp
Public Interface IProcessor
Sub ProcessData(ByVal payload As String)
End Interface
Public Class DataProcessor
Implements IProcessor
Public Sub ProcessData(ByVal payload As String) Implements IProcessor.ProcessData
Console.WriteLine($"[Processing Payload]: {payload}")
End Sub
End Class
Public Class ReflectionFactory
Public Shared Sub ExecuteDynamicCall()
' 1. Dynamic type resolution via literal string
Dim targetType As Type = Type.GetType("MyApp.DataProcessor")
If targetType Is Nothing Then
Throw New InvalidOperationException("Failed to locate target type via reflection.")
End If
' 2. Dynamic instantiation
Dim instance As Object = Activator.CreateInstance(targetType)
' 3. Dynamic method resolution
Dim method As MethodInfo = targetType.GetMethod("ProcessData")
If method Is Nothing Then
Throw New MissingMethodException("MyApp.DataProcessor", "ProcessData")
End If
' 4. Invocation
method.Invoke(instance, New Object() {"Encrypted_Order_Payload_9921"})
End Sub
End Class
End Namespace
3 What Happens Under Naive Obfuscation
When an application containing dynamic reflection calls is processed using default, aggressive symbol renaming:
The class MyApp.DataProcessor is scrambled to A.a.
The method ProcessData is scrambled to b().
Type.GetType("MyApp.DataProcessor") returns null, breaking execution.
Because the literal string "MyApp.DataProcessor" inside your code remains unchanged while assembly metadata is mutated to randomized symbols, standard runtime lookup fails completely.
4 Step-by-Step Opaquer Configuration
Follow these steps inside Opaquer .NET Obfuscator to keep reflection operational while protecting the underlying application logic.
Identify Reflection-Dependent Members
Audit your codebase for dynamic reflection invocations like Type.GetType(), Assembly.GetType(), Activator.CreateInstance(), or string queries passed to GetMethod().
Open the Assembly in Opaquer
Launch Opaquer .NET Obfuscator. Click Open File and load your primary target assembly (.exe or .dll). Opaquer inspects the assembly metadata and constructs a hierarchical visual tree under the Settings tab.
Locate Affected Classes and Methods
Expand the namespace nodes in the left panel to locate reflection targets (e.g., expand MyApp → DataProcessor → ProcessData).
Exclude Target Members from Name Obfuscation
In the tree view, uncheck Rename Type for MyApp.DataProcessor and uncheck symbol renaming for the ProcessData method node.
Decorate C# code directly before compiling using standard attributes:
[System.Reflection.Obfuscation(Exclude = true, ApplyToMembers = true)]
Keep String Encryption & Control-Flow Enabled
Excluding a type name from renaming does not leave it unprotected. Keep Strings Encryption enabled to encrypt string literals inside method bodies, and keep High-Intensity Control Flow active to obfuscate execution logic.
5 Before/After Visual Decompiler View (ILSpy)
Compare how decompilers like ILSpy view your code under different obfuscation configurations:
6 Testing the Obfuscated Application
Always validate protected binaries in your build pipeline prior to distribution:
Run unit and integration tests directly against obfuscated build output rather than raw Debug or Release assemblies.
Intercept standard reflection exceptions: TypeLoadException, MissingMethodException, and NullReferenceException.
Ensure dynamically discovered assemblies loaded via Assembly.LoadFrom() match expected string contracts.
7 Common Failures and Fixes
| Symptom / Exception | Root Cause | Solution in Opaquer |
|---|---|---|
| TypeLoadException | Class name scrambled by symbol renaming. | Uncheck Rename Type in tree view or use [Obfuscation(Exclude = true)]. |
| MissingMethodException | Method name scrambled during member obfuscation. | Disable member renaming for target method node under its class tree. |
| JsonSerializationException | Property names renamed; serializer failed mapping. | Exclude DTO properties or use explicit attributes like [JsonPropertyName]. |
| XamlParseException (WPF) | XAML view paths miss renamed code-behind types. | Enable Obfuscate XAML in WPF application option in Opaquer. |
8 Production Recommendations
nameof() Over String Literals:
Refactor legacy string queries to nameof(MyApp.DataProcessor) for compile-time safety and easier exclusion mapping.
Isolate external contract interfaces in dedicated namespaces (e.g., MyApp.Contracts) while applying maximum protection to core internal engines.
Save exclusion rules into an Opaquer project file (.opq) and trigger batch protection in Azure DevOps or GitHub Actions builds.
9 Frequently Asked Questions
System.Reflection.ObfuscationAttribute. Decorating a class or method with [Obfuscation(Exclude = true)] instructs Opaquer to automatically skip symbol renaming for that target without manual GUI tree toggles.