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
.protocontract, 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 points —
Mainmethods, 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.dllMSBuild (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.dllMSBuild (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.
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 — 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.
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}" />// 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.
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 | Just works | None — XAML-loaded types stay named and compiled XAML is patched to match renames |
| 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 | Auto-detected | Messages serialise by field number; service methods dispatch by name and are protected by a built-in rule |
| NativeAOT | Just works | Obfuscation runs on IL before AOT compilation |