| name | dotnet-trimming |
| description | Trims .NET 8+ apps and libraries. Annotations, ILLink descriptors, IL2xxx warnings, IsTrimmable. |
| allowed-tools | ["Read","Grep","Glob","Bash","Write","Edit"] |
dotnet-trimming
Trim-safe development for .NET 8+ applications and libraries: trimming annotations ([RequiresUnreferencedCode], [DynamicallyAccessedMembers], [DynamicDependency]), ILLink descriptor XML for type preservation, TrimmerSingleWarn for granular diagnostics, testing trimmed output, fixing IL2xxx/IL3xxx warnings, and library authoring with IsTrimmable.
Version assumptions: .NET 8.0+ baseline. Trimming shipped in .NET 6, but .NET 8 provides the most complete annotation surface and analyzer coverage. .NET 9 improved warning accuracy and library compat.
Scope
- MSBuild properties for trimming (apps vs libraries)
- Trimming annotations (RequiresUnreferencedCode, DynamicallyAccessedMembers, DynamicDependency)
- ILLink descriptor XML for type preservation
- TrimmerSingleWarn for granular diagnostics
- IL2xxx/IL3xxx warning reference and fixes
- Testing trimmed output and CI gates
- Library authoring with IsTrimmable and IsAotCompatible
Out of scope
- Native AOT publish pipeline and MSBuild configuration -- see [skill:dotnet-native-aot]
- AOT-first design patterns -- see [skill:dotnet-aot-architecture]
- WASM AOT compilation -- see [skill:dotnet-aot-wasm]
- MAUI-specific AOT and trimming -- see [skill:dotnet-maui-aot]
- Source generator authoring -- see [skill:dotnet-csharp-source-generators]
- Serialization depth -- see [skill:dotnet-serialization]
- Container deployment -- see [skill:dotnet-containers]
- Performance patterns (Span, pooling) -- see [skill:dotnet-performance-patterns]
Cross-references: [skill:dotnet-native-aot] for AOT compilation pipeline, [skill:dotnet-aot-architecture] for AOT-safe design patterns, [skill:dotnet-serialization] for AOT-safe serialization, [skill:dotnet-csharp-source-generators] for source gen as trimming enabler.
MSBuild Properties: Apps vs Libraries
Apps and libraries use different MSBuild properties for trimming. This distinction is critical -- using the wrong property causes subtle issues.
For Applications
<PropertyGroup>
<PublishTrimmed>true</PublishTrimmed>
<EnableTrimAnalyzer>true</EnableTrimAnalyzer>
<EnableAotAnalyzer>true</EnableAotAnalyzer>
</PropertyGroup>
PublishTrimmed tells the linker to remove unreachable code when publishing. EnableTrimAnalyzer enables Roslyn analyzers that warn about trim-unsafe patterns during development.
For Libraries
<PropertyGroup>
<IsTrimmable>true</IsTrimmable>
<IsAotCompatible>true</IsAotCompatible>
</PropertyGroup>
Key difference: Libraries do not set PublishTrimmed -- they are not published as standalone applications. IsTrimmable tells consumers that the library's public API is annotated for trimming safety. Setting IsTrimmable automatically enables the trim analyzer for the library project.
| Property | Project Type | Effect |
|---|
PublishTrimmed | App | Trims on publish, enables linker |
EnableTrimAnalyzer | App | Enables trim warnings during build |
IsTrimmable | Library | Declares trim-safe, auto-enables analyzer |
IsAotCompatible | Library | Declares AOT-safe, auto-enables AOT analyzer |
PublishAot | App | Enables AOT (implies PublishTrimmed) |
Trimming Annotations
.NET provides attributes to annotate code that interacts with reflection, helping the trimmer understand what to preserve.
[RequiresUnreferencedCode]
Marks a method as unsafe for trimming. The trimmer and analyzer produce IL2026 warnings when this method is called from trim-safe code.
[RequiresUnreferencedCode("Uses reflection to discover plugins")]
public IPlugin LoadPlugin(string typeName)
{
var type = Type.GetType(typeName)
?? throw new InvalidOperationException($"Type {typeName} not found");
return (IPlugin)Activator.CreateInstance(type)!;
}
[DynamicallyAccessedMembers]
Tells the trimmer which members of a type are accessed via reflection, so they are preserved:
public T CreateInstance<[DynamicallyAccessedMembers(
DynamicallyAccessedMemberTypes.PublicConstructors)] T>()
where T : class
=> (T)Activator.CreateInstance(typeof(T))!;
[DynamicDependency]
Explicitly preserves a specific member from trimming:
[DynamicDependency(nameof(OnConfigChanged), typeof(ConfigWatcher))]
public void StartWatching() { }
[DynamicDependency(DynamicallyAccessedMemberTypes.PublicProperties,
typeof(LegacyDto))]
public void SerializeLegacy(LegacyDto dto) { }
[UnconditionalSuppressMessage]
Suppresses a specific trim warning when you have verified the code is safe despite the analyzer's concern:
[UnconditionalSuppressMessage("Trimming",
"IL2026:RequiresUnreferencedCode",
Justification = "Type is preserved via ILLink descriptor")]
public void CallLegacyCode() { }
Use sparingly -- only when you have verified safety through ILLink descriptors or other means.
ILLink Descriptors
ILLink descriptor XML files tell the trimmer to preserve types, methods, or entire assemblies. Do not use legacy RD.xml -- it is a .NET Native/UWP format that is silently ignored by modern .NET trimming.
Descriptor Format
<linker>
<assembly fullname="MyApp">
<type fullname="MyApp.Models.PluginConfig" preserve="all" />
<type fullname="MyApp.Services.LegacyAdapter">
<method name="Initialize" />
<method name="ProcessRequest" />
</type>
</assembly>
<assembly fullname="LegacyLibrary" preserve="all" />
</linker>
Registration
<ItemGroup>
<TrimmerRootDescriptor Include="ILLink.Descriptors.xml" />
</ItemGroup>
Alternative: TrimmerRootAssembly
For entire assemblies that must not be trimmed:
<ItemGroup>
<TrimmerRootAssembly Include="LegacyLibrary" />
</ItemGroup>
TrimmerSingleWarn
By default, the trimmer groups warnings per assembly, showing one summary line. TrimmerSingleWarn=false shows every individual warning, which is essential for fixing trim issues.
dotnet publish -c Release /p:PublishTrimmed=true
dotnet publish -c Release /p:PublishTrimmed=true /p:TrimmerSingleWarn=false
dotnet build /p:EnableTrimAnalyzer=true /p:TrimmerSingleWarn=false
IL2xxx/IL3xxx Warning Reference
Trim Warnings (IL2xxx)
| Code | Meaning | Fix |
|---|
| IL2026 | Method has [RequiresUnreferencedCode] | Replace with trim-safe alternative or add descriptor |
| IL2046 | Trim attribute mismatch on override | Match annotation from base type |
| IL2057 | Unrecognized Type.GetType() argument | Use compile-time known type or [DynamicDependency] |
| IL2060 | MakeGenericType call with unknown type | Use concrete generic instantiations |
| IL2062 | Value passed to [DynamicallyAccessedMembers] parameter has no annotation | Add [DynamicallyAccessedMembers] to the source |
| IL2067 | Parameter mismatch for [DynamicallyAccessedMembers] | Ensure annotations flow correctly through call chain |
| IL2070 | this parameter of Type.GetProperties() etc. not annotated | Add [DynamicallyAccessedMembers] constraint |
| IL2072 | Return value of a method not annotated | Annotate return type with [DynamicallyAccessedMembers] |
| IL2104 | Assembly produced trim warnings (summary) | Use TrimmerSingleWarn=false for details |
AOT Warnings (IL3xxx)
| Code | Meaning | Fix |
|---|
| IL3050 | Method has [RequiresDynamicCode] | Replace with source-gen or static alternative |
| IL3051 | [RequiresDynamicCode] annotation mismatch | Match annotation from base type |
| IL3052 | COM interop with dynamic code | Use [LibraryImport] with static marshalling |
Testing Trimmed Output
Publish and Test
dotnet publish -c Release -r linux-x64 /p:PublishTrimmed=true
./bin/Release/net8.0/linux-x64/publish/MyApp
Trim Test in CI
dotnet publish src/MyApp -c Release -r linux-x64 /p:PublishTrimmed=true -o ./publish
./publish/MyApp &
APP_PID=$!
sleep 3
curl -f http://localhost:8080/health/live || (kill $APP_PID; exit 1)
curl -f http://localhost:8080/api/products || (kill $APP_PID; exit 1)
kill $APP_PID
Trim Warning CI Gate
dotnet build /p:EnableTrimAnalyzer=true /p:TrimmerSingleWarn=false \
/warnaserror:IL2026,IL2057,IL2060,IL2067,IL2070,IL3050
Library Authoring for Trimming
Making a Library Trim-Safe
- Set
<IsTrimmable>true</IsTrimmable> in the library .csproj
- Annotate all reflection-using APIs with
[RequiresUnreferencedCode]
- Add
[DynamicallyAccessedMembers] to parameters that receive types used reflectively
- Replace reflection with source generators where possible
- Test by consuming the library from a trimmed application
<PropertyGroup>
<IsTrimmable>true</IsTrimmable>
<IsAotCompatible>true</IsAotCompatible>
</PropertyGroup>
Annotating Public APIs
[RequiresUnreferencedCode(
"Uses reflection to discover plugin types. " +
"Use RegisterPlugin<T>() for trim-safe plugin registration.")]
public IPlugin LoadPlugin(string typeName) { }
public void RegisterPlugin<[DynamicallyAccessedMembers(
DynamicallyAccessedMemberTypes.PublicConstructors)] T>()
where T : class, IPlugin
{
_plugins[typeof(T).Name] = () => (IPlugin)Activator.CreateInstance<T>();
}
Conditional APIs
Provide both reflection-based and trim-safe APIs when possible:
public class ServiceRegistry
{
public void Register<[DynamicallyAccessedMembers(
DynamicallyAccessedMemberTypes.PublicConstructors)] TService,
TImplementation>()
where TImplementation : class, TService
{ }
[RequiresUnreferencedCode("Scans assembly for service types")]
public void RegisterFromAssembly(Assembly assembly)
{ }
}
Agent Gotchas
- Do not use
PublishTrimmed in library projects. Libraries use IsTrimmable to declare they are trim-safe. PublishTrimmed is for applications.
- Do not use RD.xml for type preservation. RD.xml is a .NET Native/UWP format that is silently ignored by modern .NET trimming. Use ILLink descriptor XML files instead.
- Do not suppress trim warnings without verifying safety.
[UnconditionalSuppressMessage] hides warnings but does not fix the underlying issue. Only suppress when you have verified the code is safe (e.g., via ILLink descriptors).
- Do not forget
TrimmerSingleWarn=false when debugging trim issues. Without it, you get one summary warning per assembly, making it impossible to find the specific problematic call site.
- Do not confuse
IsTrimmable with PublishTrimmed. IsTrimmable declares a library is trim-safe and enables the analyzer. PublishTrimmed enables the linker in applications. They serve different purposes.
- Do not add
[RequiresUnreferencedCode] to methods that do not use reflection. The annotation propagates virally -- callers must also be annotated or suppress the warning. Only annotate methods that actually use trim-unsafe reflection.
References