Muokkaa

Jaa


Prepare .NET libraries for trimming

The .NET SDK makes it possible to reduce the size of self-contained apps by trimming. Trimming removes unused code from the app and its dependencies. Not all code is compatible with trimming. .NET provides trim analysis warnings to detect patterns that might break trimmed apps. This article:

Prerequisites

.NET 8 SDK or later.

Enable library trim warnings

Trim warnings in a library can be found with either of the following methods:

  • Enabling project-specific trimming using the IsTrimmable property.
  • Creating a trimming test app that uses the library and enabling trimming for the test app. It's not necessary to reference all the APIs in the library.

We recommend using both approaches. Project-specific trimming is convenient and shows trim warnings for one project, but relies on the references being marked trim-compatible to see all warnings. Trimming a test app is more work, but shows all warnings.

Enable project-specific trimming

Set <IsTrimmable>true</IsTrimmable> in the project file.

<PropertyGroup>
    <IsTrimmable>true</IsTrimmable>
</PropertyGroup>

Setting the MSBuild property IsTrimmable to true marks the assembly as "trimmable" and enables trim warnings. "Trimmable" means the project:

  • Is considered compatible with trimming.
  • Shouldn't generate trim-related warnings when building. When used in a trimmed app, the assembly has its unused members trimmed in the final output.

The IsTrimmable property defaults to true when configuring a project as AOT-compatible with <IsAotCompatible>true</IsAotCompatible>. For more information, see AOT-compatibility analyzers.

To generate trim warnings without marking the project as trim-compatible, use <EnableTrimAnalyzer>true</EnableTrimAnalyzer> rather than <IsTrimmable>true</IsTrimmable>.

Show all warnings with test app

To show all analysis warnings for a library, the trimmer must analyze the implementation of the library and of all dependencies the library uses.

When building and publishing a library:

  • The implementations of the dependencies aren't available.
  • The available reference assemblies don't have enough information for the trimmer to determine if they're compatible with trimming.

Because of the dependency limitations, a self-contained test app which uses the library and its dependencies must be created. The test app includes all the information the trimmer requires to issue warning on trim incompatibilities in:

  • The library code.
  • The code that the library references from its dependencies.

Note

If the library has different behavior depending on the target framework, create a trimming test app for each of the target frameworks that support trimming. For example, if the library uses conditional compilation such as #if NET7_0 to change behavior.

To create the trimming test app:

  • Create a separate console application project.
  • Add a reference to the library.
  • Modify the project similar to the project shown below using the following list:

If library targets a TFM that is not trimmable, for example net472 or netstandard2.0, there's no benefit to creating a trimming test app. Trimming is only supported for .NET 6 and later.

  • Add <PublishTrimmed>true</PublishTrimmed>.
  • Add a reference to the library project with <ProjectReference Include="/Path/To/YourLibrary.csproj" />.
  • Specify the library as a trimmer root assembly with <TrimmerRootAssembly Include="YourLibraryName" />.
    • TrimmerRootAssembly ensures that every part of the library is analyzed. It tells the trimmer that this assembly is a "root". A "root" assembly means the trimmer analyzes every call in the library and traverses all code paths that originate from that assembly.

.csproj file

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <PublishTrimmed>true</PublishTrimmed>
  </PropertyGroup>

  <ItemGroup>
    <ProjectReference Include="..\MyLibrary\MyLibrary.csproj" />
    <TrimmerRootAssembly Include="MyLibrary" />
  </ItemGroup>

</Project>

Once the project file is updated, run dotnet publish with the target runtime identifier (RID).

dotnet publish -c Release -r <RID>

Follow the preceding pattern for multiple libraries. To see trim analysis warnings for more than one library at a time, add them all to the same project as ProjectReference and TrimmerRootAssembly items. Adding all the libraries to the same project with ProjectReference and TrimmerRootAssembly items warns about dependencies if any of the root libraries use a trim-unfriendly API in a dependency. To see warnings that have to do with only a particular library, reference that library only.

Note

The analysis results depend on the implementation details of the dependencies. Updating to a new version of a dependency might introduce analysis warnings:

  • If the new version added non-understood reflection patterns.
  • Even if there were no API changes.
  • Introducing trim analysis warnings is a breaking change when the library is used with PublishTrimmed.

Resolve trim warnings

The preceding steps produce warnings about code that might cause problems when used in a trimmed app. The following examples show the most common warnings with recommendations for fixing them.

RequiresUnreferencedCode

Consider the following code that uses [RequiresUnreferencedCode] to indicate that the specified method requires dynamic access to code that is not referenced statically, for example, through System.Reflection.

public class MyLibrary
{
    public static void MyMethod()
    {
        // warning IL2026 :
        // MyLibrary.MyMethod: Using 'MyLibrary.DynamicBehavior'
        // which has [RequiresUnreferencedCode] can break functionality
        // when trimming app code.
        DynamicBehavior();
    }

    [RequiresUnreferencedCode(
        "DynamicBehavior is incompatible with trimming.")]
    static void DynamicBehavior()
    {
    }
}

The preceding highlighted code indicates the library calls a method that has explicitly been annotated as incompatible with trimming. To get rid of the warning, consider whether MyMethod needs to call DynamicBehavior. If so, annotate the caller MyMethod with [RequiresUnreferencedCode] which propagates the warning so that callers of MyMethod get a warning instead:

public class MyLibrary
{
    [RequiresUnreferencedCode("Calls DynamicBehavior.")]
    public static void MyMethod()
    {
        DynamicBehavior();
    }

    [RequiresUnreferencedCode(
        "DynamicBehavior is incompatible with trimming.")]
    static void DynamicBehavior()
    {
    }
}

Once you have propagated up the attribute all the way to public API, apps calling the library:

  • Get warnings only for public methods that aren't trimmable.
  • Don't get warnings like IL2104: Assembly 'MyLibrary' produced trim warnings.

DynamicallyAccessedMembers

public class MyLibrary3
{
    static void UseMethods(Type type)
    {
        // warning IL2070: MyLibrary.UseMethods(Type): 'this' argument does not satisfy
        // 'DynamicallyAccessedMemberTypes.PublicMethods' in call to
        // 'System.Type.GetMethods()'.
        // The parameter 't' of method 'MyLibrary.UseMethods(Type)' doesn't have
        // matching annotations.
        foreach (var method in type.GetMethods())
        {
            // ...
        }
    }
}

In the preceding code, UseMethods is calling a reflection method that has a [DynamicallyAccessedMembers] requirement. The requirement states that the type's public methods are available. Satisfy the requirement by adding the same requirement to the parameter of UseMethods.

static void UseMethods(
   // State the requirement in the UseMethods parameter.
   [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type)
{
    // ...
}

Now any calls to UseMethods produce warnings if they pass in values that don't satisfy the PublicMethods requirement. Similar to [RequiresUnreferencedCode], once you have propagated up such warnings to public APIs, you're done.

In the following example, an unknown Type flows into the annotated method parameter. The unknown Type is from a field:

static Type type;
static void UseMethodsHelper()
{
    // warning IL2077: MyLibrary.UseMethodsHelper(Type): 'type' argument does not satisfy
    // 'DynamicallyAccessedMemberTypes.PublicMethods' in call to
    // 'MyLibrary.UseMethods(Type)'.
    // The field 'System.Type MyLibrary::type' does not have matching annotations.
    UseMethods(type);
}

Similarly, here the problem is that the field type is passed into a parameter with these requirements. It's fixed by adding [DynamicallyAccessedMembers] to the field. [DynamicallyAccessedMembers] warns about code that assigns incompatible values to the field. Sometimes this process continues until a public API is annotated, and other times it ends when a concrete type flows into a location with these requirements. For example:

[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)]
static Type type;

static void UseMethodsHelper()
{
    MyLibrary.type = typeof(System.Tuple);
}

In this case, the trim analysis keeps public methods of Tuple, and produces further warnings.

Recommendations

  • Avoid reflection when possible. When using reflection, minimize reflection scope so that it's reachable only from a small part of the library.
  • Annotate code with DynamicallyAccessedMembers to statically express the trimming requirements when possible.
  • Consider reorganizing code to make it follow an analyzable pattern that can be annotated with DynamicallyAccessedMembers
  • When code is incompatible with trimming, annotate it with RequiresUnreferencedCode and propagate this annotation to callers until the relevant public APIs are annotated.
  • Avoid using code that uses reflection in a way not understood by the static analysis. For example, reflection in static constructors should be avoided. Using statically unanalyzable reflection in static constructors result in the warning propagating to all members of the class.
  • Avoid annotating virtual methods or interface methods. Annotating virtual or interface methods requires all overrides to have matching annotations.
  • If an API is mostly trim-incompatible, alternative coding approaches to the API might need to be considered. A common example is reflection-based serializers. In these cases, consider adopting other technology like source generators to produce code that is more easily statically analyzed. For example, see How to use source generation in System.Text.Json

Resolve warnings for non-analyzable patterns

It's better to resolve warnings by expressing the intent of your code using [RequiresUnreferencedCode] and DynamicallyAccessedMembers when possible. However, in some cases, you might be interested in enabling trimming of a library that uses patterns that can't be expressed with those attributes, or without refactoring existing code. This section describes some advanced ways to resolve trim analysis warnings.

Warning

These techniques might change the behavior or your code or result in run time exceptions if used incorrectly.

UnconditionalSuppressMessage

Consider code that:

  • The intent can't be expressed with the annotations.
  • Generates a warning but doesn't represent a real issue at run time.

The warnings can be suppressed by UnconditionalSuppressMessageAttribute. This is similar to SuppressMessageAttribute, but it's persisted in IL and respected during trim analysis.

Warning

When suppressing warnings, you are responsible for guaranteeing the trim compatibility of the code based on invariants that you know to be true by inspection and testing. Use caution with these annotations, because if they are incorrect, or if invariants of your code change, they might end up hiding incorrect code.

For example:

class TypeCollection
{
    Type[] types;

    // Ensure that only types with preserved constructors are stored in the array
    [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
    public Type this[int i]
    {
        // warning IL2063: TypeCollection.Item.get: Value returned from method
        // 'TypeCollection.Item.get' can't be statically determined and may not meet
        // 'DynamicallyAccessedMembersAttribute' requirements.
        get => types[i];
        set => types[i] = value;
    }
}

class TypeCreator
{
    TypeCollection types;

    public void CreateType(int i)
    {
        types[i] = typeof(TypeWithConstructor);
        Activator.CreateInstance(types[i]); // No warning!
    }
}

class TypeWithConstructor
{
}

In the preceding code, the indexer property has been annotated so that the returned Type meets the requirements of CreateInstance. This ensures that the TypeWithConstructor constructor is kept, and that the call to CreateInstance doesn't warn. The indexer setter annotation ensures that any types stored in the Type[] have a constructor. However, the analysis isn't able to see this and produces a warning for the getter, because it doesn't know that the returned type has its constructor preserved.

If you're sure that the requirements are met, you can silence this warning by adding [UnconditionalSuppressMessage] to the getter:

class TypeCollection
{
    Type[] types;

    // Ensure that only types with preserved constructors are stored in the array
    [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
    public Type this[int i]
    {
        [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2063",
            Justification = "The list only contains types stored through the annotated setter.")]
        get => types[i];
        set => types[i] = value;
    }
}

class TypeCreator
{
    TypeCollection types;

    public void CreateType(int i)
    {
        types[i] = typeof(TypeWithConstructor);
        Activator.CreateInstance(types[i]); // No warning!
    }
}

class TypeWithConstructor
{
}

It's important to underline that it's only valid to suppress a warning if there are annotations or code that ensure the reflected-on members are visible targets of reflection. It isn't sufficient that the member was a target of a call, field, or property access. It might appear to be the case sometimes, but such code is bound to break eventually as more trimming optimizations are added. Properties, fields, and methods that aren't visible targets of reflection could be inlined, have their names removed, get moved to different types, or otherwise be optimized in ways that break reflecting on them. When suppressing a warning, it's only permissible to reflect on targets that were visible targets of reflection to the trimming analyzer elsewhere.

// Invalid justification and suppression: property being non-reflectively
// used by the app doesn't guarantee that the property will be available
// for reflection. Properties that are not visible targets of reflection
// are already optimized away with Native AOT trimming and may be
// optimized away for non-native deployment in the future as well.
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2063",
    Justification = "*INVALID* Only need to serialize properties that are used by"
                    + "the app. *INVALID*")]
public string Serialize(object o)
{
    StringBuilder sb = new StringBuilder();
    foreach (var property in o.GetType().GetProperties())
    {
        AppendProperty(sb, property, o);
    }
    return sb.ToString();
}

DynamicDependency

The [DynamicDependency] attribute can be used to indicate that a member has a dynamic dependency on other members. This results in the referenced members being kept whenever the member with the attribute is kept, but doesn't silence warnings on its own. Unlike the other attributes, which inform the trim analysis about the reflection behavior of the code, [DynamicDependency] only keeps other members. This can be used together with [UnconditionalSuppressMessage] to fix some analysis warnings.

Warning

Use [DynamicDependency] attribute only as a last resort when the other approaches aren't viable. It is preferable to express the reflection behavior using [RequiresUnreferencedCode] or [DynamicallyAccessedMembers].

[DynamicDependency("Helper", "MyType", "MyAssembly")]
static void RunHelper()
{
    var helper = Assembly.Load("MyAssembly").GetType("MyType").GetMethod("Helper");
    helper.Invoke(null, null);
}

Without DynamicDependency, trimming might remove Helper from MyAssembly or remove MyAssembly completely if it's not referenced elsewhere, producing a warning that indicates a possible failure at run time. The attribute ensures that Helper is kept.

The attribute specifies the members to keep via a string or via DynamicallyAccessedMemberTypes. The type and assembly are either implicit in the attribute context, or explicitly specified in the attribute (by Type, or by strings for the type and assembly name).

The type and member strings use a variation of the C# documentation comment ID string format, without the member prefix. The member string shouldn't include the name of the declaring type, and might omit parameters to keep all members of the specified name. Some examples of the format are shown in the following code:

[DynamicDependency("MyMethod()")]
[DynamicDependency("MyMethod(System,Boolean,System.String)")]
[DynamicDependency("MethodOnDifferentType()", typeof(ContainingType))]
[DynamicDependency("MemberName")]
[DynamicDependency("MemberOnUnreferencedAssembly", "ContainingType"
                                                 , "UnreferencedAssembly")]
[DynamicDependency("MemberName", "Namespace.ContainingType.NestedType", "Assembly")]
// generics
[DynamicDependency("GenericMethodName``1")]
[DynamicDependency("GenericMethod``2(``0,``1)")]
[DynamicDependency(
    "MethodWithGenericParameterTypes(System.Collections.Generic.List{System.String})")]
[DynamicDependency("MethodOnGenericType(`0)", "GenericType`1", "UnreferencedAssembly")]
[DynamicDependency("MethodOnGenericType(`0)", typeof(GenericType<>))]

The [DynamicDependency] attribute is designed to be used in cases where a method contains reflection patterns that can't be analyzed even with the help of DynamicallyAccessedMembersAttribute.