Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
This article explains the fundamental concepts behind trim analysis to help you understand why certain code patterns produce warnings and how to make your code trim-compatible. Understanding these concepts will help you make informed decisions when addressing trim warnings rather than simply "spreading attributes around to silence the tooling."
How the trimmer analyzes code
The trimmer performs static analysis at publish time to determine which code is used by your application. It starts from known entry points (like your Main method) and follows the code paths through your application.
What the trimmer can understand
The trimmer excels at analyzing direct, compile-time-visible code patterns:
// The trimmer CAN understand these patterns:
var date = new DateTime();
date.AddDays(1); // Direct method call - trimmer knows AddDays is used
var list = new List<string>();
list.Add("hello"); // Generic method call - trimmer knows List<string>.Add is used
string result = MyUtility.Process("data"); // Direct static method call
In these examples, the trimmer can follow the code path and mark DateTime.AddDays, List<string>.Add, and MyUtility.Process as used code that should be kept in the final application.
What the trimmer cannot understand
The trimmer struggles with dynamic operations where the target of an operation isn't known until runtime:
// The trimmer CANNOT fully understand these patterns:
Type type = Type.GetType(Console.ReadLine()); // Type name from user input
type.GetMethod("SomeMethod"); // Which method? On which type?
object obj = GetSomeObject();
obj.GetType().GetProperties(); // What type will obj be at runtime?
Assembly asm = Assembly.LoadFrom(pluginPath); // What's in this assembly?
In these examples, the trimmer has no way to know:
- Which type the user will enter
- What type
GetSomeObject()returns - What code exists in the dynamically loaded assembly
This is the fundamental problem that trim warnings address.
The reflection problem
Reflection allows code to inspect and invoke types and members dynamically at runtime. This is powerful but creates a challenge for static analysis.
Why reflection breaks trimming
Consider this example:
void PrintMethodNames(Type type)
{
foreach (var method in type.GetMethods())
{
Console.WriteLine(method.Name);
}
}
// Called somewhere in the app
PrintMethodNames(typeof(DateTime));
From the trimmer's perspective:
- It sees
type.GetMethods()is called. - It doesn't know what
typewill be (it's a parameter). - It can't determine which types' methods need to be preserved.
- Without guidance, it might remove methods from
DateTime, breaking the code.
Consequently, the trimmer produces a warning on this code.
Understanding DynamicallyAccessedMembers
DynamicallyAccessedMembersAttribute solves the reflection problem by creating an explicit contract between the caller and the called method.
The fundamental purpose
DynamicallyAccessedMembers tells the trimmer: "This parameter (or field, or return value) will hold a Type that needs specific members preserved because reflection will be used to access them."
A concrete example
Let's fix the previous example:
void PrintMethodNames(
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type)
{
foreach (var method in type.GetMethods())
{
Console.WriteLine(method.Name);
}
}
// When this is called...
PrintMethodNames(typeof(DateTime));
Now the trimmer understands:
PrintMethodNamesrequires its parameter to havePublicMethodspreserved.- The call site passes
typeof(DateTime). - Therefore,
DateTime's public methods must be kept.
The attribute creates a requirement that flows backward from the reflection usage to the source of the Type value.
It's a contract, not a hint
This is crucial to understand: DynamicallyAccessedMembers isn't just documentation. The trimmer enforces this contract.
Analogy with generic type constraints
If you're familiar with generic type constraints, DynamicallyAccessedMembers works similarly. Just as generic constraints flow through your code:
void Process<T>(T value) where T : IDisposable
{
value.Dispose(); // OK because constraint guarantees IDisposable
}
void CallProcess<T>(T value) where T : IDisposable
{
Process(value); // OK - constraint satisfied
}
void CallProcessBroken<T>(T value)
{
Process(value); // ERROR - T doesn't have IDisposable constraint
}
DynamicallyAccessedMembers creates similar requirements that flow through your code:
void UseReflection([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type)
{
type.GetMethods(); // OK because annotation guarantees methods are preserved
}
void PassType([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type)
{
UseReflection(type); // OK - requirement satisfied
}
void PassTypeBroken(Type type)
{
UseReflection(type); // WARNING - type doesn't have required annotation
}
Both create contracts that must be fulfilled, and both produce errors or warnings when the contract can't be satisfied.
How the contract is enforced
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)]
Type GetTypeForProcessing()
{
return typeof(DateTime); // OK - trimmer will preserve DateTime's public methods
}
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)]
Type GetTypeFromInput()
{
// WARNING: The trimmer can't verify that the type from GetType()
// will have its public methods preserved
return Type.GetType(Console.ReadLine());
}
If you can't fulfill the contract (like in the second example), you'll get a warning.
Understanding RequiresUnreferencedCode
Some code patterns simply cannot be made statically analyzable. For these cases, use RequiresUnreferencedCodeAttribute.
When to use RequiresUnreferencedCode
Use the RequiresUnreferencedCodeAttribute attribute when:
- The reflection pattern is fundamentally dynamic: Loading assemblies or types by string names from external sources.
- The complexity is too high to annotate: Code that uses reflection in complex, data-driven ways.
- You're using runtime code generation: Technologies like System.Reflection.Emit or the
dynamickeyword.
Example:
[RequiresUnreferencedCode("Plugin loading is not compatible with trimming")]
void LoadPlugin(string pluginPath)
{
Assembly pluginAssembly = Assembly.LoadFrom(pluginPath);
// Plugin assemblies aren't known at publish time
// This fundamentally cannot be made trim-compatible
}
The purpose of the attribute
RequiresUnreferencedCode serves two purposes:
- Suppresses warnings inside the method: The trimmer won't analyze or warn about the reflection usage.
- Creates warnings at call sites: Any code calling this method gets a warning.
This "bubbles up" the warning to give developers visibility into trim-incompatible code paths.
Writing good messages
The message should help developers understand their options:
// ❌ Not helpful
[RequiresUnreferencedCode("Uses reflection")]
// ✅ Helpful - explains what's incompatible and suggests alternatives
[RequiresUnreferencedCode("Plugin loading is not compatible with trimming. Consider using a source generator for known plugins instead")]
How requirements flow through code
Understanding how requirements propagate helps you know where to add attributes.
Requirements flow backward
Requirements flow from where reflection is used back to where the Type originates:
void CallChain()
{
// Step 1: Source of the Type value
ProcessData<DateTime>(); // ← Requirement ends here
}
void ProcessData<T>()
{
// Step 2: Type flows through generic parameter
var type = typeof(T);
DisplayInfo(type); // ← Requirement flows back through here
}
void DisplayInfo(Type type)
{
// Step 3: Reflection creates the requirement
type.GetMethods(); // ← Requirement starts here
}
To make this trim-compatible, you need to annotate the chain:
void ProcessData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] T>()
{
var type = typeof(T);
DisplayInfo(type);
}
void DisplayInfo(
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type)
{
type.GetMethods();
}
Now the requirement flows: GetMethods() requires PublicMethods → type parameter needs PublicMethods → generic T needs PublicMethods → DateTime needs PublicMethods preserved.
Requirements flow through storage
Requirements also flow through fields and properties:
class TypeHolder
{
// This field will hold Types that need PublicMethods preserved
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)]
private Type _typeToProcess;
public void SetType<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] T>()
{
_typeToProcess = typeof(T); // OK - requirement satisfied
}
public void Process()
{
_typeToProcess.GetMethods(); // OK - field is annotated
}
}
Choosing the right approach
When you encounter code that needs reflection, follow this decision tree:
1. Can you avoid reflection?
The best solution is to avoid reflection when possible:
// ❌ Uses reflection
void Process(Type type)
{
var instance = Activator.CreateInstance(type);
}
// ✅ Uses compile-time generics instead
void Process<T>() where T : new()
{
var instance = new T();
}
2. Is the Type known at compile time?
If reflection is necessary but the types are known, use DynamicallyAccessedMembers:
// ✅ Trim-compatible
void Serialize<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T>(T obj)
{
foreach (var prop in typeof(T).GetProperties())
{
// Serialize property
}
}
3. Is the pattern fundamentally dynamic?
If the types truly aren't known until runtime, use RequiresUnreferencedCode:
// ✅ Documented as trim-incompatible
[RequiresUnreferencedCode("Dynamic type loading is not compatible with trimming")]
void ProcessTypeByName(string typeName)
{
var type = Type.GetType(typeName);
// Work with type
}
Common patterns and solutions
Pattern: Factory methods
// Problem: Creating instances from Type parameter
object CreateInstance(Type type)
{
return Activator.CreateInstance(type);
}
// Solution: Specify constructor requirements
object CreateInstance(
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type type)
{
return Activator.CreateInstance(type);
}
Pattern: Plugin systems
// Problem: Loading unknown assemblies at runtime
[RequiresUnreferencedCode("Plugin loading is not trim-compatible. Plugins must be known at compile time.")]
void LoadPlugins(string pluginDirectory)
{
foreach (var file in Directory.GetFiles(pluginDirectory, "*.dll"))
{
Assembly.LoadFrom(file);
}
}
// Better solution: Known plugins with source generation
// Use source generators to create plugin registration code at compile time
Key takeaways
- The trimmer uses static analysis - it can only understand code paths visible at compile time.
- Reflection breaks static analysis - the trimmer can't see what reflection will access at runtime.
- DynamicallyAccessedMembers creates contracts - it tells the trimmer what needs to be preserved.
- Requirements flow backward - from reflection usage back to the source of the
Typevalue. - RequiresUnreferencedCode documents incompatibility - use it when code can't be made analyzable.
- Attributes aren't just hints - the trimmer enforces contracts and produces warnings when they can't be met.
Next steps
- Fix trim warnings - Apply these concepts to resolve warnings in your code
- Prepare libraries for trimming - Make your libraries trim-compatible
- Trim warning reference - Detailed information about specific warnings