From 3fa10820d070ed859c0103a7da0f6b7fa63763a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Fri, 10 Jul 2026 17:56:05 +0200 Subject: [PATCH 1/3] Make DynamicData trimming/NativeAOT-safe via DynamicallyAccessedMembers Annotate the declaring Type that DynamicData reflects over (constructor parameters, backing field, display-name declaring type, and the DynamicDataOperations.GetData parameter) with [DynamicallyAccessedMembers] so the trimmer preserves the members looked up at runtime, instead of leaving DynamicData silently trim/AOT-unsafe. The AutoDetect fallback to the test method's own class flows through a GetTestMethodDeclaringType helper carrying a narrow, truthful IL2073 suppression: the test method's declaring type is always rooted by discovery, so its members are preserved. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../DataSource/DynamicDataAttribute.cs | 10 +++++---- .../DataSource/DynamicDataOperations.cs | 21 +++++++++++++++++-- .../InternalAPI/InternalAPI.Unshipped.txt | 2 ++ 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/src/TestFramework/TestFramework/Attributes/DataSource/DynamicDataAttribute.cs b/src/TestFramework/TestFramework/Attributes/DataSource/DynamicDataAttribute.cs index fc83907db3..9ac68e24b3 100644 --- a/src/TestFramework/TestFramework/Attributes/DataSource/DynamicDataAttribute.cs +++ b/src/TestFramework/TestFramework/Attributes/DataSource/DynamicDataAttribute.cs @@ -42,6 +42,7 @@ public sealed class DynamicDataAttribute : Attribute, ITestDataSource, ITestData private readonly string _dynamicDataSourceName; private readonly DynamicDataSourceType _dynamicDataSourceType; private readonly object?[] _dynamicDataSourceArguments = []; + [DynamicallyAccessedMembers(DynamicDataOperations.RequiredMemberTypes)] private readonly Type? _dynamicDataDeclaringType; /// @@ -103,7 +104,7 @@ public DynamicDataAttribute(string dynamicDataSourceName, params object?[] dynam /// Specifies whether the data is stored as property, in method, or in field. /// [EditorBrowsable(EditorBrowsableState.Never)] - public DynamicDataAttribute(string dynamicDataSourceName, Type dynamicDataDeclaringType, DynamicDataSourceType dynamicDataSourceType) + public DynamicDataAttribute(string dynamicDataSourceName, [DynamicallyAccessedMembers(DynamicDataOperations.RequiredMemberTypes)] Type dynamicDataDeclaringType, DynamicDataSourceType dynamicDataSourceType) : this(dynamicDataSourceName, dynamicDataSourceType) => _dynamicDataDeclaringType = dynamicDataDeclaringType; /// @@ -117,7 +118,7 @@ public DynamicDataAttribute(string dynamicDataSourceName, Type dynamicDataDeclar /// The declaring type of property, method, or field having data. Useful in cases when declaring type is present in a class different from /// test method's class. If null, declaring type defaults to test method's class type. /// - public DynamicDataAttribute(string dynamicDataSourceName, Type dynamicDataDeclaringType) + public DynamicDataAttribute(string dynamicDataSourceName, [DynamicallyAccessedMembers(DynamicDataOperations.RequiredMemberTypes)] Type dynamicDataDeclaringType) : this(dynamicDataSourceName) => _dynamicDataDeclaringType = dynamicDataDeclaringType; /// @@ -134,7 +135,7 @@ public DynamicDataAttribute(string dynamicDataSourceName, Type dynamicDataDeclar /// /// Arguments to be passed to method referred to by . /// - public DynamicDataAttribute(string dynamicDataSourceName, Type dynamicDataDeclaringType, params object?[] dynamicDataSourceArguments) + public DynamicDataAttribute(string dynamicDataSourceName, [DynamicallyAccessedMembers(DynamicDataOperations.RequiredMemberTypes)] Type dynamicDataDeclaringType, params object?[] dynamicDataSourceArguments) : this(dynamicDataSourceName) { _dynamicDataDeclaringType = dynamicDataDeclaringType; @@ -149,6 +150,7 @@ public DynamicDataAttribute(string dynamicDataSourceName, Type dynamicDataDeclar /// /// Gets or sets the declaring type used to customize the display name in test results. /// + [DynamicallyAccessedMembers(DynamicDataOperations.RequiredMemberTypes)] public Type? DynamicDataDisplayNameDeclaringType { get; set; } /// @@ -168,7 +170,7 @@ public IEnumerable GetData(MethodInfo methodInfo) return TestDataSourceUtilities.ComputeDefaultDisplayName(methodInfo, data); } - Type? dynamicDisplayNameDeclaringType = DynamicDataDisplayNameDeclaringType ?? methodInfo.DeclaringType; + Type? dynamicDisplayNameDeclaringType = DynamicDataDisplayNameDeclaringType ?? DynamicDataOperations.GetTestMethodDeclaringType(methodInfo); DebugEx.Assert(dynamicDisplayNameDeclaringType is not null, "Declaring type of test data cannot be null."); MethodInfo method = dynamicDisplayNameDeclaringType.GetTypeInfo().GetDeclaredMethod(DynamicDataDisplayName) diff --git a/src/TestFramework/TestFramework/Attributes/DataSource/DynamicDataOperations.cs b/src/TestFramework/TestFramework/Attributes/DataSource/DynamicDataOperations.cs index be9c634584..1f81b979ec 100644 --- a/src/TestFramework/TestFramework/Attributes/DataSource/DynamicDataOperations.cs +++ b/src/TestFramework/TestFramework/Attributes/DataSource/DynamicDataOperations.cs @@ -7,10 +7,22 @@ internal static class DynamicDataOperations { private const BindingFlags MemberLookup = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.FlattenHierarchy; - public static IEnumerable GetData(Type? dynamicDataDeclaringType, DynamicDataSourceType dynamicDataSourceType, string dynamicDataSourceName, object?[] dynamicDataSourceArguments, MethodInfo methodInfo) + /// + /// The set of members that a dynamic data source type may expose. It mirrors (public and + /// non-public properties, fields, and methods) so that the trimmer preserves them on any annotated + /// with this value, keeping safe under trimming and NativeAOT. + /// + internal const DynamicallyAccessedMemberTypes RequiredMemberTypes = + DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties + | DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.NonPublicFields + | DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods; + + public static IEnumerable GetData([DynamicallyAccessedMembers(RequiredMemberTypes)] Type? dynamicDataDeclaringType, DynamicDataSourceType dynamicDataSourceType, string dynamicDataSourceName, object?[] dynamicDataSourceArguments, MethodInfo methodInfo) { // Check if the declaring type of test data is passed in. If not, default to test method's class type. - dynamicDataDeclaringType ??= methodInfo.DeclaringType; + // The declaring type of the test method is always rooted by test discovery, so its members are preserved even + // though MethodInfo.DeclaringType is not statically annotated with DynamicallyAccessedMembersAttribute. + dynamicDataDeclaringType ??= GetTestMethodDeclaringType(methodInfo); DebugEx.Assert(dynamicDataDeclaringType is not null, "Declaring type of test data cannot be null."); object? obj = null; @@ -170,4 +182,9 @@ private static bool TryGetData(object dataSource, [NotNullWhen(true)] out IEnume data = null; return false; } + + [return: DynamicallyAccessedMembers(RequiredMemberTypes)] + [UnconditionalSuppressMessage("Trimming", "IL2073:Value returned does not have matching annotations", Justification = "The declaring type of the test method is always rooted by test discovery, so its members are preserved by the trimmer.")] + internal static Type? GetTestMethodDeclaringType(MethodInfo methodInfo) + => methodInfo.DeclaringType; } diff --git a/src/TestFramework/TestFramework/InternalAPI/InternalAPI.Unshipped.txt b/src/TestFramework/TestFramework/InternalAPI/InternalAPI.Unshipped.txt index 7dc5c58110..8b57ef770f 100644 --- a/src/TestFramework/TestFramework/InternalAPI/InternalAPI.Unshipped.txt +++ b/src/TestFramework/TestFramework/InternalAPI/InternalAPI.Unshipped.txt @@ -1 +1,3 @@ #nullable enable +const Microsoft.VisualStudio.TestTools.UnitTesting.DynamicDataOperations.RequiredMemberTypes = System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicMethods | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicMethods | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicFields | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicFields | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicProperties -> System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes +static Microsoft.VisualStudio.TestTools.UnitTesting.DynamicDataOperations.GetTestMethodDeclaringType(System.Reflection.MethodInfo! methodInfo) -> System.Type? From aa1e9b83e54d9388f1e1f13ffb8f2949bb5e08ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Sat, 11 Jul 2026 14:02:17 +0200 Subject: [PATCH 2/3] Document trim-annotation scope and enrich IL2073 justification Address review feedback: clarify in RequiredMemberTypes docs that instance members are intentionally over-preserved (no static-only DAM flag) and that inherited members surfaced via FlattenHierarchy on a base type are outside DAM's granular reach. Enrich the IL2073 suppression justification with the [TestClass]-roots-the-type invariant breadcrumb. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../DataSource/DynamicDataOperations.cs | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/src/TestFramework/TestFramework/Attributes/DataSource/DynamicDataOperations.cs b/src/TestFramework/TestFramework/Attributes/DataSource/DynamicDataOperations.cs index 1f81b979ec..a8c294e155 100644 --- a/src/TestFramework/TestFramework/Attributes/DataSource/DynamicDataOperations.cs +++ b/src/TestFramework/TestFramework/Attributes/DataSource/DynamicDataOperations.cs @@ -8,10 +8,27 @@ internal static class DynamicDataOperations private const BindingFlags MemberLookup = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.FlattenHierarchy; /// - /// The set of members that a dynamic data source type may expose. It mirrors (public and - /// non-public properties, fields, and methods) so that the trimmer preserves them on any annotated - /// with this value, keeping safe under trimming and NativeAOT. + /// The set of members that a dynamic data source type may expose, mirroring the member kinds reached by + /// : public and non-public properties, fields, and methods. Annotating a + /// with this value keeps those members on that type alive under trimming and NativeAOT, so + /// can resolve them by name at runtime. /// + /// + /// + /// Instance members are preserved as well, even though a data source must be : the granular + /// flags have no static-only variant, and + /// itself includes so the member can be found before it is validated as static. + /// This over-preservation is intentional and unavoidable. + /// + /// + /// The granular flags only preserve members declared on the annotated type; they do not propagate to base types. + /// A data source that is an inherited (for example ) member + /// surfaced through lives on a base type and is not covered here. This is + /// a limitation of (which + /// would not fix either, as it also does not annotate base types); the supported and documented scenario is a data + /// source declared directly on the referenced type. + /// + /// internal const DynamicallyAccessedMemberTypes RequiredMemberTypes = DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties | DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.NonPublicFields @@ -184,7 +201,7 @@ private static bool TryGetData(object dataSource, [NotNullWhen(true)] out IEnume } [return: DynamicallyAccessedMembers(RequiredMemberTypes)] - [UnconditionalSuppressMessage("Trimming", "IL2073:Value returned does not have matching annotations", Justification = "The declaring type of the test method is always rooted by test discovery, so its members are preserved by the trimmer.")] + [UnconditionalSuppressMessage("Trimming", "IL2073:Value returned does not have matching annotations", Justification = "The declaring type of the test method is always rooted by test discovery (the [TestClass] attribute keeps the type and its members alive), so MethodInfo.DeclaringType and its members are preserved by the trimmer.")] internal static Type? GetTestMethodDeclaringType(MethodInfo methodInfo) => methodInfo.DeclaringType; } From 2eee67967549164bb82a6934e412cb9249c4ce75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Sat, 11 Jul 2026 14:31:26 +0200 Subject: [PATCH 3/3] Use DynamicallyAccessedMemberTypes.All to preserve inherited data sources Address review: the granular NonPublic* flags only preserve members declared directly on the annotated type, so an inherited (e.g. protected static) source surfaced via BindingFlags.FlattenHierarchy on a base type would still be trimmed away. This is a shipped, tested scenario (DynamicDataTest_Source*FromBase). Switch RequiredMemberTypes to All, which walks the whole base chain and matches the [DynamicDependency(All)] that MSTest.SourceGeneration already emits for test classes and their base types. The NonPublic*WithInherited flags are not a portable alternative (they are net9-only in the BCL). Also correct the IL2073 suppression justification: preservation comes from the source generator's [DynamicDependency(All)], not from [TestClass] (which carries no trimming annotations). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../DataSource/DynamicDataOperations.cs | 43 ++++++++----------- .../InternalAPI/InternalAPI.Unshipped.txt | 2 +- 2 files changed, 19 insertions(+), 26 deletions(-) diff --git a/src/TestFramework/TestFramework/Attributes/DataSource/DynamicDataOperations.cs b/src/TestFramework/TestFramework/Attributes/DataSource/DynamicDataOperations.cs index a8c294e155..b5ba8ec880 100644 --- a/src/TestFramework/TestFramework/Attributes/DataSource/DynamicDataOperations.cs +++ b/src/TestFramework/TestFramework/Attributes/DataSource/DynamicDataOperations.cs @@ -8,37 +8,30 @@ internal static class DynamicDataOperations private const BindingFlags MemberLookup = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.FlattenHierarchy; /// - /// The set of members that a dynamic data source type may expose, mirroring the member kinds reached by - /// : public and non-public properties, fields, and methods. Annotating a - /// with this value keeps those members on that type alive under trimming and NativeAOT, so - /// can resolve them by name at runtime. + /// The members that a dynamic data source type may expose. resolves the source member with + /// and , so the source can be an + /// inherited (for example ) member declared on a base type. Only + /// preserves inherited non-public members across the whole base chain + /// (the granular NonPublic* flags preserve only members declared directly on the annotated type, and the + /// NonPublic*WithInherited flags are not available on every target framework's base class library). Annotating a + /// with this value keeps the whole member surface alive under trimming and NativeAOT, so + /// can resolve the source by name at runtime. /// /// - /// - /// Instance members are preserved as well, even though a data source must be : the granular - /// flags have no static-only variant, and - /// itself includes so the member can be found before it is validated as static. - /// This over-preservation is intentional and unavoidable. - /// - /// - /// The granular flags only preserve members declared on the annotated type; they do not propagate to base types. - /// A data source that is an inherited (for example ) member - /// surfaced through lives on a base type and is not covered here. This is - /// a limitation of (which - /// would not fix either, as it also does not annotate base types); the supported and documented scenario is a data - /// source declared directly on the referenced type. - /// + /// This mirrors the [DynamicDependency(DynamicallyAccessedMemberTypes.All, ...)] that + /// MSTest.SourceGeneration already emits for every discovered test class and its base types. It over-preserves + /// (constructors, events, instance members, and so on) because there is no static-only or member-kind-scoped variant + /// that also walks the base hierarchy; this is intentional and unavoidable to keep the inherited-source scenario + /// trim-safe. /// - internal const DynamicallyAccessedMemberTypes RequiredMemberTypes = - DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties - | DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.NonPublicFields - | DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods; + internal const DynamicallyAccessedMemberTypes RequiredMemberTypes = DynamicallyAccessedMemberTypes.All; public static IEnumerable GetData([DynamicallyAccessedMembers(RequiredMemberTypes)] Type? dynamicDataDeclaringType, DynamicDataSourceType dynamicDataSourceType, string dynamicDataSourceName, object?[] dynamicDataSourceArguments, MethodInfo methodInfo) { // Check if the declaring type of test data is passed in. If not, default to test method's class type. - // The declaring type of the test method is always rooted by test discovery, so its members are preserved even - // though MethodInfo.DeclaringType is not statically annotated with DynamicallyAccessedMembersAttribute. + // In the supported trimming/NativeAOT configuration the test class (and its base types) are rooted by the + // [DynamicDependency(All)] that MSTest.SourceGeneration emits, so MethodInfo.DeclaringType stays trim-safe even + // though it is not statically annotated with DynamicallyAccessedMembersAttribute (see GetTestMethodDeclaringType). dynamicDataDeclaringType ??= GetTestMethodDeclaringType(methodInfo); DebugEx.Assert(dynamicDataDeclaringType is not null, "Declaring type of test data cannot be null."); @@ -201,7 +194,7 @@ private static bool TryGetData(object dataSource, [NotNullWhen(true)] out IEnume } [return: DynamicallyAccessedMembers(RequiredMemberTypes)] - [UnconditionalSuppressMessage("Trimming", "IL2073:Value returned does not have matching annotations", Justification = "The declaring type of the test method is always rooted by test discovery (the [TestClass] attribute keeps the type and its members alive), so MethodInfo.DeclaringType and its members are preserved by the trimmer.")] + [UnconditionalSuppressMessage("Trimming", "IL2073:Value returned does not have matching annotations", Justification = "In the supported trimming/NativeAOT configuration, test classes are source-generated: MSTest.SourceGeneration emits [DynamicDependency(DynamicallyAccessedMemberTypes.All, ...)] for every discovered test class and its base types, so MethodInfo.DeclaringType and its members are already rooted. This fallback only runs for the test method's own declaring type; it does not claim safety for unsupported reflection-only trimmed callers that discover tests without the source generator.")] internal static Type? GetTestMethodDeclaringType(MethodInfo methodInfo) => methodInfo.DeclaringType; } diff --git a/src/TestFramework/TestFramework/InternalAPI/InternalAPI.Unshipped.txt b/src/TestFramework/TestFramework/InternalAPI/InternalAPI.Unshipped.txt index 8b57ef770f..118fea878f 100644 --- a/src/TestFramework/TestFramework/InternalAPI/InternalAPI.Unshipped.txt +++ b/src/TestFramework/TestFramework/InternalAPI/InternalAPI.Unshipped.txt @@ -1,3 +1,3 @@ #nullable enable -const Microsoft.VisualStudio.TestTools.UnitTesting.DynamicDataOperations.RequiredMemberTypes = System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicMethods | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicMethods | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicFields | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicFields | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicProperties -> System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes +const Microsoft.VisualStudio.TestTools.UnitTesting.DynamicDataOperations.RequiredMemberTypes = (System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes)-1 -> System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes static Microsoft.VisualStudio.TestTools.UnitTesting.DynamicDataOperations.GetTestMethodDeclaringType(System.Reflection.MethodInfo! methodInfo) -> System.Type?