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) | Type, Method, Field, Property, Event |
renaming | Type, method, field, property, event, parameter name obfuscation | Type, Method, Field, Property, Event |
call-hiding | Reference proxy relay generation (hides call targets from static analysis) | Method |
string-encryption | XOR encryption of ldstr string operands | Method |
constant-encryption | Arithmetic encoding of integer constants (ldc.i4 / ldc.i8) | Method |
cfg | Control flow flattening, reordering, and opaque predicates | Method |
anti-debug | Debugger detection check injection at method entry points | Method, Type |
anti-tamper | PE file hash integrity verification in module initializer | Type |
enum-deletion | Removal of enum literal fields (breaks Enum.GetNames() / Enum.Parse()) | Type |
baml | WPF compiled XAML (BAML) type/namespace/binding patching | Type |
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, CFG flattening, call-hiding, and anti-debug injection. If you only want to exclude from renaming while keeping other protections active, use Feature = "renaming" explicitly.
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 that 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 CFG and Call-Hiding for Performance-Critical Code
[Obfuscation(Feature = "cfg,call-hiding", Exclude = true)]
public void HotLoopMethod(Span buffer)
{
// Tight inner loop — skip CFG flattening and proxy indirection
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 = "renaming", Exclude = true, ApplyToMembers = true)]
public class MostlyProtected
{
public string PublicApi() { ... } // NOT renamed (inherits type-level)
[Obfuscation(Feature = "renaming", 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)] 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 CFG 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 |
| CFG | 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, renaming, call-hiding, string-encryption,
constant-encryption, cfg, anti-debug, anti-tamper, enum-deletion, baml. 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 CFG 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.