Reports & Incremental Obfuscation
Enterprise feature. Generate detailed obfuscation reports for auditing, stack trace decoding, and incremental builds.
Generating a Report
demeanor --report --report-file MyAppReport.json MyApp.dll Or in MSBuild:
<ObfuscateReport>true</ObfuscateReport>
<ObfuscateReportFile>$(TargetDir)$(TargetName)Report.json</ObfuscateReportFile> The report is a JSON file containing every type and member in the obfuscated assembly. For each symbol, it records:
- Renamed symbols: original name → obfuscated name
- Excluded symbols: original name + reason why it was not renamed
Report Schema
Each type in the report contains lists of methods, fields, properties, and events. Each symbol has mutually exclusive fields:
// Renamed symbol:
{ "name": "GetCount", "renamed": "a", "accessibility": "public" }
// Excluded symbol:
{ "name": ".ctor", "excludedReason": "runtime special name", "accessibility": "public" }
// Type:
{
"name": "MyApp.PricingEngine",
"renamed": "b",
"visibility": "public",
"methods": [ ... ],
"fields": [ ... ]
} Exclusion Reasons
The excludedReason field explains why a symbol was not renamed:
| Reason | Meaning |
|---|---|
| category disabled | A --no-types, --no-methods, etc. flag disabled this category |
| excluded by --exclude or --xr | Matched a command-line exclusion pattern |
| excluded by [Obfuscation] attribute | The source code has [Obfuscation(Exclude = true)] |
| name used in string-based reflection | Detected GetMethod("Name") or similar pattern in IL |
| name used in dynamic dispatch | Detected DLR binder usage (dynamic keyword) |
| public/protected visibility | Symbol is externally visible (use --include-publics to override) |
| runtime special name | .ctor, .cctor, or RTSpecialName flag |
| native/runtime implementation | InternalCall, Native, or Runtime method |
| virtual override of external method | Overrides a framework method (e.g., Object.ToString) |
| vararg calling convention | CLI uses name-based lookup for vararg call sites |
| COM interop type | [ComVisible] or [ComImport] — COM uses name-based dispatch |
| serializable type | --no-serializable flag active |
| enumeration type | --no-enumerations flag active |
| compiler-generated type | Async state machine, lambda closure, etc. |
Incremental Obfuscation
Incremental obfuscation preserves name mappings across versions. When you update your application, existing symbols keep their same obfuscated names while new symbols get new names.
Why use incremental obfuscation?
- Serialization compatibility: Data serialized with v1's obfuscated type and field names must deserialize correctly in v2. Without incremental mode, a new type inserted before existing types shifts all obfuscated names.
- Plugin stability: External code may reference obfuscated names stored in configuration files, databases, or DI containers.
- Smaller patches: Unchanged symbols produce identical metadata bytes, keeping binary diffs small for update distribution.
Workflow
# v1: Initial obfuscation with report
demeanor --report --report-file v1-report.json MyApp.dll
# v2: Incremental obfuscation using v1's report
demeanor --prior-report v1-report.json --report --report-file v2-report.json MyApp.dll
# v3: Chain continues — always use the most recent report
demeanor --prior-report v2-report.json --report --report-file v3-report.json MyApp.dll How it works
- Demeanor loads the prior report and builds a name reservation map: original name → prior obfuscated name.
- For each symbol in the current assembly, Demeanor checks if it exists in the prior map.
- If found: the prior obfuscated name is reserved in the naming scope and reused.
- If not found (new symbol): a new obfuscated name is generated, skipping reserved names.
- If a prior name conflicts (e.g., a non-renamable symbol now occupies the prior name): Demeanor assigns a new name and emits a warning.
Adding new types and members
New symbols get fresh obfuscated names. Even if a new type is declared before existing types in source code (shifting TypeDef indices in metadata), the prior report ensures existing symbols keep their original obfuscated names. This is the core problem incremental mode solves — without it, index shifts cause all names to change.
Removing types and members
Removed symbols are simply absent from the new report. Their prior obfuscated names are reserved in the naming scope but never assigned, preventing accidental reuse that could cause cross-version name collisions.
MSBuild integration
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<Obfuscate>true</Obfuscate>
<ObfuscateReport>true</ObfuscateReport>
<ObfuscateReportFile>$(TargetDir)$(TargetName)Report.json</ObfuscateReportFile>
<ObfuscatePriorReport>$(MSBuildProjectDirectory)\prior-report.json</ObfuscatePriorReport>
</PropertyGroup> Store the report in source control alongside your release. Before each release, copy the current report to prior-report.json and rebuild.
CI/CD workflow
# GitHub Actions example
- name: Download prior report
uses: actions/download-artifact@v4
with:
name: obfuscation-report
path: prior-report.json
continue-on-error: true # First build has no prior report
- name: Build and obfuscate
env:
DEMEANOR_LICENSE: ${{ secrets.DEMEANOR_LICENSE }}
run: dotnet build -c Release
- name: Upload report for next build
uses: actions/upload-artifact@v4
with:
name: obfuscation-report
path: bin/Release/net10.0/MyAppReport.json