Introduction to trim warnings
Conceptually, trimming is simple: when you publish an application, the .NET SDK analyzes the entire application and removes all unused code. However, it can be difficult to determine what is unused, or more precisely, what is used.
To prevent changes in behavior when trimming applications, the .NET SDK provides static analysis of trim compatibility through trim warnings. The trimmer produces trim warnings when it finds code that might not be compatible with trimming. Code that's not trim-compatible can produce behavioral changes, or even crashes, in an application after it has been trimmed. Ideally, all applications that use trimming shouldn't produce any trim warnings. If there are any trim warnings, the app should be thoroughly tested after trimming to ensure that there are no behavior changes.
This article helps you understand why some patterns produce trim warnings, and how these warnings can be addressed.
Examples of trim warnings
For most C# code, it's straightforward to determine what code is used and what code is unused—the trimmer can walk method calls, field and property references, and so on, and determine what code is accessed. Unfortunately, some features, like reflection, present a significant problem. Consider the following code:
string s = Console.ReadLine();
Type type = Type.GetType(s);
foreach (var m in type.GetMethods())
{
Console.WriteLine(m.Name);
}
In this example, GetType() dynamically requests a type with an unknown name, and then prints the names of all of its methods. Because there's no way to know at publish-time what type name is going to be used, there's no way for the trimmer to know which type to preserve in the output. It's likely that this code could have worked before trimming (as long as the input is something known to exist in the target framework), but would probably produce a null reference exception after trimming, as Type.GetType
returns null when the type isn't found.
In this case, the trimmer issues a warning on the call to Type.GetType
, indicating that it can't determine which type is going to be used by the application.
Reacting to trim warnings
Trim warnings are meant to bring predictability to trimming. There are two large categories of warnings that you'll likely see:
- Functionality isn't compatible with trimming
- Functionality has certain requirements on the input to be trim compatible
Functionality incompatible with trimming
These are typically methods that either don't work at all, or might be broken in some cases if they're used in a trimmed application. A good example is the Type.GetType
method from the previous example. In a trimmed app it might work, but there's no guarantee. Such APIs are marked with RequiresUnreferencedCodeAttribute.
RequiresUnreferencedCodeAttribute is simple and broad: it's an attribute that means the member has been annotated incompatible with trimming. This attribute is used when code is fundamentally not trim compatible, or the trim dependency is too complex to explain to the trimmer. This would often be true for methods that dynamically load code for example via LoadFrom(String), enumerate or search through all types in an application or assembly for example via GetType(), use the C# dynamic
keyword, or use other runtime code generation technologies. An example would be:
[RequiresUnreferencedCode("This functionality is not compatible with trimming. Use 'MethodFriendlyToTrimming' instead")]
void MethodWithAssemblyLoad()
{
...
Assembly.LoadFrom(...);
...
}
void TestMethod()
{
// IL2026: Using method 'MethodWithAssemblyLoad' which has 'RequiresUnreferencedCodeAttribute'
// can break functionality when trimming application code. This functionality is not compatible with trimming. Use 'MethodFriendlyToTrimming' instead.
MethodWithAssemblyLoad();
}
There aren't many workarounds for RequiresUnreferencedCode
. The best fix is to avoid calling the method at all when trimming and use something else that's trim-compatible.
Mark functionality as incompatible with trimming
If you're writing a library and it's not in your control whether or not to use incompatible functionality, you can mark it with RequiresUnreferencedCode
. This annotates your method as incompatible with trimming. Using RequiresUnreferencedCode
silences all trim warnings in the given method, but produces a warning whenever someone else calls it.
The RequiresUnreferencedCodeAttribute requires you to specify a Message
. The message is shown as part of a warning reported to the developer who calls the marked method. For example:
IL2026: Using member <incompatible method> which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. <The message value>
With the example above, a warning for a specific method might look like this:
IL2026: Using member 'MethodWithAssemblyLoad()' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. This functionality is not compatible with trimming. Use 'MethodFriendlyToTrimming' instead.
Developers calling such APIs are generally not going to be interested in the particulars of the affected API or specifics as it relates to trimming.
A good message should state what functionality isn't compatible with trimming and then guide the developer what are their potential next steps. It might suggest to use a different functionality or change how the functionality is used. It might also simply state that the functionality isn't yet compatible with trimming without a clear replacement.
If the guidance to the developer becomes too long to be included in a warning message, you can add an optional Url
to the RequiresUnreferencedCodeAttribute to point the developer to a web page describing the problem and possible solutions in greater detail.
For example:
[RequiresUnreferencedCode("This functionality is not compatible with trimming. Use 'MethodFriendlyToTrimming' instead", Url = "https://site/trimming-and-method")]
void MethodWithAssemblyLoad() { ... }
This produces a warning:
IL2026: Using member 'MethodWithAssemblyLoad()' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. This functionality is not compatible with trimming. Use 'MethodFriendlyToTrimming' instead. https://site/trimming-and-method
Using RequiresUnreferencedCode
often leads to marking more methods with it, due to the same reason. This is common when a high-level method becomes incompatible with trimming because it calls a low-level method that isn't trim-compatible. You "bubble up" the warning to a public API. Each usage of RequiresUnreferencedCode
needs a message, and in these cases the messages are likely the same. To avoid duplicating strings and making it easier to maintain, use a constant string field to store the message:
class Functionality
{
const string IncompatibleWithTrimmingMessage = "This functionality is not compatible with trimming. Use 'FunctionalityFriendlyToTrimming' instead";
[RequiresUnreferencedCode(IncompatibleWithTrimmingMessage)]
private void ImplementationOfAssemblyLoading()
{
...
}
[RequiresUnreferencedCode(IncompatibleWithTrimmingMessage)]
public void MethodWithAssemblyLoad()
{
ImplementationOfAssemblyLoading();
}
}
Functionality with requirements on its input
Trimming provides APIs to specify more requirements on input to methods and other members that lead to trim-compatible code. These requirements are usually about reflection and the ability to access certain members or operations on a type. Such requirements are specified using the DynamicallyAccessedMembersAttribute.
Unlike RequiresUnreferencedCode
, reflection can sometimes be understood by the trimmer as long as it's annotated correctly. Let's take another look at the original example:
string s = Console.ReadLine();
Type type = Type.GetType(s);
foreach (var m in type.GetMethods())
{
Console.WriteLine(m.Name);
}
In the previous example, the real problem is Console.ReadLine()
. Because any type could be read, the trimmer has no way to know if you need methods on System.DateTime
or System.Guid
or any other type. On the other hand, the following code would be fine:
Type type = typeof(System.DateTime);
foreach (var m in type.GetMethods())
{
Console.WriteLine(m.Name);
}
Here the trimmer can see the exact type being referenced: System.DateTime
. Now it can use flow analysis to determine that it needs to keep all public methods on System.DateTime
. So where does DynamicallyAccessMembers
come in? When reflection is split across multiple methods. In the following code, we can see that the type System.DateTime
flows to Method3
where reflection is used to access System.DateTime
's methods,
void Method1()
{
Method2<System.DateTime>();
}
void Method2<T>()
{
Type t = typeof(T);
Method3(t);
}
void Method3(Type type)
{
var methods = type.GetMethods();
...
}
If you compile the previous code, the following warning is produced:
IL2070: Program.Method3(Type): 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicMethods' in call to 'System.Type.GetMethods()'. The parameter 'type' of method 'Program.Method3(Type)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to.
For performance and stability, flow analysis isn't performed between methods, so an annotation is needed to pass information between methods, from the reflection call (GetMethods
) to the source of the Type
. In the previous example, the trimmer warning is saying that GetMethods
requires the Type
object instance it's called on to have the PublicMethods
annotation, but the type
variable doesn't have the same requirement. In other words, we need to pass the requirements from GetMethods
up to the caller:
void Method1()
{
Method2<System.DateTime>();
}
void Method2<T>()
{
Type t = typeof(T);
Method3(t);
}
void Method3(
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type)
{
var methods = type.GetMethods();
...
}
After annotating the parameter type
, the original warning disappears, but another appears:
IL2087: 'type' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicMethods' in call to 'Program.Method3(Type)'. The generic parameter 'T' of 'Program.Method2<T>()' does not have matching annotations.
We propagated annotations up to the parameter type
of Method3
, in Method2
we have a similar issue. The trimmer is able to track the value T
as it flows through the call to typeof
, is assigned to the local variable t
, and passed to Method3
. At that point it sees that the parameter type
requires PublicMethods
but there are no requirements on T
, and produces a new warning. To fix this, we must "annotate and propagate" by applying annotations all the way up the call chain until we reach a statically known type (like System.DateTime
or System.Tuple
), or another annotated value. In this case, we need to annotate the type parameter T
of Method2
.
void Method1()
{
Method2<System.DateTime>();
}
void Method2<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] T>()
{
Type t = typeof(T);
Method3(t);
}
void Method3(
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type)
{
var methods = type.GetMethods();
...
}
Now there are no warnings because the trimmer knows which members might be accessed via runtime reflection (public methods) and on which types (System.DateTime
), and it preserves them. It's best practice to add annotations so the trimmer knows what to preserve.
Warnings produced by these extra requirements are automatically suppressed if the affected code is in a method with RequiresUnreferencedCode
.
Unlike RequiresUnreferencedCode
, which simply reports the incompatibility, adding DynamicallyAccessedMembers
makes the code compatible with trimming.
Note
Using DynamicallyAccessedMembersAttribute
will root all the specified DynamicallyAccessedMemberTypes
members of the type. This means it will keep the members, as well as any metadata referenced by those members. This can lead to much larger apps than expected. Be careful to use the minimum DynamicallyAccessedMemberTypes
required.
Suppressing trimmer warnings
If you can somehow determine that the call is safe, and all the code that's needed won't be trimmed away, you can also suppress the warning using UnconditionalSuppressMessageAttribute. For example:
[RequiresUnreferencedCode("Use 'MethodFriendlyToTrimming' instead")]
void MethodWithAssemblyLoad() { ... }
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode",
Justification = "Everything referenced in the loaded assembly is manually preserved, so it's safe")]
void TestMethod()
{
InitializeEverything();
MethodWithAssemblyLoad(); // Warning suppressed
ReportResults();
}
Warning
Be very careful when suppressing trim warnings. It's possible that the call may be trim-compatible now, but as you change your code that may change, and you may forget to review all the suppressions.
UnconditionalSuppressMessage
is like SuppressMessage
but it can be seen by publish
and other post-build tools.
Important
Do not use SuppressMessage
or #pragma warning disable
to suppress trimmer warnings. These only work for the compiler, but are not preserved in the compiled assembly. Trimmer operates on compiled assemblies and would not see the suppression.
The suppression applies to the entire method body. So in our sample above it suppresses all IL2026
warnings from the method. This makes it harder to understand, as it's not clear which method is the problematic one, unless you add a comment. More importantly, if the code changes in the future, such as if ReportResults
becomes trim-incompatible as well, no warning is reported for this method call.
You can resolve this by refactoring the problematic method call into a separate method or local function and then applying the suppression to just that method:
void TestMethod()
{
InitializeEverything();
CallMethodWithAssemblyLoad();
ReportResults();
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode",
Justification = "Everything referenced in the loaded assembly is manually preserved, so it's safe")]
void CallMethodWithAssemblyLoad()
{
MethodWIthAssemblyLoad(); // Warning suppressed
}
}