---
name: aot-trimming
description: "Guidelines for making .NET libraries and applications trimming-safe and Native AOT compatible. Covers the trimming/AOT model, the MSBuild properties that enable analysis (IsTrimmable, IsAotCompatible, PublishTrimmed, PublishAot), the trimming attributes (RequiresUnreferencedCode, RequiresDynamicCode, DynamicallyAccessedMembers, UnconditionalSuppressMessage), IL2xxx/IL3xxx warning codes, and a pattern playbook: source generators and UnsafeAccessor, generated-interception diagnostic suppressors, intentional runtime scanning boundaries, trimming-safe islands and feature switches, migration analyzers, and temporary warning-approval baselines. Use when introducing trimming/AOT support, resolving IL2xxx/IL3xxx warnings, or making reflection-heavy code trimming-safe in C# / .NET codebases. For System.Text.Json AOT scenarios, see also the serialization skill."
version: 1.0.0
tags:
- csharp
- dotnet
- aot
- trimming
- nativeaot
- code-quality
---
# .NET Trimming and Native AOT
## When to Use
- Adding trimming (`PublishTrimmed`) or Native AOT (`PublishAot`) support to a library or application
- Resolving `IL2xxx` (trimming) and `IL3xxx` (AOT/single-file) warnings
- Making reflection-heavy code trimming-safe
- Replacing runtime reflection, assembly scanning, or `Reflection.Emit` with compile-time alternatives
- Reviewing a codebase or PR for trimming/AOT compatibility, including generated interceptors and analyzer suppressors
- Designing public APIs that must carry trimming annotations
## Core Goals
- Produce code that survives the linker's reachability analysis and runs under Native AOT (no JIT).
- Express what the compiler cannot see — which members reflection needs — using the trimming attributes.
- Keep trimming-safe and trimming-unsafe code separated so unsafe paths are explicit and contained.
## Core Model
### Trimming vs Native AOT
- **Trimming** (`PublishTrimmed`) runs ILLink reachability analysis: only statically reachable code is kept. Reflection the analyzer cannot see is trimmed away and produces warnings.
- **Native AOT** (`PublishAot`) makes trimming mandatory and removes the JIT. `Reflection.Emit` is unsupported, constructing *unknown* generic instantiations at runtime is not guaranteed, `Expression.Compile()` may fall back to interpretation, and reflection works only over members the linker preserved.
- Both run the same static analysis:
- `IL2xxx` — trimming warnings: `RequiresUnreferencedCode` propagation + `DynamicallyAccessedMembers` dataflow.
- `IL3xxx` — AOT and single-file warnings: `RequiresDynamicCode` / `RequiresAssemblyFiles`.
A library can be trimmable but **not** AOT-compatible (it uses `Reflection.Emit`, which trimming tolerates but AOT does not).
### The two problems to solve
1. **Unseen reflection** (trimming): the linker removes a member or type you load by name, or reach through `typeof(T)` in a generic method.
2. **Dynamic code generation** (AOT): the JIT is gone, so `Reflection.Emit` is unsupported and constructing *unknown* generic instantiations is not guaranteed.
Both are solved the same way: make the required members *statically visible* to the analyzer, or remove the need for reflection entirely.
### Reflection is not the enemy
Reflection is a legitimate technique and is completely fine in ordinary JIT applications — you need none of this there. The trimming attributes exist to *allow* reflection under trimming/AOT, not to forbid it: annotate what reflection needs, and the linker keeps it. Reach for source generators, `[UnsafeAccessor]`, or capability gating only when you want a specific reflective surface removed or contained (for example, a public registration API you do not want to ship with `[RequiresUnreferencedCode]`).
## Project Configuration
For a **library**, declare compatibility so the analyzers surface warnings and consumers know:
```xml
true
true
```
For an **application**, publish trimmed or AOT:
```xml
true
true
```
Verify by actually publishing — a trimmed/AOT app must produce zero warnings:
```bash
dotnet publish -c Release -r -p:PublishTrimmed=true
dotnet publish -c Release -r -p:PublishAot=true
```
A **test app** that roots the library is the standard way to validate a library:
```xml
true
```
See [aot-trimming-playbook-reference.md](aot-trimming-playbook-reference.md) for the full property table, feature switches, and the warning-approval baseline pattern.
## The Attribute Model
These live in `System.Diagnostics.CodeAnalysis`. See [trimming-attributes-reference.md](trimming-attributes-reference.md) for the full catalog with exact signatures, warning codes, and rules.
- `[RequiresUnreferencedCode(message)]` — marks code that needs members the linker cannot see. Suppresses warnings *inside*; emits `IL2026` at every call site.
- `[RequiresDynamicCode(message)]` — marks code that needs a JIT (emitting, dynamic). Emits `IL3050` at call sites.
- `[DynamicallyAccessedMembers(memberTypes)]` — declares which members of a `Type`/`string` must be preserved. Flows *backward* from the reflection site to the `Type` source. Scope it (and the other attributes) to a single accessor by placing the attribute directly on the accessor, or with the `[method:]`, `[field:]`, or `[return:]` targets.
- `[UnconditionalSuppressMessage(category, checkId, Justification = "...")]` — last-resort, IL-persisted suppression; must carry a justification and a real invariant.
- `[DynamicDependency("Member", typeof(T))]` — keeps named members but does **not** silence warnings.
Choose the narrowest `DynamicallyAccessedMemberTypes` for the reflection you actually perform:
| Reflection | Member type |
| --- | --- |
| `Activator.CreateInstance()`, `new T()`, `Activator.CreateInstance(type)` | `PublicParameterlessConstructor` |
| DI activation (`ActivatorUtilities.CreateFactory`) | `PublicConstructors` |
| Explicit non-public activation | `NonPublicConstructors` |
| `type.GetInterfaces()` | `Interfaces` |
| `type.GetMethods()` | `PublicMethods` (add `NonPublicMethods` when needed) |
| `type.GetProperties()` | `PublicProperties` |
| `type.GetFields()` | `PublicFields` |
| `type.GetNestedTypes()` | `PublicNestedTypes` |
## The Pattern Playbook (selection)
First decide whether the code must be trimming/AOT safe at all. A plain JIT app or library needs none of this — reflection is fine there. Only when you or a consumer publish trimmed/AOT does reflection need to become visible to the linker.
The goal is never "remove all reflection"; it is *make the reflection statically visible* so the linker keeps what it needs. Choose per call site:
- **Keep the reflection and annotate it** — the common case. Put `[DynamicallyAccessedMembers(...)]` on the `Type`/`string` source and propagate it through the chain; mark entry points `[RequiresUnreferencedCode]`/`[RequiresDynamicCode]`. The attributes exist precisely to let reflection work under trimming. See [trimming-attributes-reference.md](trimming-attributes-reference.md).
- **Replace the reflection with a compile-time alternative** — when you want a reflective surface gone (for example, a public registration API you do not want to ship with `[RequiresUnreferencedCode]`). A **source generator** emits direct type references (marker attribute → generated registration); `[UnsafeAccessor]` replaces `FieldInfo`/`MethodInfo`/`ConstructorInfo`. See [aot-trimming-playbook-reference.md](aot-trimming-playbook-reference.md#source-generators-and-compile-time-registration) and [aot-trimming-playbook-reference.md](aot-trimming-playbook-reference.md#unsafe-accessors-for-non-public-members).
- **Isolate a whole dynamic path** — when a path is inherently dynamic (emitting proxies, loading plugins by name), move it behind a boundary and provide a trimming-safe alternative selected by `RuntimeFeature.IsDynamicCodeSupported`. See [aot-trimming-playbook-reference.md](aot-trimming-playbook-reference.md#trimming-safe-islands).
- **Suppress only as a last resort** — a single `[UnconditionalSuppressMessage(...)]` at an invariant-proven leaf, with a `Justification`. Never `#pragma` (not persisted in IL).
See [aot-trimming-playbook-reference.md](aot-trimming-playbook-reference.md) for the full playbook, including object-overload → generic-overload redirects, generated-interception suppressors, intentional scanning boundaries, migration analyzers, strict-mode precedence, and temporary warning-approval baselines.
## Reference Files
- [trimming-attributes-reference.md](trimming-attributes-reference.md): The complete attribute catalog — `RequiresUnreferencedCode`, `RequiresDynamicCode`, `DynamicallyAccessedMembers` (full `DynamicallyAccessedMemberTypes` list), `UnconditionalSuppressMessage`, `DynamicDependency` — each with intent, exact signature, and rules, plus the `IL2xxx`/`IL3xxx` warning-code table.
- [aot-trimming-playbook-reference.md](aot-trimming-playbook-reference.md): The full pattern playbook — source generators vs reflection, trimming-safe islands and capability guards, separating safe/unsafe code, System.Text.Json source generation, object-overload → generic-overload redirects with `[OverloadResolutionPriority]` and opt-in analyzers, migration analyzers, warning-approval baselines, known gotchas, and the full generation checklist.
## Generation Checklist (Summary)
1. **Project** — `IsTrimmable`/`IsAotCompatible` on libraries; `PublishTrimmed`/`PublishAot` on apps; zero warnings on publish.
2. **Make reflection visible (or replace it)** — annotate the reflection you keep (`DynamicallyAccessedMembers`/`RequiresUnreferencedCode`/`RequiresDynamicCode`); replace it with source generators, generic (`typeof(T)`) APIs, or `[UnsafeAccessor]` only where you want the reflective surface gone.
3. **Annotate** — narrowest `DynamicallyAccessedMembers` on every `Type`/`string` source; propagate through the chain; `[RequiresUnreferencedCode]`/`[RequiresDynamicCode]` on unsafe entry points.
4. **Isolate** — keep unsafe reflective/emit code behind a boundary; gate *dynamic-code* paths with `RuntimeFeature.IsDynamicCodeSupported`, and separate *trimming* paths with `#if` or annotations.
5. **Suppress** — only with `[UnconditionalSuppressMessage]` + justification at invariant-proven leaves; never `#pragma`/`SuppressMessage`.
6. **Verify** — publish trimmed and AOT, run analyzers, and lock the warning surface with an approval baseline.
## References
- [Trimming options](https://learn.microsoft.com/dotnet/core/deploying/trimming/trimming-options)
- [Prepare .NET libraries for trimming](https://learn.microsoft.com/dotnet/core/deploying/trimming/prepare-libraries-for-trimming)
- [Introduction to trim warnings](https://learn.microsoft.com/dotnet/core/deploying/trimming/fixing-warnings)
- [Native AOT deployment](https://learn.microsoft.com/dotnet/core/deploying/native-aot/)
- [ILLink error codes](https://github.com/dotnet/runtime/blob/main/docs/tools/illink/error-codes.md)