Obfuscation and reflection: what breaks and why

Renaming is safe until something looks up a member by name. Serialization, DI containers and XAML are the usual suspects — here is how to avoid each trap.

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.

The rule in one line. If a name appears inside a string literal, a config file, or an external file such as XAML or JSON, renaming will break 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 nameof to 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:

SymptomLikely cause
NullReferenceException right after a GetMethod or GetPropertyThe lookup string no longer matches.
Empty UI fields, no exceptionXAML bindings failing to resolve.
Deserialized object with all-default valuesProperty names changed under the serializer.
"Unable to resolve service for type…"Convention-based DI scan matched nothing.
SQL error naming an unknown columnEF 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.

Try it on your own assembly

Upload a DLL or EXE and download the obfuscated result. Free, no signup, nothing stored.