Nearly every obfuscation problem has the same root cause. Renaming is safe when the compiler resolves a reference, because the compiler updates both sides. It is unsafe the moment something looks up a member by name at runtime, because the string doing the lookup does not get renamed with it.
Why the mismatch happens
A normal method call is a metadata reference. When ValidateLicense is
renamed to a, every call site is rewritten to match, and the assembly
stays internally consistent.
Reflection sidesteps that entirely:
// Rewritten correctly — this is a metadata reference.
var ok = validator.ValidateLicense(key);
// Not rewritten — "ValidateLicense" is just a string.
var method = type.GetMethod("ValidateLicense");
var ok2 = method.Invoke(validator, new object[] { key });
After obfuscation the method is called a, so GetMethod returns
null and you get a NullReferenceException — at runtime, in
production, in whichever code path happened not to be smoke-tested.
The usual suspects
JSON and XML serialization
Serializers map property names to fields in the payload. Rename the property and the shape of your JSON changes with it — so previously written files, API contracts and stored settings stop round-tripping.
The fix is to state the wire name explicitly, which is good practice regardless of obfuscation because it decouples your serialized contract from your internal naming:
public class LicenseInfo
{
[JsonPropertyName("expiryDate")]
public DateTime ExpiryDate { get; set; }
[JsonPropertyName("customerId")]
public string CustomerId { get; set; }
}
With explicit names the serialized form is pinned to the attribute, and renaming the property is harmless.
WPF, WinForms and XAML
XAML binds to properties by name, and much of it is resolved at runtime. A binding to
{Binding CustomerName} looks up a property called
CustomerName — which no longer exists.
In WPF this fails quietly. Bindings that cannot resolve produce a trace warning and render nothing, so instead of a crash you get a window with empty fields. This is exactly why public renaming is disabled for libraries and why you should test every view after obfuscating a desktop application. Anything referenced from XAML — bound properties, commands, converters, code-behind event handlers — needs to keep its name.
Dependency injection
Explicit registration is fine, because AddScoped<IFoo, Foo>() is a
type reference the compiler rewrites. Convention-based scanning is not:
// Safe — type references.
services.AddScoped<IUserService, UserService>();
// Fragile — depends on the literal type name surviving.
var types = assembly.GetTypes()
.Where(t => t.Name.EndsWith("Service"));
After renaming, no type is called SomethingService any more and the scan
silently registers nothing. Prefer explicit registration, or mark the types with an
attribute and scan for the attribute rather than the name.
Entity Framework
EF maps entity and property names to tables and columns by convention. Renaming changes the inferred mapping and produces SQL referencing columns that do not exist. Configure the mapping explicitly:
modelBuilder.Entity<Customer>(e =>
{
e.ToTable("Customers");
e.Property(c => c.EmailAddress).HasColumnName("EmailAddress");
});
Plugin loading and type resolution
Any Type.GetType("MyApp.Plugins.CsvExporter") or assembly-qualified name
in a config file breaks. If you load plugins dynamically, keep the contract assembly
unobfuscated — the interfaces both sides bind to must keep stable names by definition.
Testing frameworks
Test discovery works by reflection over attributes and names. Never obfuscate test assemblies, and always run your suite against the unobfuscated build. Only the shipped artifacts should be protected.
Designing so this stops being a problem
The teams who have the least trouble here follow a few habits:
- Pin external contracts explicitly. Anything crossing a boundary — JSON, database columns, config keys — gets an explicit attribute rather than relying on the member name.
-
Prefer
nameofto string literals.nameof(CustomerName)is evaluated at compile time and updated by renaming, so it stays correct. A bare"CustomerName"does not. - Keep the reflective surface small and deliberate. Reflection over a handful of known types is easy to reason about; reflection over everything in the assembly is not.
- Split contracts into their own assembly. If plugins or external consumers bind to your interfaces, put those in a separate library and leave it unobfuscated. Protect the implementation, which is where the value actually is.
Diagnosing a break
When the obfuscated build misbehaves, the symptoms are fairly diagnostic:
| Symptom | Likely cause |
|---|---|
NullReferenceException right after a GetMethod or GetProperty | The lookup string no longer matches. |
| Empty UI fields, no exception | XAML bindings failing to resolve. |
| Deserialized object with all-default values | Property names changed under the serializer. |
| "Unable to resolve service for type…" | Convention-based DI scan matched nothing. |
| SQL error naming an unknown column | EF convention mapping shifted. |
Compare the two builds side by side in ILSpy to confirm which member was renamed, then pin that name explicitly rather than turning obfuscation off. Reaching for the global switch costs you the protection everywhere to fix one binding.
Keeping perspective
This reads like a long list of hazards, but in practice most applications obfuscate cleanly on the first attempt. The failure modes are concentrated in a small number of well-known places — serialization, XAML, DI scanning, EF — and each has a straightforward fix that improves the code anyway. Test those four paths and you have covered nearly everything.