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 points —
Mainmethods, 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.
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);
});// 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.
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}" />// 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.
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);
}// 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.
<!-- 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; }
}// 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
| Framework | Status | Exclusions Needed |
|---|---|---|
| EF Core | Just works | None — auto-detected |
| Console / CLI apps | Just works | None |
| ASP.NET Minimal API | Routes + DI work | Serialization-attribute-mapped names on response DTOs |
| WPF / MVVM | Properties auto-preserved | Mark Window/UserControl classes with [Obfuscation], or --xr ".*Window$" |
| Blazor | Just works | None — components and routing auto-detected |
| WinForms | Mostly works | Mark forms/user controls with [Obfuscation], or --xr ".*Form$" --xr ".*UserControl$" |
| System.Text.Json | Auto-detected | Use serialization naming attributes for explicit contract |
| Newtonsoft.Json | Auto-detected | Use serialization naming attributes for explicit contract |
| gRPC / Protobuf | Just works | Generated code uses field numbers, not names |
| NativeAOT | Just works | Obfuscation runs on IL before AOT compilation |