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

PropertyTypeDefaultDescription
ExcludebooltrueWhen true, the named feature is disabled for this symbol. When false, the feature is forced on — overriding a global --no-* CLI flag.
Featurestring"all"Which obfuscation feature(s) to control. Comma-separated for multiple features. See table below.
ApplyToMembersbooltrueWhen on a type, cascades the directive to all methods, fields, properties, and events.
StripAfterObfuscationbooltrueRemoves the attribute from the output assembly. Set to false to preserve it for downstream tools.

Supported Features

Feature NameWhat It ControlsApplies To
allEvery obfuscation feature (renaming + all transforms)Type, Method, Field, Property, Event
renamingType, method, field, property, event, parameter name obfuscationType, Method, Field, Property, Event
call-hidingReference proxy relay generation (hides call targets from static analysis)Method
string-encryptionXOR encryption of ldstr string operandsMethod
constant-encryptionArithmetic encoding of integer constants (ldc.i4 / ldc.i8)Method
cfgControl flow flattening, reordering, and opaque predicatesMethod
anti-debugDebugger detection check injection at method entry pointsMethod, Type
anti-tamperPE file hash integrity verification in module initializerType
enum-deletionRemoval of enum literal fields (breaks Enum.GetNames() / Enum.Parse())Type
bamlWPF compiled XAML (BAML) type/namespace/binding patchingType

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:

PrioritySourceScopeExample
1 (highest)Member-level [Obfuscation]One method, field, property, or event[Obfuscation(Feature = "cfg", Exclude = false)] on a method
2Type-level [Obfuscation] with ApplyToMembers = trueAll members of one type[Obfuscation(Feature = "anti-debug", Exclude = true, ApplyToMembers = true)] on a class
3Assembly-level [assembly: Obfuscation]Every type and member in the assembly[assembly: Obfuscation(Feature = "string-encryption", Exclude = true)]
4 (lowest)CLI flag / MSBuild propertyAll 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() { ... }
}
FeatureProcessPaymentGetBalanceWhy
RenamingRunsRunsNo directive at any level disables it
Call-hidingRunsSkippedProcessPayment 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 encryptionSkippedSkippedAssembly-level Exclude=true (priority 3) — no member or type directive overrides it
CFGSkippedSkippedType-level ApplyToMembers=true (priority 2) applies to both methods
Constant encryptionRunsRunsNo directive at any level disables it
Anti-debugRunsRunsNo 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 audit first. 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 Feature over Exclude = true alone. [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.