Per-Feature Obfuscation Control
Starting with Demeanor v6.0.4, the standard [ObfuscationAttribute] supports fine-grained control over individual obfuscation features. Instead of excluding a symbol from all obfuscation, you can target specific features — leave renaming active but skip string encryption for one method, or disable call-hiding on a class while keeping everything else.
The attribute is part of the .NET runtime (System.Reflection.ObfuscationAttribute) — no Demeanor-specific references needed in your source code.
Attribute Properties
| Property | Type | Default | Description |
|---|---|---|---|
Exclude | bool | true | When true, the named feature is disabled for this symbol. When false, the feature is forced on — overriding a global --no-* CLI flag. |
Feature | string | "all" | Which obfuscation feature(s) to control. Comma-separated for multiple features. See table below. |
ApplyToMembers | bool | true | When on a type, cascades the directive to all methods, fields, properties, and events. |
StripAfterObfuscation | bool | true | Removes the attribute from the output assembly. Set to false to preserve it for downstream tools. |
Supported Features
| Feature Name | What It Controls | Applies To |
|---|---|---|
all | Every obfuscation feature (renaming + all transforms). Alias: default. | Type, Method, Field, Property, Event |
anti-debug | Runtime debugger detection | Method, Type |
anti-tamper | Detection of post-build modification of the assembly | Type |
baml | WPF compiled XAML (BAML) consistency with renamed types | Type |
call-hiding | Hides call targets from static call-graph analysis | Method |
cfg | Makes the order of operations inside a method unreadable in a decompiler | Method |
constant-encryption | Hides numeric literals from decompilers | Method |
enum-deletion | Removes enum members so decompilers see empty enums (breaks Enum.GetNames() / Enum.Parse()) | Type |
rename | Type, method, field, property, event, parameter name obfuscation. Local decision — does not propagate. v6.0.10+ | Type, Method, Field, Property, Event |
rename-propagate | Renaming with the Exclude decision propagated to all derived types (in this assembly and others). v6.0.10+ | Type |
string-encryption | Removes literal text from the shipped file | Method |
Feature names are case-insensitive. Unknown feature names produce a build warning.
Important: Feature="all" Scope
When Feature is omitted (or set to "all"), the directive applies to every obfuscation feature — not just renaming. This means [Obfuscation(Exclude = true)] on a method excludes it from renaming, string encryption, constant encryption, control-flow obfuscation, call hiding, and anti-debug. If you only want to exclude from renaming while keeping other protections active, use Feature = "rename" explicitly.
anti-debug
Causes the application to exit when a managed or native debugger is attached at runtime. Applies to methods and types (type-level applies to all methods in the type). Exclude diagnostic services, test infrastructure, and any code that intentionally runs under a debugger.
anti-tamper
Causes the application to terminate immediately if the obfuscated assembly is modified after the build. Applies to types. Incompatible with pipelines that alter the binary after Demeanor runs (other than strong-name signing) — exclude those types or disable the option.
baml
Keeps WPF compiled XAML (BAML) resources embedded in the assembly consistent with any renamed types, namespaces, and binding paths. Applies to types. Demeanor enables this automatically for WPF assemblies — use Exclude = true on a specific type to suppress BAML adjustment for that type alone.
call-hiding
Hides the real target of calls from static analysis tools and decompilers. Applies to methods only. Adds a small runtime cost — exclude performance-critical hot loops where the overhead is measurable.
cfg
Makes the order of operations inside a method unreadable in a decompiler. Applies to methods. Increases assembly size — exclude tight inner loops or any method where the original branch layout is required for correctness or performance.
constant-encryption
Hides numeric literals (magic numbers, buffer sizes, protocol values) from decompilers by replacing each with an equivalent expression that produces the same value at runtime. Applies to methods. Exclude if a method’s constants are matched by external runtime checks or interop contracts outside your control.
enum-deletion
Removes enum members from the shipped assembly so decompilers see empty enum bodies. Applies to types. Exclude any enum used with Enum.GetNames(), Enum.Parse(), Enum.ToString(), or any reflection-based serializer that resolves enum members by name.
rename
Obfuscates the symbol’s name in the output assembly. Applies to types, methods, fields, properties, events, and parameters. Use Exclude = true to preserve a name — required for types accessed by reflection, serialized by name, or exported as part of a public API. Use Exclude = false to force renaming even when the global --no-rename CLI flag is set or an enclosing type-level directive would prevent it.
The rename feature is local: the decision applies only to the symbol it’s declared on, not to derived types. For a hierarchical (inheritable) rename decision, use rename-propagate.
string-encryption
Removes literal text (connection strings, API keys, error messages) from the shipped binary so it is not visible to a hex editor or static search. Applies to methods. Exclude when a method builds strings that must remain human-readable in crash dumps, log output, or diagnostic tooling.
Examples
Exclude a Type from All Obfuscation
[Obfuscation(Exclude = true, ApplyToMembers = true)]
public class MyApiResponse
{
public string Status { get; set; }
public string Message { get; set; }
} Skip String Encryption for One Method
Useful when a method builds dynamic SQL or log messages whose strings you want readable in crash dumps.
public class DataAccess
{
[Obfuscation(Feature = "string-encryption", Exclude = true)]
public string BuildConnectionString(string server, string db)
{
return $"Server={server};Database={db};Trusted_Connection=true";
}
} Skip Control-Flow Obfuscation and Call Hiding for Performance-Critical Code
[Obfuscation(Feature = "cfg,call-hiding", Exclude = true)]
public void HotLoopMethod(Span buffer)
{
// Tight inner loop — skip control-flow obfuscation and call hiding
for (int i = 0; i < buffer.Length; i++)
buffer[i] ^= 0xAA;
} Preserve Enum Members from Deletion
Enum deletion removes named constants so decompilers show empty enums. Exclude it when your code uses Enum.GetNames(), Enum.Parse(), or Enum.ToString() on the enum.
[Obfuscation(Feature = "enum-deletion", Exclude = true)]
public enum LogLevel
{
Debug, Info, Warning, Error, Critical
} Skip Anti-Debug for an Entire Class
// Type-level with ApplyToMembers: no debug checks injected into any method
[Obfuscation(Feature = "anti-debug", Exclude = true, ApplyToMembers = true)]
public class DiagnosticService
{
public void CollectMetrics() { ... }
public void DumpState() { ... }
} Force a Feature ON When Globally Disabled
CLI flags like --no-call-hiding disable a feature globally. An attribute with Exclude = false overrides the global setting for that specific member.
// Global: demeanor MyApp.dll --no-call-hiding
// This one method still gets call-hiding despite the global flag:
[Obfuscation(Feature = "call-hiding", Exclude = false)]
public void LicenseCheck()
{
// Call targets in this method are hidden even though
// --no-call-hiding disabled it everywhere else
} Member Overrides Type-Level Directive
A member-level attribute takes precedence over a type-level ApplyToMembers directive for the same feature.
// Type-level: exclude all members from renaming
[Obfuscation(Feature = "rename", Exclude = true, ApplyToMembers = true)]
public class MostlyProtected
{
public string PublicApi() { ... } // NOT renamed (inherits type-level)
[Obfuscation(Feature = "rename", Exclude = false)]
public string InternalHelper() { ... } // IS renamed (member overrides type)
} Multiple Attributes on One Member
You can apply multiple [Obfuscation] attributes to the same member, each targeting a different feature. Alternatively, use comma-separated feature names in a single attribute.
// Two attributes — equivalent to Feature="cfg,call-hiding"
[Obfuscation(Feature = "cfg", Exclude = true)]
[Obfuscation(Feature = "call-hiding", Exclude = true)]
public void PerformanceCritical() { ... } Assembly-Level Directives
Apply [assembly: Obfuscation(...)] to control a feature for the entire assembly — the attribute-level equivalent of --no-* CLI flags. Multiple assembly-level attributes are supported.
// Disable string encryption assembly-wide
[assembly: Obfuscation(Feature = "string-encryption", Exclude = true)]
// Disable constant encryption assembly-wide
[assembly: Obfuscation(Feature = "constant-encryption", Exclude = true)] Inheritable Renaming via Feature="rename-propagate"
Demeanor v6.0.10 adds support for hierarchical rename decisions using the standard System.Reflection.ObfuscationAttribute. When Feature="rename-propagate" is set on a type, the type's Exclude decision is inherited by all derived types — in this assembly and in every other assembly in the obfuscation set — eliminating the need to manually apply [Obfuscation] to every class in a public API subtree. No custom attributes are required.
The name reflects what the feature does: it's a rename decision (type-level only) that propagates down the base-type chain. Compare to rename, the local-only rename directive.
What rename-propagate Does
When applied to a type, Demeanor propagates the attribute's Exclude value to all derived types:
Exclude = true— the type and all descendants are excluded from renaming.Exclude = false— the type and all descendants are not excluded (escape hatch).
Only the Exclude decision is inherited. All other feature strings (nocfg, string-encryption, rename, etc.) remain local to the type where they appear.
Examples
Excluding an Entire Type Hierarchy
[Obfuscation(Exclude = true, Feature = "rename-propagate", ApplyToMembers = true)]
public abstract class PublicApiBase
{
public virtual string GetDescription() => "...";
}
// No attribute needed — exclusion is inherited
public class ConcreteImpl : PublicApiBase
{
public override string GetDescription() => "...";
} Result: PublicApiBase, ConcreteImpl, and all other subclasses are excluded from renaming.
Escape Hatch: Re-Enable Renaming for a Subclass
[Obfuscation(Exclude = false, Feature = "rename-propagate")]
internal class InternalDetail : PublicApiBase
{
} Result: InternalDetail is not excluded, and its descendants inherit this non-exclusion.
Subclass of an Escape Hatch
public class Leaf : InternalDetail
{
} Result: Leaf is not excluded — it inherits the nearest propagated decision (from InternalDetail).
Propagation Rules
Demeanor determines the effective exclusion for a type T using this precedence:
| Priority | Source | Effect |
|---|---|---|
| 1 (highest) | Local [Obfuscation(Feature="rename-propagate")] on T | T's own Exclude value wins. |
| 2 | Nearest ancestor with rename-propagate | That ancestor's Exclude value becomes T's effective decision. |
| 3 (lowest) | No ancestor has rename-propagate | Standard [Obfuscation] rules apply. |
Member Behavior
ApplyToMembers behaves exactly as documented for the standard attribute: if the attribute is local to the type, ApplyToMembers controls whether that type's members are excluded. Propagation of Exclude does not propagate member-level settings — descendant types' members are governed by their own attributes and existing precedence rules.
Interaction with Other Features
rename-propagate affects only the rename decision. Other features do not inherit via propagation — each remains local to the type where it appears:
anti-debug,anti-tamper,bamlcall-hiding,cfgconstant-encryption,enum-deletionrename,string-encryption, and any other feature string
Diagnostics
Demeanor reports propagation decisions in the build log:
Type MyNamespace.ConcreteImpl excluded via rename-propagate from base MyNamespace.PublicApiBase
Type MyNamespace.InternalDetail opted out via local rename-propagate (Exclude=false) Precedence Rules
For every method, field, property, or event that Demeanor processes, it asks: should feature X run on this member? The answer comes from the first matching rule in this chain — most specific wins:
| Priority | Source | Scope | Example |
|---|---|---|---|
| 1 (highest) | Member-level [Obfuscation] | One method, field, property, or event | [Obfuscation(Feature = "cfg", Exclude = false)] on a method |
| 2 | Type-level [Obfuscation] with ApplyToMembers = true | All members of one type | [Obfuscation(Feature = "anti-debug", Exclude = true, ApplyToMembers = true)] on a class |
| 3 | Assembly-level [assembly: Obfuscation] | Every type and member in the assembly | [assembly: Obfuscation(Feature = "string-encryption", Exclude = true)] |
| 4 (lowest) | CLI flag / MSBuild property | All assemblies in the obfuscation run | --no-call-hiding or <DemeanorNoCallHiding>true</DemeanorNoCallHiding> |
The rule: Demeanor walks the chain from priority 1 to 4. At each level, if a directive exists that matches the queried feature, its Exclude value decides: true = skip, false = run. If no directive matches at that level, Demeanor checks the next level. If nothing matches at any level, the feature runs (the default is "enabled").
Worked Example
Consider this setup:
// CLI: demeanor MyApp.dll --no-call-hiding
// Assembly-level: disable string encryption
[assembly: Obfuscation(Feature = "string-encryption", Exclude = true)]
// Type-level: disable control-flow obfuscation for all members
[Obfuscation(Feature = "cfg", Exclude = true, ApplyToMembers = true)]
public class PaymentService
{
// Member-level: force call-hiding ON despite the global --no-call-hiding
[Obfuscation(Feature = "call-hiding", Exclude = false)]
public void ProcessPayment() { ... }
public void GetBalance() { ... }
} | Feature | ProcessPayment | GetBalance | Why |
|---|---|---|---|
| Renaming | Runs | Runs | No directive at any level disables it |
| Call-hiding | Runs | Skipped | ProcessPayment has member-level Exclude=false (priority 1 overrides CLI). GetBalance has no member/type/assembly directive → falls to CLI --no-call-hiding (priority 4) |
| String encryption | Skipped | Skipped | Assembly-level Exclude=true (priority 3) — no member or type directive overrides it |
| Control-flow obfuscation | Skipped | Skipped | Type-level ApplyToMembers=true (priority 2) applies to both methods |
| Constant encryption | Runs | Runs | No directive at any level disables it |
| Anti-debug | Runs | Runs | No directive at any level disables it |
The key insight: Exclude = false at any level overrides a higher-numbered (lower-priority) disable. This lets you disable a feature globally for safety, then selectively re-enable it on the specific members that need maximum protection.
Validation Warnings
Demeanor validates every [Obfuscation] attribute at obfuscation time and warns about:
- Unknown feature names — typos or features from other obfuscators produce a warning listing the known feature names.
- Misapplied features — applying a method-only feature (like
call-hiding) to a field produces a warning explaining which member types the feature applies to.
Warnings appear in the obfuscation output and MSBuild build log. They do not stop obfuscation — the misapplied attribute is silently ignored for the inapplicable feature.
WARNING: [Obfuscation(Feature="call-hiding")] on 'MyApp.Config.SomeField':
'call-hiding' applies to Method, not Field.
WARNING: [Obfuscation] on 'MyApp.Service.DoWork': unknown Feature 'obfuscate'.
Known features: all, anti-debug, anti-tamper, baml, call-hiding, cfg,
constant-encryption, default, enum-deletion, rename, rename-propagate,
string-encryption. Best Practices
- Start with defaults. Most code obfuscates correctly with zero attributes. Add attributes only when you observe a specific issue.
- Use
demeanor auditfirst. The audit identifies which types and members Demeanor auto-protects. Many exclusions you'd manually add are already handled. - Prefer attributes over CLI flags. Attributes live next to the code they protect — they survive refactoring and are self-documenting. CLI flags are for broad strokes; attributes are for surgical precision.
- Use
FeatureoverExclude = truealone.[Obfuscation(Exclude = true)]disables all obfuscation.[Obfuscation(Feature = "cfg", Exclude = true)]disables only control-flow obfuscation while keeping renaming, string encryption, and everything else active. - Use comma-separated features rather than multiple attributes when excluding several features from the same member.