.NET Trimming and Native AOT
When to Use
- Adding trimming (
PublishTrimmed) or Native AOT (PublishAot) support to a library or application - Resolving
IL2xxx(trimming) andIL3xxx(AOT/single-file) warnings - Making reflection-heavy code trimming-safe
- Replacing runtime reflection, assembly scanning, or
Reflection.Emitwith 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.Emitis 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:RequiresUnreferencedCodepropagation +DynamicallyAccessedMembersdataflow.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
- Unseen reflection (trimming): the linker removes a member or type you load by name, or reach through
typeof(T)in a generic method. - Dynamic code generation (AOT): the JIT is gone, so
Reflection.Emitis 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:
<PropertyGroup>
<!-- Trim-compatible: enables trim warnings. -->
<IsTrimmable>true</IsTrimmable>
<!-- AOT-compatible: implies IsTrimmable + EnableTrimAnalyzer + EnableSingleFileAnalyzer + EnableAotAnalyzer. -->
<IsAotCompatible Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0'))">true</IsAotCompatible>
</PropertyGroup>
For an application, publish trimmed or AOT:
<PropertyGroup>
<PublishTrimmed>true</PublishTrimmed>
<!-- or -->
<PublishAot>true</PublishAot>
</PropertyGroup>
Verify by actually publishing — a trimmed/AOT app must produce zero warnings:
dotnet publish -c Release -r <rid> -p:PublishTrimmed=true
dotnet publish -c Release -r <rid> -p:PublishAot=true
A test app that roots the library is the standard way to validate a library:
<PropertyGroup>
<PublishTrimmed>true</PublishTrimmed>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\MyLibrary\MyLibrary.csproj" />
<TrimmerRootAssembly Include="MyLibrary" />
</ItemGroup>
See 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 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; emitsIL2026at every call site.[RequiresDynamicCode(message)]— marks code that needs a JIT (emitting, dynamic). EmitsIL3050at call sites.[DynamicallyAccessedMembers(memberTypes)]— declares which members of aType/stringmust be preserved. Flows backward from the reflection site to theTypesource. 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<T>(), new T(), Activator.CreateInstance(type) | PublicParameterlessConstructor |
DI activation (ActivatorUtilities.CreateFactory<T>) | 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 theType/stringsource 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. -
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]replacesFieldInfo/MethodInfo/ConstructorInfo. See aot-trimming-playbook-reference.md and aot-trimming-playbook-reference.md. -
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. -
Suppress only as a last resort — a single
[UnconditionalSuppressMessage(...)]at an invariant-proven leaf, with aJustification. Never#pragma(not persisted in IL).
See 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: The complete attribute catalog —
RequiresUnreferencedCode,RequiresDynamicCode,DynamicallyAccessedMembers(fullDynamicallyAccessedMemberTypeslist),UnconditionalSuppressMessage,DynamicDependency— each with intent, exact signature, and rules, plus theIL2xxx/IL3xxxwarning-code table. - 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)
- Project —
IsTrimmable/IsAotCompatibleon libraries;PublishTrimmed/PublishAoton apps; zero warnings on publish. - 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. - Annotate — narrowest
DynamicallyAccessedMemberson everyType/stringsource; propagate through the chain;[RequiresUnreferencedCode]/[RequiresDynamicCode]on unsafe entry points. - Isolate — keep unsafe reflective/emit code behind a boundary; gate dynamic-code paths with
RuntimeFeature.IsDynamicCodeSupported, and separate trimming paths with#ifor annotations. - Suppress — only with
[UnconditionalSuppressMessage]+ justification at invariant-proven leaves; never#pragma/SuppressMessage. - Verify — publish trimmed and AOT, run analyzers, and lock the warning surface with an approval baseline.