Skip to content
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

Unexpected trimming on a type on which DynamicallyAccessedMembersAttribute is applied (.NET 6 Preview 6) #57140

Closed
TheCakeMonster opened this issue Aug 10, 2021 · 7 comments

Comments

@TheCakeMonster
Copy link

Short Description

I expected a type on which the DynamicallyAccessedMembersAttribute is applied (with the enum value DynamicallyAccessedMemberTypes.All) to be excluded from trimming, and have all of its members preserved. However, this is not the case; instead private members are being removed. Repro created for inspection.

https://github.com/TheCakeMonster/Examples/tree/master/Issues/UnexpectedTrimming

Description

I have created two test libraries - one is a .NET Standard library, the other a .NET 6 library.

Each library contains a type that has the DynamicallyAccessedMembersAttribute applied using the DynamicallyAccessedMemberTypes.All enum value, indicating that all members are subject to dynamic access. However, the private instance method on each type is removed when the application is published. This is unexpected.

I have created a repro at https://github.com/TheCakeMonster/Examples/tree/master/Issues/UnexpectedTrimming

When the console app in this solution is run in debug mode, all of the console outputs are true, indicating that the private methods and the types they access are present. This is exactly what I expect (as trimming is not run) but is important in that it proves that the reflection code works.

When the console application is published and the published application is run, the flags all return false, instead of the second set - the ones associated with the class to which the attribute has been applied - continuing to return true.

Configuration

.NET 6 Preview 6

Dotnet --version output is as follows:

.NET SDK (reflecting any global.json):
Version: 6.0.100-preview.6.21355.2
Commit: 7f8e0d76c0

Runtime Environment:
OS Name: Windows
OS Version: 10.0.19042
OS Platform: Windows
RID: win10-x64
Base Path: C:\Program Files\dotnet\sdk\6.0.100-preview.6.21355.2\

Host (useful for support):
Version: 6.0.0-preview.6.21352.12
Commit: 770d630

Publishing is being performed using Visual Studio 2019 16.11 Preview 2, if that is relevant.

Regression?

This code makes use of a change to DynamicallyAccessedMembersAttribute that is only available in .NET 6, so it would not compile on .NET 5. This is new functionality in .NET 6, although it makes use of the trimming feature made more widely available in .NET 5.

The code makes use of the change made under issue #49465, which allows the DynamicallyAccessedMembersAttribute attribute to be applied to a class for the first time.

@dotnet-issue-labeler dotnet-issue-labeler bot added the untriaged New issue has not been triaged by the area owner label Aug 10, 2021
@dotnet-issue-labeler
Copy link

I couldn't figure out the best area label to add to this issue. If you have write-permissions please help me learn by adding exactly one area label.

@eerhardt
Copy link
Member

@TheCakeMonster - check out the description in dotnet/linker#1929, which describes how the DynamicallyAccessedMembersAttribute works when it is applied to a type. Specifically:

The desired behavior is that calling object.GetType() on a statically typed instance will look for the annotation on the static type (or its base class or interface). If it finds it there, it will apply the annotation to the static type and all of its derived types (which are marked). Then it will treat the return value of the object.GetType() as a Type annotated with the found annotation.

Annotating a type with DynamicallyAccessedMembersAttribute by itself doesn't preserve the members on the type. It also needs some code to call instanceOfType.GetType() as well for the members to be preserved.

Closing, as this is the intended behavior.

cc @vitek-karas @tlakollo @LakshanF @sbomer @agocke

@eerhardt eerhardt added area-AssemblyLoader-coreclr and removed untriaged New issue has not been triaged by the area owner labels Aug 10, 2021
@vitek-karas
Copy link
Member

I want to add some more explanation of what is the expected behavior of trimming overall.

There are basically only two cases:

  • The application when published as trimmed reports some trimmer warnings (even just 1 is enough) - in this case trimming makes no guarantee that the app will work and the behavior of the app is effectively undefined (since the trimming may have removed things the app will need)
  • The application is published as trimmed without any warnings - in this case trimming tried to guarantee that the behavior of the app will not change. That is running the app with and without trimming will produce the same behavior.

Caveat: Suppressing any of the trimmer warnings can easily leave the app in a state where it doesn't report warnings, but it's still broken. Please be VERY careful when suppressing trimmer warnings.

The repro app you provided should report several warnings, so it falls into the first bucket.

As for what is removed, if the app falls into the second bucket (no warnings) - the trimmer does not make any guarantee that it will remove or keep anything in the app - all it guarantees is that the behavior of the app is not going to be changed. Obviously it tries to remove as much as possible (to get the size down), but the exact behavior of the trimming is subject to change at any point.

In the case of the DynamicallyAccessedMembersAttribute on the type - the trimmer will keep the required members ONLY if it sees code which makes use of it - in your repro, there's no apparent usage, so it is free to remove the members.

@TheCakeMonster
Copy link
Author

TheCakeMonster commented Aug 10, 2021

OK, thanks everyone.

Is there another attribute for disabling trimming on a per-type basis that I am missing?

If I understand the responses correctly, there are quite a few scenarios making any support for trimming unviable at the moment. If the names of types are not known until runtime - for example they come from a config file, a database or a serialization stream - then enabling trimming on that assembly is not possible?

I'm wondering how pluggable architectures are covered in respect of trimming. There are a number of techniques that spring to mind that seem not to be covered by the existing capabilities.

@agocke
Copy link
Member

agocke commented Aug 10, 2021

If the names of types are not known until runtime - for example they come from a config file, a database or a serialization stream - then enabling trimming on that assembly is not possible?

Correct. There's no conceivable way to determine what is used by the application, so there's no way to trim what is unused.

@TheCakeMonster
Copy link
Author

@agocke thanks, that's what I thought.

I wonder if it's time for a new attribute, perhaps called PleaseDontTrimMyTypeMrTrimmerIKnowWhatIAmDoingAndItsInappropriateAttribute? Hehehe

@tlakollo
Copy link
Contributor

That sounds like the DynamicDependency attribute, it's not documented in the repo but you can find some instances of its use in tests that demonstrate how it can help to keep members the trimmer can't reason about. One of the simplest examples is in DynamicDependencyField test, which keeps an extra method just by referencing it in the attribute. Although they are other ways to keep members that trimmer cannot analyze, this is the attribute one.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

5 participants