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 — Protobuf messages serialise by field number, so message classes are safe. Service dispatch is by method name from the .proto contract, and a built-in rule protects service implementations.
  • 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)
  • ORM entity types — entities reached through DbSet<T>, and the properties the column mapping binds by name
  • 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: --rename-types off, --rename-methods off, --rename-fields off, --rename-properties off, --rename-events off, --rename-parameters off, --rename-enums off, --rename-resource-names off.

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 maps columns to entity properties by name. Demeanor finds your entities the same way EF Core does — by walking the DbSet<T> properties on your DbContext — and preserves each entity type and its properties. 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 — Works Without Exclusions

Compiled XAML (BAML) refers to types, namespaces, binding paths, and members set from markup by string. Demeanor rewrites those references as part of obfuscation, and separately keeps the types XAML loads by name — Window, UserControl, Page, Application, and ResourceDictionary subclasses — under their original names. Neither needs a hand-written exclusion.

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
// Bound properties PRESERVED:
//   Name, Email, Summary, SaveCommand
//
// MainWindow keeps its name — XAML
//   loads it by name, and the pack URI
//   embeds the original namespace.
//
// A plain CLR class used as a XAML
//   resource IS renamed, and its
//   compiled XAML is rewritten to
//   match: type name, clr-namespace,
//   binding paths, and the members
//   set from markup.
//
// Internal logic, fields, and non-UI
//   types are fully obfuscated.
//
// Zero exclusions needed.

What still needs your attention. x:Name values are left as they are, which is safe — the generated code-behind field is wired up by connection id, not by name. A literal string lookup is a different matter: if you call FindResource("MyStyle") from code, that string is not correlated with the resource key in your markup, so preserve the name explicitly if it breaks at runtime.

If a third-party change-notification pattern stands in for INotifyPropertyChanged (Fody’s PropertyChanged.Fody, CommunityToolkit’s [ObservableProperty]), the bound properties may not be detected — exclude those explicitly.

To go the other way and rename a window anyway:

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

Demeanor turns BAML patching on automatically for any assembly containing compiled XAML. To suppress it for one type, apply [Obfuscation(Feature = "baml", Exclude = true)] to that type.

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 / MVVMJust worksNone — XAML-loaded types stay named and compiled XAML is patched to match renames
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 / ProtobufAuto-detectedMessages serialise by field number; service methods dispatch by name and are protected by a built-in rule
NativeAOTJust worksObfuscation runs on IL before AOT compilation