Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
194 changes: 194 additions & 0 deletions docs/fixing-warnings.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@

# Trim warnings in .NET 6

[In .NET Core 3.1 and 5.0 we introduced
trimming](https://devblogs.microsoft.com/dotnet/app-trimming-in-net-5/) as a new preview feature
for self-contained .NET core applications. Conceptually the feature is very simple: when
publishing the application the .NET SDK analyzes the entire application and removes all unused
code. In the time trimming has been in preview, we've learned that trimming is very powerful --
it can reduce application size by half or more. However, we've also learned about the
difficulties in safely trimming applications.

The most difficult question in trimming is what is unused, or more precisely, what is used. For
most standard C# code this is trivial -- the trimmer can easily walk method calls, field and
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is "the trimmer" ? We have not been using that term and most developers are familiar with linker term. Do we really have to confuse the community with another new name?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In all locations other than mono/linker we've been trying to avoid the term "linker" because most people understand it as the "platform linker". We use "trim tool" or sometimes "trimmer" (I would prefer "trim tool" if possible).
For the same reason we use "trimming" and not "linking" everywhere.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm intentionally "ignoring" the mono/Xamarin side of things - those will likely use "linker" a lot. Most of our docs and blog posts use "Trimming" - we also use it everywhere in the messages (RUC) in the framework. I don't see us going back to "linking".

That said - I understand the confusion for Xamarin users - we might want to mention this on the first use of the term.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was not referring to trimming but to "trimmer". Even .NET Core MSDN documentation is written in the way that it uses the term since 3.1.

.NET Core 3+ ships with an IL Linker which allows you to reduce the size of the self-contained application. It automatically determines which assemblies are needed and copies only them.

I'm really not sure we have to rename everything to cause more confusion.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would probably just not give it a name here. We can update the sentence to say "For most standard C# code this is trivial -- the trimmerwe can easily walk method calls, field and"

That said,

https://en.wikipedia.org/wiki/Linker_(computing)

This is the definition of the term used in computer engineering. It's not what IL Linker is doing. It's confusing to anyone who has ever used a linker in the past.

Reusing this term in .NET was a mistake. We should have gone with something like Remover 8000. Now Xamarin often needs to qualify the word "linker" with the word "native" when referring to what everyone outside .NET calls "linker". It's a pretty sad place to be in and puts us (the authors of .NET) into a bad light (trying to steal a well-established term).

I've seen this cause confusion in the past. "Linker failed. Oh, not that one, the other one."

We should phase this name out before it moves out of the niche parts of .NET (both Xamarin and Blazor-WASM are still niches).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My use of the term "trimmer" here is in its most generic sense -- it is the thing which "trims." The other documentation about a specific tool which is responsible for trimming is incorrect in this case because the only supported method of trimming applications in .NET 6 will be passing PublishTrimmed=true to the SDK. Rather than talk specifically about the SDK, I wanted to just use a generic term.

I don't really see the need to get more specific here and talk about any specific tool. The term "linker" is certainly incorrect for .NET 6 since we do not plan on shipping a separate linker tool for supported scenarios.

property references, etc, and determine what code is used. Unfortunately, some features, like
reflection, present a significant problem. Consider the following code:

```C#
string s = Console.ReadLine();
Type type = Type.GetType(s);
foreach (var m in type.GetMethods())
{
Console.WriteLine(m.Name);
}
```

In this example, `Type.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 very 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 (due to `Type.GetType` returning null).

This is a frustrating situation. Trimming often works just fine but occasionally it can produce
breaking behavior, sometimes in rare code paths, and it can be very hard to trace down the cause.

For .NET 6 we want to bring a new feature to trimming: trim warnings. Trim warnings happen
because the trimmer sees a call which may access other code in the app but the trimmer can't
determine which code. This could mean that the trimmer would trim away code which is used at
runtime.

## Reacting to trim warnings

Trim warnings are meant to bring predictability to trimming. The problem with trimming is that some
code patterns can depend on code in a way that isn't understandable by the trimmer. Whenever the
trimmer encounters code like that, that's when you should expect a trim warning.

There are two big categories of warnings which you will likely see:

1. `RequiresUnreferencedCode`
2. `DynamicallyAccessedMembers`

### RequiresUnreferencedCode

[RequiresUnreferencedCode](https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.codeanalysis.requiresunreferencedcodeattribute?view=net-5.0) is simple: it's an attribute that can be placed on methods to indicate
that the method is not trim-compatible, meaning that it might use reflection or some other
mechanism to access code that may be trimmed away. This attribute is used when it's not possible
for the trimmer to understand what's necessary, and a blanket warning is needed. This would often
be true for methods which use the C# `dynamic` keyword, `Assembly.LoadFrom`, or other runtime
code generation technologies.
An example would be:

```C#
[RequiresUnreferencedCode("Use 'MethodFriendlyToTrimming' instead")]
void MethodWithAssemblyLoad() { ... }

void TestMethod()
{
// IL2026: Using method 'MethodWithAssemblyLoad' which has 'RequiresUnreferencedCodeAttribute'
// can break functionality when trimming application code. Use 'MethodFriendlyToTrimming' instead.
MethodWithAssemblyLoad();
}
```

There aren't many workarounds for `RequiresUnreferencedCode`. The best way is to avoid calling
the method at all when trimming and use something else which is trim-compatible. If you're
writing a library and it's not in your control whether or not to call the method and you just
want to communicate to *your* caller, you can also add `RequiresUnreferencedCode` to your own
method. This silences all trimming warnings in your code, but will produce a warning whenever

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does this produce a warning rather than an error? If the method is marked that its not going to work, and is called, then why only warn?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because in almost all cases the method might work in some cases and might not in others. For example XmlSerializer.Serialize(object o) - it's definitely not trim compatible, but if you make sure that the type passed in is preserved enough - it will work just fine.

We use this attribute basically to say "If you ignore this, you're on your own and will have to test at runtime".

There are also cases where the method is sooo close to being trim compatible except for some weird special case - we can't mark it as compatible (since it's not 100%), but it almost always is.

And as with any other trim analysis - the linker plays it super safe, the annotations do that as well. We don't want to block people completely if we are too careful.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pretty much everything in trimming is a warning, because many things just happen to work. For instance, if you call a method that has RUC does a lot of reflection on its parameter, but the parameter type is in user code (and therefore untrimmed) and nothing important from the framework was removed, it all just works.

Of course, this is just luck, which is why the warning is there.

someone calls your method.

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](https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.codeanalysis.unconditionalsuppressmessageattribute?view=net-5.0).
For example:

```C#
[RequiresUnreferencedCode("Use 'MethodFriendlyToTrimming' instead")]
void MethodWithAssemblyLoad() { ... }

[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode",
Justification = "Everything referenced in the loaded assembly is manually preserved, so it's safe")]
Copy link
Contributor

@tlakollo tlakollo Apr 9, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we explain/link to how to preserve things manually? If I were to read this blog post looking for answers on how to solve my current broken app I would see that I can:

  • Use other function that is trim compatible (which is not always possible)
  • Mark my method with RequiresUnreferencedCode which changes the warning location but my app would still be broken
  • Preserve manually and Suppress the warning, the blog explains how to suppress the warning, but not how to preserve manually.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A good question, but not really a post I'm prepared to write.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well just in case someone reads these comments, an XML descriptor file or DynamicDependencyAttribute could help to preserve stuff on the app

void TestMethod()
{
MethodWithAssemblyLoad(); // Warning suppressed
}
```

`UnconditionalSuppressMessage` is like `SuppressMessage` but it is preserved into IL, so the
trimmer can see the suppression even after build and publish. `SuppressMessage` and `#pragma`
directives are only present in source so they can't be used to silence warnings from the
trimmer. 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.

### DynamicallyAccessedMembers

[DynamicallyAccessedMembers](https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.codeanalysis.dynamicallyaccessedmembersattribute?view=net-5.0) is usually about reflection. 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:

```C#
string s = Console.ReadLine();
Type type = Type.GetType(s);
foreach (var m in type.GetMethods())
{
Console.WriteLine(m.Name);
}
```

In the example above, 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.Tuple` or `System.Guid`
or any other type. On the other hand, if your code looked like,

```C#
Type type = typeof(System.Tuple);
foreach (var m in type.GetMethods())
{
Console.WriteLine(m.Name);
}
```

This would be fine. Here the trimmer can see the exact type being referenced: `System.Tuple`. Now
it can use flow analysis to determine that it needs to keep all public methods. So where does
`DynamicallyAccessMembers` come in? What happens if the reflection is split across methods?

```C#
void Method1()
{
Method2(typeof(System.Tuple));
}
void Method2(Type type)
{
var methods = type.GetMethods();
...
}
```

If you compile the above, now you see a warning:

```
Trim analysis warning IL2070: net6.Program.Method2(Type): 'this' argument does not satisfy
'DynamicallyAccessedMemberTypes.PublicMethods' in call to 'System.Type.GetMethods()'. The
parameter 'type' of method 'net6.Program.Method2(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 annotation is needed to pass information upward, from the reflection call
(`GetMethods`) to the source of the `Type` (`typeof`). In the above example, the trimmer warning
is saying that `GetMethods` requires the `PublicMethods` annotation on types, 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:

```C#
void Method1()
{
Method2(typeof(System.Tuple));
}
void Method2(
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type)
{
var methods = type.GetMethods();
...
}
```

Now the warning disappears, because the trimmer knows exactly which members to preserve, and
which type(s) to preserve them on. In general, this is the best way to deal with
`DynamicallyAccessedMembers` warnings: add annotations so the trimmer knows what to preserve.

As with `RequiresUnreferencedCode` warnings, adding `RequiresUnreferencedCode` or
`UnconditionalSuppressMessage` attributes also works, but none of these options make the code
compatible with trimming, while adding `DynamicallyAccessedMembers` does.

## Conclusion

This description should cover the most common situations you end up in while trimming your
application. Over time we'll continue to improve the diagnostic experience and tooling.

As we continue developing trimming we hope to see more code that's fully annotated, so users can
trim with confidence. Because trimming involves the whole application, trimming is as much a
feature of the ecosystem as it is of the product and we're depending on all developers to help
improve the ecosystem.