Obfuscation works both ways. Once your members are renamed, a crash report from a user
comes back full of names like a.b(c d) instead of
OrderService.Submit(Order order). The rename map is how you read those
reports again — without shipping the readable names to anyone.
Why the trace is unreadable
A .NET stack trace is built from the method names in the assembly's metadata. After obfuscation those names are the renamed ones, so the exception that reaches your logs, your error reporter or a user's screenshot is written in the obfuscated alphabet:
System.NullReferenceException: Object reference not set to an instance of an object.
at d.a(e f)
at d.b()
at m.n(String s)
That is working as intended — a reader of your shipped binary gets the same uninformative trace. The problem is that so do you. Without a way back, every production crash is a guessing game.
The rename map
When you obfuscate on Pro you can ask for a rename map — a
.map.txt file that records, for that specific build, which original name
became which obfuscated one. It is the private key to your own crash reports:
OrderService.Submit(Order) -> d.a(e)
OrderService.Validate() -> d.b()
PaymentGateway.Charge(String) -> m.n(String)
Keep it somewhere safe alongside your build artifacts — the same place you would keep the PDBs for a release. One map belongs to exactly one build, so tag it with the version.
Decoding a trace
We ran this on a small console app, not an illustration. The app crashes with a null reference inside a three-call chain. Obfuscated with renaming, the protected build crashes the same way, and the trace reads like this:
System.NullReferenceException: Object reference not set to an instance of an object.
at @e.@b(String @b)
at @d.@c(@c @b)
at @d.@b(@c @b)
at @f.Main()
Paste that trace and the matching map into the stack-trace decoder. The decoded output:
System.NullReferenceException: Object reference not set to an instance of an object.
at Shop.PaymentGateway.Charge(String @b)
at Shop.OrderService.Validate(@c @b)
at Shop.OrderService.Submit(@c @b)
at Shop.Program.Main()
Be clear about what comes back. The decoder restores each method's name and its
owning type, so the call chain is readable. It does not restore parameter
names or parameter types (@b and @c stay), and it does not
add line numbers. Line numbers come from the PDB of the unobfuscated build, which is
why you keep that build archived (see below).
The decoder runs on our server: you post the map and the trace over HTTPS, the decode is computed, and nothing is stored. If you do not want a map to leave your machine, do not use the web decoder for that map.
Fitting it into a real support workflow
-
Version every build. Put the version in the assembly and in the
map filename, so an incoming crash from
2.4.1is decoded with the2.4.1map and not a neighbour. - Store maps with releases. Your CI artifact store or release bucket is the natural home. If you obfuscate from a pipeline, capture the map as a build output in the same step — see obfuscating in CI/CD.
- Do not ship the map. It never goes near the client. Shipping it would hand a reader the exact thing obfuscation removed.
- Keep a readable build for yourself. The map covers names; for line numbers you still want the unobfuscated build and its PDBs archived.
How this compares
Commercial suites such as SmartAssembly bundle this into automated error reporting — incoming crashes are de-obfuscated for you against a stored mapping, which is genuinely convenient and priced accordingly. Our approach is deliberately smaller and more private: you hold the map, nothing is stored, and you decode on demand. If you want the full managed error-reporting pipeline, that comparison lays out the trade-off honestly. If you want to read your own crashes without a mapping sitting on someone else's server, this is the lighter way to do it.