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:
<DemeanorReport>true</DemeanorReport>
<DemeanorReportFile>$(TargetDir)$(TargetName)Report.json</DemeanorReportFile> 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. Common reasons:
| 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 | The name appears in a reflection lookup whose target Demeanor can resolve |
| name used in dynamic dispatch | The dynamic keyword resolves this member by name at runtime |
| public/protected visibility | Symbol is externally visible (use --include-publics to override) |
| runtime special name | .ctor, .cctor, and other runtime-reserved names |
| native/runtime implementation | Method body is provided by the runtime, not the assembly |
| virtual override of external method | Overrides a framework method whose name cannot rename |
| vararg calling convention | The runtime resolves vararg call sites by name |
| 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 reads the prior version’s report and reuses each existing symbol’s obfuscated name when that symbol still exists in the current assembly. New symbols get fresh names; removed symbols’ names are not reused, so cross-version data does not collide.
Adding new types and members
New symbols get fresh obfuscated names. Adding types or members in a new release does not change the obfuscated names of symbols that existed in the prior release.
Removing types and members
Removed symbols are absent from the new report. Their previous obfuscated names are not reused, preventing accidental name collisions that could break cross-version serialization.
MSBuild integration
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<!-- <Obfuscate>true</Obfuscate> not needed: auto-enabled on Release. -->
<DemeanorReport>true</DemeanorReport>
<DemeanorReportFile>$(TargetDir)$(TargetName)Report.json</DemeanorReportFile>
<DemeanorPriorReport>$(MSBuildProjectDirectory)\prior-report.json</DemeanorPriorReport>
</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