Edit

Share via


Understanding trim analysis

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 type will 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:

  1. PrintMethodNames requires its parameter to have PublicMethods preserved.
  2. The call site passes typeof(DateTime).
  3. 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 dynamic keyword.

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:

  1. Suppresses warnings inside the method: The trimmer won't analyze or warn about the reflection usage.
  2. 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 PublicMethodstype parameter needs PublicMethods → generic T needs PublicMethodsDateTime 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 Type value.
  • 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