dotnet-trimming
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>
<!-- Enable trimming on publish -->
<PublishTrimmed>true</PublishTrimmed>
<!-- Enable trim analyzer during development -->
<EnableTrimAnalyzer>true</EnableTrimAnalyzer>
<!-- Optional: also enable AOT analyzer if targeting AOT -->
<EnableAotAnalyzer>true</EnableAotAnalyzer>
</PropertyGroup>
```text
`PublishTrimmed` tells the linker to remove unreachable code when publishing. `EnableTrimAnalyzer` enables Roslyn analyzers that warn about trim-unsafe patterns during development.
### For Libraries
```xml
<PropertyGroup>
<!-- Declare the library is trim-safe (auto-enables trim analyzer) -->
<IsTrimmable>true</IsTrimmable>
<!-- Declare AOT compatibility (auto-enables AOT analyzer) -->
<IsAotCompatible>true</IsAotCompatible>
</PropertyGroup>
```text
**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.
```csharp
[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)!;
}
```text
### `[DynamicallyAccessedMembers]`
Tells the trimmer which members of a type are accessed via reflection, so they are preserved:
```csharp
public T CreateInstance<[DynamicallyAccessedMembers(
DynamicallyAccessedMemberTypes.PublicConstructors)] T>()
where T : class
=> (T)Activator.CreateInstance(typeof(T))!;
// The trimmer preserves public constructors of T
// because the constraint tells it what's needed
```text
### `[DynamicDependency]`
Explicitly preserves a specific member from trimming:
```csharp
// Preserve a method that is only called via reflection
[DynamicDependency(nameof(OnConfigChanged), typeof(ConfigWatcher))]
public void StartWatching() { /* reflects on OnConfigChanged */ }
// Preserve all public properties (e.g., for serialization)
[DynamicDependency(DynamicallyAccessedMemberTypes.PublicProperties,
typeof(LegacyDto))]
public void SerializeLegacy(LegacyDto dto) { /* ... */ }
```text
### `[UnconditionalSuppressMessage]`
Suppresses a specific trim warning when you have verified the code is safe despite the analyzer's concern:
```csharp
[UnconditionalSuppressMessage("Trimming",
"IL2026:RequiresUnreferencedCode",
Justification = "Type is preserved via ILLink descriptor")]
public void CallLegacyCode() { /* ... */ }
```text
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
```xml
<!-- ILLink.Descriptors.xml -->
<linker>
<!-- Preserve specific types -->
<assembly fullname="MyApp">
<type fullname="MyApp.Models.PluginConfig" preserve="all" />
<type fullname="MyApp.Services.LegacyAdapter">
<method name="Initialize" />
<method name="ProcessRequest" />
</type>
</assembly>
<!-- Preserve an entire third-party assembly -->
<assembly fullname="LegacyLibrary" preserve="all" />
</linker>
```text
### Registration
```xml
<!-- In .csproj -->
<ItemGroup>
<TrimmerRootDescriptor Include="ILLink.Descriptors.xml" />
</ItemGroup>
```csharp
### Alternative: TrimmerRootAssembly
For entire assemblies that must not be trimmed:
```xml
<ItemGroup>
<!-- Preserve entire assembly (no trimming at all) -->
<TrimmerRootAssembly Include="LegacyLibrary" />
</ItemGroup>
```text
---
## 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.
```bash
# Default: one warning per assembly (hard to debug)
dotnet publish -c Release /p:PublishTrimmed=true
# warning IL2104: Assembly 'MyApp' produced trim warnings
# Detailed: per-occurrence warnings (easier to fix)
dotnet publish -c Release /p:PublishTrimmed=true /p:TrimmerSingleWarn=false
# warning IL2026: MyApp.PluginLoader.LoadPlugin(...) requires unreferenced code
# warning IL2057: Unrecognized value passed to Type.GetType(...)
# Analysis without publishing
dotnet build /p:EnableTrimAnalyzer=true /p:TrimmerSingleWarn=false
```text
---
## 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
```bash
# Publish with trimming
dotnet publish -c Release -r linux-x64 /p:PublishTrimmed=true
# Run the trimmed binary
./bin/Release/net8.0/linux-x64/publish/MyApp
# Verify functionality:
# 1. All endpoints respond correctly
# 2. JSON deserialization produces populated objects
# 3. DI-resolved services function
# 4. No MissingMethodException or MissingMetadataException
```json
### Trim Test in CI
```bash
# CI script: publish trimmed and run integration tests
dotnet publish src/MyApp -c Release -r linux-x64 /p:PublishTrimmed=true -o ./publish
# Run smoke tests against trimmed binary
./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
```text
### Trim Warning CI Gate
```bash
# Fail CI if any trim warnings exist
dotnet build /p:EnableTrimAnalyzer=true /p:TrimmerSingleWarn=false \
/warnaserror:IL2026,IL2057,IL2060,IL2067,IL2070,IL3050
```bash
---
## Library Authoring for Trimming
### Making a Library Trim-Safe
1. Set `<IsTrimmable>true</IsTrimmable>` in the library `.csproj`
2. Annotate all reflection-using APIs with `[RequiresUnreferencedCode]`
3. Add `[DynamicallyAccessedMembers]` to parameters that receive types used reflectively
4. Replace reflection with source generators where possible
5. Test by consuming the library from a trimmed application
```xml
<!-- Library .csproj -->
<PropertyGroup>
<!-- Auto-enables trim analyzer -->
<IsTrimmable>true</IsTrimmable>
<!-- Auto-enables AOT analyzer; implies IsTrimmable in .NET 8+ -->
<IsAotCompatible>true</IsAotCompatible>
</PropertyGroup>
```text
### Annotating Public APIs
```csharp
// Method that uses reflection internally -- annotate honestly
[RequiresUnreferencedCode(
"Uses reflection to discover plugin types. " +
"Use RegisterPlugin<T>() for trim-safe plugin registration.")]
public IPlugin LoadPlugin(string typeName) { /* ... */ }
// Trim-safe alternative
public void RegisterPlugin<[DynamicallyAccessedMembers(
DynamicallyAccessedMemberTypes.PublicConstructors)] T>()
where T : class, IPlugin
{
_plugins[typeof(T).Name] = () => (IPlugin)Activator.CreateInstance<T>();
}
```text
### Conditional APIs
Provide both reflection-based and trim-safe APIs when possible:
```csharp
public class ServiceRegistry
{
// Trim-safe: explicit type
public void Register<[DynamicallyAccessedMembers(
DynamicallyAccessedMemberTypes.PublicConstructors)] TService,
TImplementation>()
where TImplementation : class, TService
{ /* ... */ }
// Not trim-safe: assembly scanning
[RequiresUnreferencedCode("Scans assembly for service types")]
public void RegisterFromAssembly(Assembly assembly)
{ /* ... */ }
}
```text
---
## Agent Gotchas
1. **Do not use `PublishTrimmed` in library projects.** Libraries use `IsTrimmable` to declare they are trim-safe. `PublishTrimmed` is for applications.
2. **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.
3. **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).
4. **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.
5. **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.
6. **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
- [Trim self-contained applications](https://learn.microsoft.com/en-us/dotnet/core/deploying/trimming/trimming-options)
- [Prepare libraries for trimming](https://learn.microsoft.com/en-us/dotnet/core/deploying/trimming/prepare-libraries-for-trimming)
- [Introduction to trim warnings](https://learn.microsoft.com/en-us/dotnet/core/deploying/trimming/fixing-warnings)
- [Trimming annotation attributes](https://learn.microsoft.com/en-us/dotnet/core/deploying/trimming/prepare-libraries-for-trimming#trimming-annotation-attributes)
- [ILLink descriptor format](https://learn.microsoft.com/en-us/dotnet/core/deploying/trimming/trimming-options#descriptor-format)