Skip to content
← All posts
· Delta1 Labs Obfuscation.NETGuide

What Obfuscation Breaks: Reflection, Serialization, and What to Exclude

The first time you obfuscate a working app it can break in surprising ways: JSON comes out wrong, reflection returns null, data binding stops. The cause is always the same — something resolved a renamed member by its name. Here is exactly what breaks, why, and how to exclude the right things without weakening protection.

You add an obfuscator to a working .NET app, build, run — and something that worked yesterday is broken. The JSON your API returns has fields named a and b. A Type.GetMethod("Process") call returns null. Your WPF window binds to nothing. Dependency injection can”t find a service. None of this is a bug in the obfuscator; it is the single most common obfuscation mistake, and it always has the same root cause: something resolved a renamed member by its name at runtime.

Why names are the fault line

Renaming — the core of most obfuscation — turns CustomerName into a, CalculateTotal into b. The compiled code that calls those members is rewritten to match, so direct calls keep working; the compiler already resolved them to tokens, not names. The danger is any code path that goes looking for a member by its string name at runtime, because that lookup still asks for the original name, which no longer exists in the assembly.

That is the whole failure mode. Everything below is a specific instance of it.

The usual casualties

Serialization. A serializer maps between member names and a wire format. Rename the members and the output renames with them:

public class Invoice { public decimal Total { get; set; } public string Customer { get; set; } }

JsonSerializer.Serialize(new Invoice { Total = 42, Customer = "Acme" });
// Expected: {"Total":42,"Customer":"Acme"}
// After renaming: {"a":42,"b":"Acme"}   ← your API contract just changed

Worse, deserialization of previously stored data now fails to bind, because the JSON says Total and the property is called a. Any type that crosses a serialization boundary — API DTOs, settings files, saved documents, message payloads — is at risk.

Reflection. Any by-name lookup returns the wrong member or nothing:

var mi = typeof(Report).GetMethod("Generate"); // null — it's called 'a' now
typeof(Plugin).GetProperty("Name")?.GetValue(obj); // null
Activator.CreateInstance(Type.GetType("MyApp.Widgets.Chart")); // type name changed → null
Enum.Parse<Status>("Active"); // enum member renamed → throws

Data binding is reflection in disguise. WPF/XAML {Binding CustomerName}, Blazor @bind, and DataGrid auto-columns all resolve property names as strings at runtime — rename the property and the binding silently shows nothing.

Convention-based frameworks. DI that maps IFooService → FooService by name, EF Core mapping a property to a column by name, AutoMapper matching members by name, MVC model binding — anything whose “magic” is name matching breaks when the names change.

The rule: exclude the contract, not the logic

The fix is not to obfuscate less; it is to exclude from renaming exactly the names that are resolved by name, and nothing more. In practice that is a small, identifiable set:

  • Types that are serialized (DTOs, settings, message contracts).
  • Members reached by reflection, including anything a framework resolves by name.
  • Types/members referenced from XAML or other markup.
  • Public API surface of a library others compile against.

You mark these to skip renaming — with an attribute or a config rule. Nebula.NET takes rename-exclusion rules by namespace, type or attribute, so you keep a clean boundary: your Contracts namespace keeps its names, the rest of the assembly is renamed freely.

// Exclude a serialized contract from RENAMING, keep everything else renamed.
[Obfuscation(Feature = "renaming", Exclude = true, ApplyToMembers = true)]
public class Invoice
{
    public decimal Total { get; set; }
    public string Customer { get; set; }
}
Resolved by name — KEEP namesDTOs, reflection targets, XAML, public APIInternal logic — RENAME freelyeverything called directly in codeBoth still get:control-flow flatteningstring encryptionconstant masking — exclusion is name-only

The more robust fix: decouple the wire name

Excluding types works, but for serialization there is a better pattern that removes the fragility entirely: pin the serialized names explicitly so they never depend on the member name.

public class Invoice
{
    [JsonPropertyName("total")]    public decimal Total { get; set; }
    [JsonPropertyName("customer")] public string Customer { get; set; }
}

Now the JSON says total and customer no matter what the property is called in IL — rename Total to a and the wire format is unchanged. This decouples your contract from your code, which is good engineering independent of obfuscation (a plain C# rename refactor won”t silently alter your API either). The same thinking applies elsewhere: reference enum values by the enum type, not Enum.Parse on a string; resolve DI by explicit registration, not convention; prefer compiled bindings (x:Bind in UWP/WinUI) over string paths where the platform offers them.

Exclusion is name-level, not protection-level

The most important nuance: excluding a type from renaming does not leave it unprotected. Renaming is one transform among several. A DTO whose property names you preserve can still have its method bodies control-flow-flattened, its string literals encrypted and its constants masked. You are telling the obfuscator “don”t change these names,” not “don”t protect this code.” So the cost of doing exclusions correctly is essentially zero in protection terms — you keep full coverage on logic and give up renaming only on the handful of names the runtime looks up as strings.

A reliable workflow

Obfuscate, then run your real test suite against the obfuscated build — not just the un-obfuscated one. Name-resolution breakages are invisible at compile time and obvious the moment a serialization round-trip or a reflection path executes, so an integration test that serializes a DTO, hits an API, or exercises a plugin will catch them immediately. Fix each by excluding that specific contract (or pinning its wire names), never by disabling renaming wholesale. Do that once, and “obfuscation broke my app” becomes a one-time setup step rather than a recurring mystery — your logic stays hard to read, and the names your program depends on stay exactly where it expects them.

Try Nebula.NET

Harden your .NET code in minutes — start with the free edition.