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.
Preserve names required by reflection
From 1.44.0, assemblies referencing WPF's PresentationFramework or containing .g.resources keep type/member names. XAML/BAML and binding paths cannot be safely updated by ordinary IL renaming, so this deliberately trades name obfuscation for compatibility; other selected IL transformations remain enabled. Actual Windows WPF checks cover loose XAML binding, compiled BAML loading and property-change updates. Test your full application: custom controls, converters and third-party UI frameworks can add further contracts.
Dependency-injection checks use Microsoft.Extensions.DependencyInjection against the protected assembly: interface-based scanning, service resolution, constructor injection and ActivatorUtilities activation pass with basic and advanced options. Type names embedded in configuration still require explicit preservation unless one of the safeguards documented here applies.
From 1.43.0, a type with a matching embedded Full.Type.Name.resources keeps its type/member names and resource name, so fixed ResourceManager base names remain valid. Arbitrary resource/configuration text is not rewritten automatically. For type names in appsettings.json, XML or plug-in manifests, use the explicit renaming exclusion below on the configured type and required members (and containing types for nested names).
Strong-name signed and delay-signed assemblies are rejected rather than returned with an invalid signature. Upload an unsigned build, test the protected artifact, then sign it in your own trusted build pipeline. Re-signing with the original identity needs the appropriate original key and correct build tooling; this service does not accept private signing keys or provide hosted re-signing.
From 1.42.0, assemblies declaring InternalsVisibleTo retain their original type and member names. A friend DLL can reference internal metadata names, and independent uploads cannot rewrite those callers together. This deliberately reduces name obfuscation for that assembly; selected string encryption, control flow and reference proxies still run. Remove unnecessary friend declarations before rebuilding if you want ordinary name renaming.
Verified with two separately compiled DLLs: an unchanged caller and a protected dependency, then both DLLs protected independently. Generic calls, asynchronous continuations and caught asynchronous exceptions execute correctly in an isolated load context. Keep the original assembly filenames and deploy all required dependencies; this is not a DLL-merging or dependency-bundling feature.
From 1.41.0, names marked with JsonInclude, JsonPropertyName or DataMember are preserved automatically, as are DataContract type names. This prevents marked internal fields and default XML contract names from changing. Unmarked models, custom serializers and nested containing-type names can still require explicit exclusions.
Compatibility checks run on protected assemblies: old JSON payloads round-trip with their original field names, DataContract XML keeps its original type/member names, inherited interfaces and explicit implementations still dispatch, and a separately protected plugin loads against an unchanged host interface. These cases pass with renaming alone and with string encryption, control flow and reference proxies enabled. They do not establish compatibility with every plug-in framework or serializer.
From version 1.40.0, FreeObfuscator recognizes targeted renaming exclusions in your compiled assembly. Put this standard attribute on a type whose name and member names must stay stable, then rebuild before uploading:
[System.Reflection.Obfuscation(Exclude = true, Feature = "renaming", ApplyToMembers = true)]
internal sealed class ReflectionModel
{
public string Name { get; set; }
}
Use ApplyToMembers = false to keep only the type name. You can also put the attribute directly on a field, property or method. An excluded property's getter and setter names are preserved too. For a nested type, preserve its containing types if callers need the complete original name.
This support controls renaming only. Other selected transformations still run; it does not implement competitor-specific feature expressions, assembly-wide rules or re-inclusion overrides. Omitted Feature (or all) is treated as a renaming exclusion here, not a promise to disable every transformation. Use the explicit renaming example above.
Validated with an actual protected .NET DLL: string-based type lookup, reflected private field/property writes and reflected method invocation returned the original results with string encryption, control flow and reference proxies enabled. Unmarked private members still changed names. Test your own reflection, JSON/XML contracts, plug-in loading and UI bindings against the protected artifact before release.
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.