-
Notifications
You must be signed in to change notification settings - Fork 4.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Resolve MakeGenericType ILLink warning in DependencyInjection #55102
Conversation
Note regarding the This serves as a reminder for when your PR is modifying a ref *.cs file and adding/modifying public APIs, to please make sure the API implementation in the src *.cs file is documented with triple slash comments, so the PR reviewers can sign off that change. |
Tagging subscribers to this area: @eerhardt, @maryamariyan Issue DetailsThis adds RequiresUnreferencedCode to any DI API that could take a separate Service and Implementation type. The reason is because if the Implementation type is generic and has a DynamicallyAccessedMembers annotation on its generic types, and the Service (interface) type doesn't, the trimmer won't preserve the correct members on the inner type. This leads to runtime failures. Adding the RequiresUnreferencedCode attribute allows us to suppress the MakeGenericType warning we are getting in DependencyInjection, but it forces all places that add DI services to ensure their types annotations match, and then suppress the warning.
|
Tagging subscribers to 'linkable-framework': @eerhardt, @vitek-karas, @LakshanF, @sbomer, @joperezr Issue DetailsThis adds RequiresUnreferencedCode to any DI API that could take a separate Service and Implementation type. The reason is because if the Implementation type is generic and has a DynamicallyAccessedMembers annotation on its generic types, and the Service (interface) type doesn't, the trimmer won't preserve the correct members on the inner type. This leads to runtime failures. Adding the RequiresUnreferencedCode attribute allows us to suppress the MakeGenericType warning we are getting in DependencyInjection, but it forces all places that add DI services to ensure their types annotations match, and then suppress the warning.
|
Heads up @pranavkm - when this change gets to the aspnetcore repo, we will probably need to suppress some new warnings. I can probably do that in aspnetcore. |
This change doesn't look right. This is a type system limitation that will cause things to warn unnecessarily just in case you use a generic type in that slot. This should only warn for open generic types not closed ones. It's too aggressive |
Agreed. But given the current feature set of the trimmer, I don't see how it is possible to make it less aggressive. cc @vitek-karas @marek-safar @agocke @MichalStrehovsky @sbomer |
Let it break for open generics and build an analyzer to detect open generics used in these methods. This is way too aggressive for my liking. |
@@ -13,12 +13,15 @@ namespace Microsoft.Extensions.DependencyInjection | |||
[DebuggerDisplay("Lifetime = {Lifetime}, ServiceType = {ServiceType}, ImplementationType = {ImplementationType}")] | |||
public class ServiceDescriptor | |||
{ | |||
internal const string RequiresUnreferencedCode = "Using generic types in DependencyInjection can lead to required code being trimmed. Ensure any generic Service types (e.g. 'IMyService<T>') have matching trimming annotations as the Implementation types implementing them (e.g. 'MyService<[DynamicallyAccessedMembers] T>)."; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
internal const string RequiresUnreferencedCode = "Using generic types in DependencyInjection can lead to required code being trimmed. Ensure any generic Service types (e.g. 'IMyService<T>') have matching trimming annotations as the Implementation types implementing them (e.g. 'MyService<[DynamicallyAccessedMembers] T>)."; | |
internal const string RequiresUnreferencedCode = "Using generic types in DependencyInjection can lead to required code being trimmed. Ensure any generic Service types (e.g. 'IMyService<T>') have the same trimming annotations as the Implementation types implementing them (e.g. 'MyService<[DynamicallyAccessedMembers] T>)."; |
@@ -129,6 +129,9 @@ public static class ServiceCollectionDescriptorExtensions | |||
/// </summary> | |||
/// <param name="collection">The <see cref="IServiceCollection"/>.</param> | |||
/// <param name="service">The type of the service to register.</param> | |||
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", | |||
Justification = "AddSingleton has RequiresUnreferencedCode because Service and Implementation types could be generic with mis-matching trimming annotations. " + |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Justification = "AddSingleton has RequiresUnreferencedCode because Service and Implementation types could be generic with mis-matching trimming annotations. " + | |
Justification = "ServiceDescriptor has RequiresUnreferencedCode because Service and Implementation types could be generic with mis-matching trimming annotations. " + |
@@ -217,6 +221,9 @@ public static class ServiceCollectionDescriptorExtensions | |||
/// </summary> | |||
/// <typeparam name="TService">The type of the service to add.</typeparam> | |||
/// <param name="collection">The <see cref="IServiceCollection"/>.</param> | |||
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", | |||
Justification = "AddSingleton has RequiresUnreferencedCode because Service and Implementation types could be generic with mis-matching trimming annotations. " + |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Justification = "AddSingleton has RequiresUnreferencedCode because Service and Implementation types could be generic with mis-matching trimming annotations. " + | |
Justification = "TryAddTransient has RequiresUnreferencedCode because Service and Implementation types could be generic with mis-matching trimming annotations. " + |
@@ -270,6 +278,9 @@ public static class ServiceCollectionDescriptorExtensions | |||
/// </summary> | |||
/// <param name="collection">The <see cref="IServiceCollection"/>.</param> | |||
/// <param name="service">The type of the service to register.</param> | |||
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", | |||
Justification = "AddSingleton has RequiresUnreferencedCode because Service and Implementation types could be generic with mis-matching trimming annotations. " + |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Justification = "AddSingleton has RequiresUnreferencedCode because Service and Implementation types could be generic with mis-matching trimming annotations. " + | |
Justification = "ServiceDescriptor has RequiresUnreferencedCode because Service and Implementation types could be generic with mis-matching trimming annotations. " + |
@@ -358,6 +370,9 @@ public static class ServiceCollectionDescriptorExtensions | |||
/// </summary> | |||
/// <typeparam name="TService">The type of the service to add.</typeparam> | |||
/// <param name="collection">The <see cref="IServiceCollection"/>.</param> | |||
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", | |||
Justification = "AddSingleton has RequiresUnreferencedCode because Service and Implementation types could be generic with mis-matching trimming annotations. " + |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Justification = "AddSingleton has RequiresUnreferencedCode because Service and Implementation types could be generic with mis-matching trimming annotations. " + | |
Justification = "TryAddScoped has RequiresUnreferencedCode because Service and Implementation types could be generic with mis-matching trimming annotations. " + |
collection.Configure(configureOptions); | ||
}); | ||
} | ||
|
||
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", | ||
Justification = "ServiceDescriptor has RequiresUnreferencedCode because Service and Implementation types could be generic with mis-matching trimming annotations. " + |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Justification = "ServiceDescriptor has RequiresUnreferencedCode because Service and Implementation types could be generic with mis-matching trimming annotations. " + | |
Justification = "AddSingleton has RequiresUnreferencedCode because Service and Implementation types could be generic with mis-matching trimming annotations. " + |
@@ -282,5 +283,14 @@ private void CreateServiceProvider() | |||
// service provider, ensuring it will be properly disposed with the provider | |||
_ = _appServices.GetService<IConfiguration>(); | |||
} | |||
|
|||
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", | |||
Justification = "ServiceDescriptor has RequiresUnreferencedCode because Service and Implementation types could be generic with mis-matching trimming annotations. " + |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Justification = "ServiceDescriptor has RequiresUnreferencedCode because Service and Implementation types could be generic with mis-matching trimming annotations. " + | |
Justification = "AddSingleton has RequiresUnreferencedCode because Service and Implementation types could be generic with mis-matching trimming annotations. " + |
@@ -67,5 +67,13 @@ public static IHostBuilder UseWindowsService(this IHostBuilder hostBuilder, Acti | |||
|
|||
return hostBuilder; | |||
} | |||
|
|||
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", | |||
Justification = "ServiceDescriptor has RequiresUnreferencedCode because Service and Implementation types could be generic with mis-matching trimming annotations. " + |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Justification = "ServiceDescriptor has RequiresUnreferencedCode because Service and Implementation types could be generic with mis-matching trimming annotations. " + | |
Justification = "AddSingleton has RequiresUnreferencedCode because Service and Implementation types could be generic with mis-matching trimming annotations. " + |
@@ -19,6 +19,9 @@ public static class OptionsServiceCollectionExtensions | |||
/// </summary> | |||
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param> | |||
/// <returns>The <see cref="IServiceCollection"/> so that additional calls can be chained.</returns> | |||
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", | |||
Justification = "ServiceDescriptor's have RequiresUnreferencedCode because Service and Implementation types could be generic with mis-matching trimming annotations. " + |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Justification = "ServiceDescriptor's have RequiresUnreferencedCode because Service and Implementation types could be generic with mis-matching trimming annotations. " + | |
Justification = "ServiceDescriptor has RequiresUnreferencedCode because Service and Implementation types could be generic with mis-matching trimming annotations. " + |
@@ -20,6 +20,9 @@ public static class HttpClientFactoryServiceCollectionExtensions | |||
/// </summary> | |||
/// <param name="services">The <see cref="IServiceCollection"/>.</param> | |||
/// <returns>The <see cref="IServiceCollection"/>.</returns> | |||
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", | |||
Justification = "ServiceDescriptor has RequiresUnreferencedCode because Service and Implementation types could be generic with mis-matching trimming annotations. " + |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This one looks like it silences warnings from ServiceDescriptor static methods, and also from TryAddTransient/TryAddSingleton - I wonder if that's worth mentioning in the justification.
Justification = "ServiceDescriptor has RequiresUnreferencedCode because Service and Implementation types could be generic with mis-matching trimming annotations. " + | |
Justification = "ServiceDescriptor, TryAddTransient, and TryAddSingleton have RequiresUnreferencedCode because Service and Implementation types could be generic with mis-matching trimming annotations. " + |
I think this should be a forcing function to improve generics (the language and runtime), attributes and the linker. We shouldn't make the library unusable because of those limitations. |
We do not break user code without warnings. Suppression of a warning that can result in broken code after trimming is a buggy suppression - not all dynamic patterns can be described statically - it's laws of physics. If this pattern ends up being general enough warranting a illinker feature to describe it, we can add an illinker feature to describe it. |
This can be described statically and I'd be ok if it warned for just open generics but we don't currently have a way to describe that. I don't think it's reasonable to make everything suffer because of that one scenario. |
What's the failure mode when the constructor gets stripped? Will the user get actionable feedback on what to do next? The warning added here states what the potential problem can be. I would not describe it as "suffering". I've seen a lot of failure modes caused by tools stripping necessary things. That is a lot closer to what I see when someone says "suffering":
|
The failure mode is based on what happens with the generic type. I don't know how they would know what to do next as its really dependent on the usage of the T in the concrete implementation of the generic. |
Is it an option to add new set of APIs that disallow the open generic types and that are trimming friendly? Then the fix for this warning would be to use the new trimming friendly API, and the warnings would be only issued for the really problematic cases. |
@eerhardt and I discussed this offline and we both like the analyzer solution proposed by @davidfowl, just "improved". The way I see this is that this is a pattern for which we don't have a solution in the trimmer. We don't even know what the solution would look like yet. And we won't get one in .NET 6 (too late for this I think). I also think (based on my own perception as well as feedback from @davidfowl) that we can't accept the PR as is - it would produce too much noise. The assumption is that vast majority of the callsites for the problematic methods are actually completely trim compatible, we just can't detect it. So generating warnings for all of them is just super noisy. My assumptions:
So I accept that this is a problem we don't have a nice solution for in .NET 6 but at the same time we want at least some solution. So using a "hacky" solution seems like an OK thing to do - as long as we see it as a point-in-time change. The "hacky" solution would be to build understanding of this into the linker:
This is not too different from what we're doing for example for operators and Obviously I would like to avoid doing this, but if we think this is too important the above solution doesn't sound too bad. |
It would be useful to clarify why it is must-have to do something about this for .NET 6. |
The current behavior isn't acceptable in my opinion. If I
Navigating to that link brings me to a page that shows me how to make my own library trimmable - not what I can do because As a user, I have no idea what is the problem with DependencyInjection. And this is the same warning I get for something that 100% doesn't work with trimming - like System.ComponentModel.Composition, System.CodeDom, or System.DirectoryServices. However, 95% of DependencyInjection works just fine with trimming. It is just this one last issue - open generic services - that is causing a problem. If there was a way to just flag those usages, I think the user experience would be perfect. The situation would be slightly different if this was a one-off, rarely used library. But For these reasons, I think this is an issue we must do something about in .NET 6 to complete our "make the .NET Libraries trim compatible" user story #43078. |
I agree that this is a popular library. But I do not think doing something about it in .NET 6 will move the needle significantly from the user scenario point of view. The app models where this library is used will generated a ton of other impossible to reason about warnings in .NET 6. (For example, I have just tried I understand that the special casing in the linker is always the easy way out. I do not think it is sustainable. If we want to do something about this, we should be looking at fixing this properly, and not by special casing in the linker. |
Even Maui? Note: this isn't just for app developers. If library authors want to make their libraries trimmable, our docs say to reference the library in a console app, and run the linker to get all the warnings in the library. If the library interacts with DependencyInjection, giving them a decent warning when they inject open generic services would actually move the needle to helping them make their libraries trim compatible. |
We should absolutely try our best to make the trim warnings accurate and safe but this PR isn't the solution. I like the idea that the linker would have knowledge of this open generic "issue" but it would be nice if there was some attribute to indicate you wanted tho behavior on these types of methods. |
I've updated this PR with the above plan (excluding leaving a LibraryBuild warning in). We believe the runtime check that is enabled when PublishTrimmed=true will be enough notification to the developer. Please take a look at the new approach. |
😢 |
I thought you didn't want the warning. |
Oh wait, I don't 😄 |
Sounds good - if we figure out a way to check for this statically we can add a new warning in future releases. |
I also noticed that there are now multiple source-generator-based dependency injection libraries, e.g. https://github.com/giggio/sourceinject/ which promise AOT-friendly codegen for dependency injection. These libraries could be alternatives for users if we find more problems than expected. |
Yes, there will be more problems. The current pattern, even with this change, is not AOT friendly. Do we have a solution in mind that would make it AOT friendly?
Yep, we will know that trimming and AOT friendliness is taking off once there is something like https://quarkus.io/ for .NET. |
AFAIK, no. If we limited the generic parameter types to "ref" types, would that make it AOT friendly? If yes, maybe that's just what we instruct library and app developers who want to use this feature with AOT. The open generics feature is used in a few pretty core places:
|
What isn't AOT friendly? |
I believe all uses of |
I don't see why if these are reference types. |
Is there any protection against registering structs? |
For arbitrary open generics no. |
I think the fear that this pattern isn't AOT friendly is a "perfect is the enemy of good" situation. I think this is pretty good already. I can see a future where we have a new method designed for open generics that would allow us to add more static analysis where possible. |
Any feedback here? I believe this is ready to merge. |
Resolve the ILLink warning in DependencyInjection by adding a runtime check that is behind a new AppContext switch 'Microsoft.Extensions.DependencyInjection.VerifyOpenGenericServiceTrimmability'. The runtime check ensures the trimming annotations on the open generic types are compatible between the service and implementation types. The check is enabled by default when PublishTrimmed=true.
Here is a simple repro of the trimming issue where the developer doesn't see a warning:
If you publish this app, it doesn't work:
Today, the developer sees a warning coming from DependencyInjection:
That is the warning being suppressed here and being validated by the new runtime check.