|
| 1 | + |
| 2 | +# Trim warnings in .NET 6 |
| 3 | + |
| 4 | +[In .NET Core 3.1 and 5.0 we introduced |
| 5 | +trimming](https://devblogs.microsoft.com/dotnet/app-trimming-in-net-5/) as a new preview feature |
| 6 | +for self-contained .NET core applications. Conceptually the feature is very simple: when |
| 7 | +publishing the application the .NET SDK analyzes the entire application and removes all unused |
| 8 | +code. In the time trimming has been in preview, we've learned that trimming is very powerful -- |
| 9 | +it can reduce application size by half or more. However, we've also learned about the |
| 10 | +difficulties in safely trimming applications. |
| 11 | + |
| 12 | +The most difficult question in trimming is what is unused, or more precisely, what is used. For |
| 13 | +most standard C# code this is trivial -- the trimmer can easily walk method calls, field and |
| 14 | +property references, etc, and determine what code is used. Unfortunately, some features, like |
| 15 | +reflection, present a significant problem. Consider the following code: |
| 16 | + |
| 17 | +```C# |
| 18 | +string s = Console.ReadLine(); |
| 19 | +Type type = Type.GetType(s); |
| 20 | +foreach (var m in type.GetMethods()) |
| 21 | +{ |
| 22 | + Console.WriteLine(m.Name); |
| 23 | +} |
| 24 | +``` |
| 25 | + |
| 26 | +In this example, `Type.GetType` dynamically requests a type with an unknown name, and then prints |
| 27 | +the names of all of its methods. Because there's no way to know at publish time what type name is |
| 28 | +going to be used, there's no way for the trimmer to know which type to preserve in the output. |
| 29 | +It's very likely that this code could have worked before trimming (as long as the input is |
| 30 | +something known to exist in the target framework), but would probably produce a null reference |
| 31 | +exception after trimming (due to `Type.GetType` returning null). |
| 32 | + |
| 33 | +This is a frustrating situation. Trimming often works just fine but occasionally it can produce |
| 34 | +breaking behavior, sometimes in rare code paths, and it can be very hard to trace down the cause. |
| 35 | + |
| 36 | +For .NET 6 we want to bring a new feature to trimming: trim warnings. Trim warnings happen |
| 37 | +because the trimmer sees a call which may access other code in the app but the trimmer can't |
| 38 | +determine which code. This could mean that the trimmer would trim away code which is used at |
| 39 | +runtime. |
| 40 | + |
| 41 | +## Reacting to trim warnings |
| 42 | + |
| 43 | +Trim warnings are meant to bring predictability to trimming. The problem with trimming is that some |
| 44 | +code patterns can depend on code in a way that isn't understandable by the trimmer. Whenever the |
| 45 | +trimmer encounters code like that, that's when you should expect a trim warning. |
| 46 | + |
| 47 | +There are two big categories of warnings which you will likely see: |
| 48 | + |
| 49 | + 1. `RequiresUnreferencedCode` |
| 50 | + 2. `DynamicallyAccessedMembers` |
| 51 | + |
| 52 | +### RequiresUnreferencedCode |
| 53 | + |
| 54 | +[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 |
| 55 | +that the method is not trim-compatible, meaning that it might use reflection or some other |
| 56 | +mechanism to access code that may be trimmed away. This attribute is used when it's not possible |
| 57 | +for the trimmer to understand what's necessary, and a blanket warning is needed. This would often |
| 58 | +be true for methods which use the C# `dynamic` keyword, `Assembly.LoadFrom`, or other runtime |
| 59 | +code generation technologies. |
| 60 | +An example would be: |
| 61 | + |
| 62 | +```C# |
| 63 | +[RequiresUnreferencedCode("Use 'MethodFriendlyToTrimming' instead")] |
| 64 | +void MethodWithAssemblyLoad() { ... } |
| 65 | + |
| 66 | +void TestMethod() |
| 67 | +{ |
| 68 | + // IL2026: Using method 'MethodWithAssemblyLoad' which has 'RequiresUnreferencedCodeAttribute' |
| 69 | + // can break functionality when trimming application code. Use 'MethodFriendlyToTrimming' instead. |
| 70 | + MethodWithAssemblyLoad(); |
| 71 | +} |
| 72 | +``` |
| 73 | + |
| 74 | +There aren't many workarounds for `RequiresUnreferencedCode`. The best way is to avoid calling |
| 75 | +the method at all when trimming and use something else which is trim-compatible. If you're |
| 76 | +writing a library and it's not in your control whether or not to call the method and you just |
| 77 | +want to communicate to *your* caller, you can also add `RequiresUnreferencedCode` to your own |
| 78 | +method. This silences all trimming warnings in your code, but will produce a warning whenever |
| 79 | +someone calls your method. |
| 80 | + |
| 81 | +If you can somehow determine that the call is safe, and all the code that's needed won't be |
| 82 | +trimmed away, you can also suppress the warning using |
| 83 | +[UnconditionalSuppressMessageAttribute](https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.codeanalysis.unconditionalsuppressmessageattribute?view=net-5.0). |
| 84 | +For example: |
| 85 | + |
| 86 | +```C# |
| 87 | +[RequiresUnreferencedCode("Use 'MethodFriendlyToTrimming' instead")] |
| 88 | +void MethodWithAssemblyLoad() { ... } |
| 89 | + |
| 90 | +[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", |
| 91 | + Justification = "Everything referenced in the loaded assembly is manually preserved, so it's safe")] |
| 92 | +void TestMethod() |
| 93 | +{ |
| 94 | + MethodWithAssemblyLoad(); // Warning suppressed |
| 95 | +} |
| 96 | +``` |
| 97 | + |
| 98 | +`UnconditionalSuppressMessage` is like `SuppressMessage` but it is preserved into IL, so the |
| 99 | +trimmer can see the suppression even after build and publish. `SuppressMessage` and `#pragma` |
| 100 | +directives are only present in source so they can't be used to silence warnings from the |
| 101 | +trimmer. Be very careful when suppressing trim warnings: it's possible that the call may be |
| 102 | +trim-compatible now, but as you change your code that may change and you may forget to review all |
| 103 | +the suppressions. |
| 104 | + |
| 105 | +### DynamicallyAccessedMembers |
| 106 | + |
| 107 | +[DynamicallyAccessedMembers](https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.codeanalysis.dynamicallyaccessedmembersattribute?view=net-5.0) is usually about reflection. Unlike `RequiresUnreferencedCode`, |
| 108 | +reflection can sometimes be understood by the trimmer as long as it's annotated correctly. |
| 109 | +Let's take another look at the original example: |
| 110 | + |
| 111 | +```C# |
| 112 | +string s = Console.ReadLine(); |
| 113 | +Type type = Type.GetType(s); |
| 114 | +foreach (var m in type.GetMethods()) |
| 115 | +{ |
| 116 | + Console.WriteLine(m.Name); |
| 117 | +} |
| 118 | +``` |
| 119 | + |
| 120 | +In the example above, the real problem is `Console.ReadLine()`. Because *any* type could |
| 121 | +be read the trimmer has no way to know if you need methods on `System.Tuple` or `System.Guid` |
| 122 | +or any other type. On the other hand, if your code looked like, |
| 123 | + |
| 124 | +```C# |
| 125 | +Type type = typeof(System.Tuple); |
| 126 | +foreach (var m in type.GetMethods()) |
| 127 | +{ |
| 128 | + Console.WriteLine(m.Name); |
| 129 | +} |
| 130 | +``` |
| 131 | + |
| 132 | +This would be fine. Here the trimmer can see the exact type being referenced: `System.Tuple`. Now |
| 133 | +it can use flow analysis to determine that it needs to keep all public methods. So where does |
| 134 | +`DynamicallyAccessMembers` come in? What happens if the reflection is split across methods? |
| 135 | + |
| 136 | +```C# |
| 137 | +void Method1() |
| 138 | +{ |
| 139 | + Method2(typeof(System.Tuple)); |
| 140 | +} |
| 141 | +void Method2(Type type) |
| 142 | +{ |
| 143 | + var methods = type.GetMethods(); |
| 144 | + ... |
| 145 | +} |
| 146 | +``` |
| 147 | + |
| 148 | +If you compile the above, now you see a warning: |
| 149 | + |
| 150 | +``` |
| 151 | +Trim analysis warning IL2070: net6.Program.Method2(Type): 'this' argument does not satisfy |
| 152 | +'DynamicallyAccessedMemberTypes.PublicMethods' in call to 'System.Type.GetMethods()'. The |
| 153 | +parameter 'type' of method 'net6.Program.Method2(Type)' does not have matching annotations. The |
| 154 | +source value must declare at least the same requirements as those declared on the target |
| 155 | +location it is assigned to. |
| 156 | +``` |
| 157 | + |
| 158 | +For performance and stability flow analysis isn't performed between |
| 159 | +methods, so annotation is needed to pass information upward, from the reflection call |
| 160 | +(`GetMethods`) to the source of the `Type` (`typeof`). In the above example, the trimmer warning |
| 161 | +is saying that `GetMethods` requires the `PublicMethods` annotation on types, but the `type` |
| 162 | +variable doesn't have the same requirement. In other words, we need to pass the requirements from |
| 163 | +`GetMethods` up to the caller: |
| 164 | + |
| 165 | +```C# |
| 166 | +void Method1() |
| 167 | +{ |
| 168 | + Method2(typeof(System.Tuple)); |
| 169 | +} |
| 170 | +void Method2( |
| 171 | + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type) |
| 172 | +{ |
| 173 | + var methods = type.GetMethods(); |
| 174 | + ... |
| 175 | +} |
| 176 | +``` |
| 177 | + |
| 178 | +Now the warning disappears, because the trimmer knows exactly which members to preserve, and |
| 179 | +which type(s) to preserve them on. In general, this is the best way to deal with |
| 180 | +`DynamicallyAccessedMembers` warnings: add annotations so the trimmer knows what to preserve. |
| 181 | + |
| 182 | +As with `RequiresUnreferencedCode` warnings, adding `RequiresUnreferencedCode` or |
| 183 | +`UnconditionalSuppressMessage` attributes also works, but none of these options make the code |
| 184 | +compatible with trimming, while adding `DynamicallyAccessedMembers` does. |
| 185 | + |
| 186 | +## Conclusion |
| 187 | + |
| 188 | +This description should cover the most common situations you end up in while trimming your |
| 189 | +application. Over time we'll continue to improve the diagnostic experience and tooling. |
| 190 | + |
| 191 | +As we continue developing trimming we hope to see more code that's fully annotated, so users can |
| 192 | +trim with confidence. Because trimming involves the whole application, trimming is as much a |
| 193 | +feature of the ecosystem as it is of the product and we're depending on all developers to help |
| 194 | +improve the ecosystem. |
0 commit comments