Exclusions Guide

Most .NET code obfuscates without any configuration. Demeanor analyzes your IL at obfuscation time and automatically preserves symbols that would break if renamed.

Obfuscation and reflection are fundamentally in tension: obfuscation renames symbols, while reflection resolves them by name at runtime. Demeanor bridges this gap with built-in intelligence that detects and adapts to the most common reflection and binding patterns automatically. For frameworks not covered by auto-detection, Demeanor provides simple exclusion mechanisms ([Obfuscation] attributes, --exclude, regex patterns) that let you protect specific symbols while keeping everything else fully obfuscated.

What Works Out of the Box

The following frameworks have been tested against Demeanor and work with zero configuration:

  • Entity Framework Core — Demeanor auto-detects EF Core usage and preserves the entity types and properties it depends on. All CRUD operations, navigation properties, and queries work after obfuscation.
  • Console and CLI applications — no reflection-sensitive patterns; fully obfuscated.
  • gRPC / Protobuf — generated code uses field numbers, not names.
  • NativeAOT — obfuscation runs on IL before AOT compilation.

Automatic Detection

Demeanor's static analysis scans every method body in your assembly and automatically preserves symbols that would break if renamed:

  • Entry pointsMain methods, module initializers
  • Public/protected API — preserved in library DLLs (unless --include-publics)
  • LINQ and ORM lambdas — properties referenced through expression trees and Fluent configuration
  • Data-binding patterns — properties bound by the common desktop and web-component UI frameworks
  • Component parameters, dependency injection, and routing — preserved for the frameworks that resolve them by name
  • Serialization contracts — DTOs and members used by the common JSON, XML, WCF, and binary serializers
  • Reflection string patterns — string-based type, method, and member lookups whose target can be resolved at analysis time
  • COM interop — types and members exposed to COM
  • Dynamic dispatch — DLR call sites
  • Compiler-generated types — async state machines, lambda closures

This detection runs at obfuscation time with zero runtime overhead — no agents, no instrumentation, no phone-home. Run demeanor audit to see exactly what was preserved in your assemblies.

How the Audit Classifies Findings

Most .NET obfuscators leave you to discover reflection and serialization pitfalls at runtime — broken in production, days of debugging obfuscated stack traces. Demeanor's pre-obfuscation audit finds them before anything is rewritten, and groups every finding into one of four categories. Run demeanor audit on your assembly and you'll see exactly this taxonomy in the report — no other .NET obfuscator ships this today.

Auto-handled — no action needed

Demeanor detects and preserves these automatically. The audit reports that it found them so you aren't surprised when the rename percentages look modest.

  • Common JSON, XML, WCF, and binary serialization patterns
  • Data-binding patterns used by desktop and web-component UIs
  • Component parameters, dependency injection, and routing
  • String-based reflection whose target can be resolved at analysis time
  • DLR / dynamic dispatch
  • COM interop

Needs an exclusion — your decision

Demeanor can't prove the symbol is safe to rename on its own. The audit explains the risk and points to the fix; you choose whether to apply [Obfuscation(Exclude = true)], add a CLI --exclude pattern, or accept the rename because you know the caller.

  • Realtime hub methods invoked by name from clients
  • Controllers and endpoints routed or resolved by name
  • ORM contexts and entity types resolved by reflection
  • Configuration binding to options types
  • Plugin / extensibility contracts
  • RPC service contracts
  • Compiled-XAML data bindings
  • Handler return types whose serialization contract isn't declared

Needs a code change

A small source edit is the cleanest fix. A typical example: a handler returns a DTO but the DTO isn't registered with the app's source-generated JSON context. One line added to the context resolves both the rename advisory and any Native AOT warning at the same time.

Advisory — informational

The obfuscation itself would tolerate the pattern, but it's worth knowing about — a reflection-path API call that breaks Native AOT, a public DTO that isn't in the source-gen JSON context, etc. No action required to obfuscate; acting on the advisory improves the codebase.

See Getting Started, Step 2 for a real audit run and how to read each section.

Exclusion Mechanisms

[Obfuscation] Attribute (recommended)

The standard .NET attribute for controlling obfuscation. Works with any ECMA-335 compliant obfuscator.

// Exclude a single type
[Obfuscation(Exclude = true)]
public class MyApiResponse { ... }

// Exclude a type and all its members
[Obfuscation(Exclude = true, ApplyToMembers = true)]
public class MyDataContract { ... }

// Exclude a single member
public class Settings
{
    [Obfuscation(Exclude = true)]
    public string ConnectionString { get; set; }
}

Name Exclusions (--exclude)

demeanor --exclude "MyApp.Models.ApiResponse" MyApp.dll

MSBuild (one Include per type, inside an <ItemGroup>):

<ItemGroup>
  <DemeanorExclude Include="MyApp.Models.ApiResponse" />
  <DemeanorExclude Include="MyApp.Services.PaymentService" />
</ItemGroup>

Regex Exclusions (--xr)

demeanor --xr ".*ViewModel$" --xr ".*Controller$" MyApp.dll

MSBuild (one Include per pattern, inside an <ItemGroup>):

<ItemGroup>
  <DemeanorExcludeRegex Include=".*ViewModel$" />
  <DemeanorExcludeRegex Include=".*Controller$" />
</ItemGroup>

Category Exclusions

Disable renaming for entire symbol categories: --no-types, --no-methods, --no-fields, --no-properties, --no-events, --no-parameters, --no-enumerations, --no-resource-names.

Framework Guide: Tested Results

Every example below was tested by building a real sample app, obfuscating it with Enterprise-tier Demeanor, and verifying the result. Sample apps are in the samples/ directory of the Demeanor repository.

Entity Framework Core — Just Works

EF Core uses expression tree lambdas for model configuration and queries. Demeanor's static analysis auto-detects EF Core usage and preserves the entity types and properties it depends on. No exclusions needed.

YOUR CODE
public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
    public List<Order> Orders { get; set; }
}

// Fluent API uses expression trees
modelBuilder.Entity<Customer>(e =>
{
    e.HasKey(c => c.Id);
    e.Property(c => c.Name).HasMaxLength(200);
    e.HasMany(c => c.Orders)
     .WithOne(o => o.Customer);
});
AFTER OBFUSCATION
// Entity types PRESERVED automatically:
// EfCoreSample.Customer (not renamed)
// EfCoreSample.Order (not renamed)
// EfCoreSample.OrderItem (not renamed)
//
// Properties preserved: Id, Name, Email,
//   Orders, Customer, OrderDate, Total, etc.
//
// Internal methods, fields, and
// non-entity types: fully obfuscated.
//
// Result: app runs identically.
// Zero exclusions needed.

Tested with samples/EfCoreSample using SQLite in-memory database. All CRUD operations, navigation properties, LINQ queries, and enum filtering work after obfuscation with zero exclusions.

WPF / MVVM — Properties Preserved, Types Need Exclusion

Demeanor auto-detects data-binding patterns and preserves the bound properties. However, XAML's x:Class directive references types by fully-qualified name in compiled BAML — type renaming breaks this.

YOUR CODE
public class CustomerViewModel
    : INotifyPropertyChanged
{
    public string Name { get; set; }
    public string Email { get; set; }
    public string Summary => ...;
    public ICommand SaveCommand => ...;
}

<!-- XAML binding -->
<TextBox Text="{Binding Name}" />
<Button Command="{Binding SaveCommand}" />
AFTER OBFUSCATION
// Properties PRESERVED automatically:
//   Name, Email, Summary, SaveCommand
//   (0 of 4 properties renamed)
//
// Types renamed: 6 of 6 (100%)
// Fields renamed: 6 of 6 (100%)
// Methods renamed: 21 of 25 (84%)
//
// Problem: XAML x:Class="WpfSample
//   .MainWindow" references the old
//   type name — now renamed to "d".
//   The app won't load the Window.

Solution (permanent fix): mark Window and UserControl classes in source:

[Obfuscation(Exclude = true, ApplyToMembers = false)]
public partial class MainWindow : Window { ... }

If you cannot modify source, exclude them from the CLI:

demeanor --xr ".*Window$" --xr ".*UserControl$" --include-publics MyWpfApp.dll

Or in MSBuild:

<ItemGroup>
  <DemeanorExcludeRegex Include=".*Window$" />
  <DemeanorExcludeRegex Include=".*UserControl$" />
</ItemGroup>

Data binding properties are automatically preserved. Only the type names need exclusion because BAML references them by string name. Internal logic, fields, and non-UI types are fully obfuscated.

ASP.NET Core / Minimal API — Routes Work, DTOs Need Attention

Minimal API routes, middleware, and dependency injection all survive obfuscation — they use delegates and types, not string names. The concern is JSON-serialized response types: System.Text.Json resolves property names via reflection.

YOUR CODE
app.MapGet("/weather", () =>
    new WeatherForecast(
        DateOnly.FromDateTime(DateTime.Now),
        22, "Warm"));

public record WeatherForecast(
    DateOnly Date,
    int TemperatureC,
    string? Summary)
{
    public int TemperatureF =>
        32 + (int)(TemperatureC / 0.5556);
}
AFTER OBFUSCATION
// Routes work: MapGet delegates survive
// DI works: GreetingService injected
//
// Problem: WeatherForecast properties
// renamed. JSON output changes from:
//   {"date": "...", "temperatureC": 22}
// to:
//   {"a": "...", "b": 22}
//
// API consumers break.

Solution: Use [JsonPropertyName] to fix JSON names (best practice regardless of obfuscation):

using System.Text.Json.Serialization;

public record WeatherForecast(
    [property: JsonPropertyName("date")] DateOnly Date,
    [property: JsonPropertyName("temperatureC")] int TemperatureC,
    [property: JsonPropertyName("summary")] string? Summary);

Or exclude the DTO type:

[Obfuscation(Exclude = true, ApplyToMembers = true)]
public record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary);

Using [JsonPropertyName] is the better approach — it makes your API contract explicit and survives refactoring even without obfuscation.

Preferred: register the DTO with a source-generated JsonSerializerContext

If you already use a source-generated JsonSerializerContext (the standard way to make System.Text.Json trim- and AOT-safe), adding [JsonSerializable(typeof(T))] to it is strictly better than [JsonPropertyName] alone. Demeanor follows the typeof argument and freezes every property on T automatically, and the handler switches off the reflection path at the same time — so your endpoint becomes both obfuscation-safe and AOT-clean from a single edit.

// Serialization/CatalogJsonContext.cs
 [JsonSerializable(typeof(Product))]
 [JsonSerializable(typeof(Order))]
 [JsonSerializable(typeof(OrderItem))]
+[JsonSerializable(typeof(OrderSummary))]
 [JsonSerializable(typeof(List<Product>))]
 [JsonSerializable(typeof(List<Order>))]
 public partial class CatalogJsonContext : JsonSerializerContext
 {
 }

This is the fix applied in the CatalogService walkthrough — one line, resolves both a rename advisory and an IL2026/IL3050 AOT warning.

Blazor — Routes and Component Parameters Auto-Preserved

Blazor routing uses attribute metadata that survives obfuscation, and component type names can be renamed safely. Component parameters, dependency injection, and cascading values are auto-detected and preserved by Demeanor — no exclusions required for standard Blazor components. The snippets below show what the old renaming failure mode looked like before auto-detection; they are retained for context only.

YOUR CODE
<!-- GreetingCard.razor -->
<div class="card">
    <h3>@Name</h3>
    <p>@Message</p>
    <button @onclick="OnDismiss">
        Dismiss
    </button>
</div>

@code {
    [Parameter]
    public string Name { get; set; }
    [Parameter]
    public string Message { get; set; }
    [Parameter]
    public EventCallback OnDismiss { get; set; }
}
AFTER OBFUSCATION
// Routes preserved: routing metadata
//   survives on renamed component types.
// Type names renamed (fine — Blazor
//   discovers by route metadata, not
//   by name).
//
// Component-bound properties preserved
//   automatically:
//   Name, Message, OnDismiss — all kept.
//   Parent component bindings still
//   resolve correctly at runtime.
//
// Internal component logic (private
//   fields, methods, event handlers)
//   fully obfuscated.

Current behavior: Demeanor's static analysis recognizes standard Blazor component-binding patterns and preserves the properties they depend on automatically.

If you use a custom component framework that binds properties by name in a way Demeanor doesn’t recognize, the permanent fix is to mark those component classes in source:

[Obfuscation(Exclude = true, ApplyToMembers = true)]
public partial class MyCustomComponent : ComponentBase { ... }

Or, if you cannot modify source, exclude them via CLI: demeanor --xr ".*ComponentBase$" --include-publics MyBlazorApp.dll.

Quick Reference

FrameworkStatusExclusions Needed
EF CoreJust worksNone — auto-detected
Console / CLI appsJust worksNone
ASP.NET Minimal APIRoutes + DI workSerialization-attribute-mapped names on response DTOs
WPF / MVVMProperties auto-preservedMark Window/UserControl classes with [Obfuscation], or --xr ".*Window$"
BlazorJust worksNone — components and routing auto-detected
WinFormsMostly worksMark forms/user controls with [Obfuscation], or --xr ".*Form$" --xr ".*UserControl$"
System.Text.JsonAuto-detectedUse serialization naming attributes for explicit contract
Newtonsoft.JsonAuto-detectedUse serialization naming attributes for explicit contract
gRPC / ProtobufJust worksGenerated code uses field numbers, not names
NativeAOTJust worksObfuscation runs on IL before AOT compilation