Skip to content

Make DynamicData trimming/NativeAOT-safe via DynamicallyAccessedMembers#9832

Merged
Evangelink merged 3 commits into
mainfrom
dev/amauryleve/literate-journey
Jul 11, 2026
Merged

Make DynamicData trimming/NativeAOT-safe via DynamicallyAccessedMembers#9832
Evangelink merged 3 commits into
mainfrom
dev/amauryleve/literate-journey

Conversation

@Evangelink

Copy link
Copy Markdown
Member

What / why

DynamicDataAttribute resolves its source member (property/method/field) by name via reflection in DynamicDataOperations.GetData, reflecting over the declaring Type with BindingFlags.Public | NonPublic | Instance | Static | FlattenHierarchy. That declaring Type carried no trimming annotations, so under trimming/NativeAOT the members it looks up can be trimmed away and the lookup fails at runtime — silently, with no warning.

This makes DynamicData genuinely trim/AOT-safe by telling the trimmer to preserve those members, following the exact pattern already used by MemberConditionAttribute in this same project ([DynamicallyAccessedMembers] on the Type).

Why this approach (and not the alternatives)

  • [RequiresUnreferencedCode]/[RequiresDynamicCode] would only warn, and — because the adapter consumes data sources through the ITestDataSource interface, and users only ever apply the attribute — the warning would never actually reach the user; it would only fire inside MSTest's own build, tempting us into a suppression that would be a lie.
  • Annotating the ITestDataSource interface was rejected: the contract is trim-safe by design (e.g. DataRowAttribute returns static data), so the problem is the implementation, not the contract.
  • [DynamicallyAccessedMembers] is the honest fix: it makes the code safe, emits no warning, needs no suppression, and — being polyfilled in this repo — works on all TFMs (netstandard2.0/net462/net8.0/net9.0) with no #if guards.

Changes

  • Add an internal DynamicDataOperations.RequiredMemberTypes constant (public+non-public properties/fields/methods, mirroring the BindingFlags used).
  • Apply [DynamicallyAccessedMembers(RequiredMemberTypes)] to:
    • the three Type-taking DynamicDataAttribute constructors,
    • the _dynamicDataDeclaringType field,
    • the DynamicDataDisplayNameDeclaringType property,
    • the Type? parameter of DynamicDataOperations.GetData.
  • The AutoDetect fallback to the test method's own class flows through a new GetTestMethodDeclaringType helper carrying a narrow, truthful IL2073 suppression — MethodInfo.DeclaringType isn't statically annotated, but the test class is always rooted by discovery, so its members are preserved.
  • Declare the two new internal symbols in InternalAPI.Unshipped.txt.

Verification

  • Builds cleanly across all four TFMs.
  • With the trim/AOT analyzer force-enabled (/p:EnableAotAnalyzers=true), DynamicDataOperations.cs and DynamicDataAttribute.cs produce zero IL warnings — the annotations fully cover the reflection.
  • All 35 DynamicData unit tests pass (behavior is unchanged; the helper just extracts the existing methodInfo.DeclaringType fallback).

Out of scope / follow-up

Flipping the assembly-wide EnableAotAnalyzers switch is intentionally not done here. Enabling it surfaces 13 pre-existing, unrelated IL warnings in Assert.That.ExpressionDetails and Assert.AreEquivalent.* (Expression.Lambda, Type.MakeGenericType, Type.GetInterfaces, …), which would break CI (-TreatWarningsAsErrors) and belong in a separate effort. This PR moves DynamicData one step toward that goal.

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>
Copilot AI review requested due to automatic review settings July 10, 2026 15:56

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds trimming and NativeAOT annotations for reflection-based DynamicData member lookup.

Changes:

  • Defines required reflected member types.
  • Annotates declaring-type flows.
  • Adds a suppressed helper for test-method declaring types.
Show a summary per file
File Description
InternalAPI.Unshipped.txt Tracks new internal symbols.
DynamicDataOperations.cs Adds trimming requirements and declaring-type helper.
DynamicDataAttribute.cs Annotates constructors, storage, and display-name types.

Review details

  • Files reviewed: 3/3 changed files
  • Comments generated: 2
  • Review effort level: Medium

Comment thread src/TestFramework/TestFramework/Attributes/DataSource/DynamicDataOperations.cs Outdated

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Note

🤖 Automated review by GitHub Copilot. Generated by the Expert Code Review workflow. To request a follow-up action, reply by tagging @copilot directly.

Review Summary

The approach is sound — annotating Type parameters/fields with [DynamicallyAccessedMembers] is the correct pattern for making reflection-based member lookup trim-safe, and it mirrors the existing MemberConditionAttribute precedent in this repo.

Findings

# Dimension Severity Finding
1 Public API Surface 🔴 Major Adding [DynamicallyAccessedMembers] to 3 public constructor parameters and 1 public property (DynamicDataDisplayNameDeclaringType) changes the public API signature. These changes are not declared in PublicAPI.Unshipped.txt — only the internal symbols are tracked in InternalAPI.Unshipped.txt. Callers passing unannotated Type values will now receive new IL2067 trimmer warnings, which is a source-level breaking change for -WarnAsError users.
2 Trimming Correctness 🟡 Suggestion The IL2073 suppression on GetTestMethodDeclaringType is justified today because [TestClass] roots the type. Consider enriching the justification string to mention that invariant explicitly.
3 Over-preservation i️ Info RequiredMemberTypes preserves instance members too (since DynamicallyAccessedMemberTypes has no static-only flag), though DynamicData only supports static members. This is expected and unavoidable — no action needed.

Dimensions with no findings (N/A or clean)

Algorithmic Correctness, Concurrency, Error Handling, Resource Management, Security, Performance, Cross-TFM, IPC, Localization, Naming, Style, Tests, Documentation, Backward Compat (aside from #1), Dependencies, Diagnostics, Logging, Serialization, Accessibility.

Comment thread src/TestFramework/TestFramework/Attributes/DataSource/DynamicDataOperations.cs Outdated
@github-actions

This comment has been minimized.

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>
Copilot AI review requested due to automatic review settings July 11, 2026 12:02

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review details

  • Files reviewed: 3/3 changed files
  • Comments generated: 2
  • Review effort level: Medium

Comment thread src/TestFramework/TestFramework/Attributes/DataSource/DynamicDataOperations.cs Outdated
Comment thread src/TestFramework/TestFramework/Attributes/DataSource/DynamicDataOperations.cs Outdated
@github-actions

Copy link
Copy Markdown
Contributor

🔴 Build Failure Analysis

Summary

The build failed with 4 CA1416 (platform compatibility) errors in FileLoggerTests.cs. These errors are not caused by this PR — they were introduced by commit c66515a (RFC 018: Artifact post-processing for dotnet test (MTP) (#9187)) which was merged into the target branch.

Root Cause

ITask.RunLongRunning was annotated with [UnsupportedOSPlatform("browser")] and [UnsupportedOSPlatform("wasi")] in ITask.cs, but two test helper classes in FileLoggerTests.cs call _inner.RunLongRunning(...) without propagating the platform suppression:

File Line Class
FileLoggerTests.cs 465 SynchronousLoopStartingTask.RunLongRunning
FileLoggerTests.cs 490 NeverCompletingTask.RunLongRunning

Suggested Fix (for the base branch)

Add [UnsupportedOSPlatform("browser")] and [UnsupportedOSPlatform("wasi")] to both RunLongRunning implementations, or suppress CA1416 since these are test-only types that will never run on browser/wasi:

// Option 1: Propagate the platform annotation
[UnsupportedOSPlatform("browser")]
[UnsupportedOSPlatform("wasi")]
public Task RunLongRunning(Func<Task> action, string name, CancellationToken cancellationToken)
    => _inner.RunLongRunning(action, name, cancellationToken);

// Option 2: Suppress the warning (preferred for test code)
#pragma warning disable CA1416 // Validate platform compatibility
public Task RunLongRunning(Func<Task> action, string name, CancellationToken cancellationToken)
    => _inner.RunLongRunning(action, name, cancellationToken);
#pragma warning restore CA1416

Impact on This PR

This PR's changes (trimming annotations for DynamicDataAttribute) are not related to the build failure. A fix to the base branch (or a merge of the fix) is needed to unblock CI.

🤖 Automated content by GitHub Copilot. Generated by the Build Failure Analysis workflow. · 83.2 AIC · ⌖ 5.62 AIC · ⊞ 7.3K · [◷]( · )

…rces

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>
Copilot AI review requested due to automatic review settings July 11, 2026 12:31
@Evangelink
Evangelink merged commit 01014ae into main Jul 11, 2026
25 of 35 checks passed
@Evangelink
Evangelink deleted the dev/amauryleve/literate-journey branch July 11, 2026 12:33

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review details

  • Files reviewed: 3/3 changed files
  • Comments generated: 0 new
  • Review effort level: Medium

Evangelink added a commit that referenced this pull request Jul 11, 2026
…, BOMs, doc

Tighten the source generator's [DynamicData] resolution so a reflection-free
accessor is only emitted when it provably behaves like the runtime reflection
lookup in DynamicDataOperations; otherwise fall back to the (DAM-safe) reflection
path:

- Default the source/display-name declaring type to the test method's containing
  type (matches methodInfo.DeclaringType), so inherited [DynamicData] resolves
  under the base type instead of the leaf.
- Honor DynamicDataSourceType (explicit Property/Method/Field) and skip when the
  name maps to more than one member kind across the hierarchy (AutoDetect kind
  selection vs C# binding can diverge).
- Validate the property getter itself (static + accessible from the consuming
  assembly), skip params/param-collection and by-ref source methods, and skip
  ambiguous method overloads.
- Resolve display-name methods declared-only (GetDeclaredMethod), require
  RefKind.None params, and reject ambiguous overloads.
- Guard declaring types to be closed + referenceable.
- Escape emitted member/method identifiers so reserved-keyword names (e.g.
  @Class) compile.

Also: add UTF-8 BOM to the four new .cs files, and correct the
DynamicDataSourceResolver doc to describe the DAM-safe reflection fallback (#9832)
rather than claiming it is unsafe.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f5797f94-192e-479e-a45f-2b2f8094c93b
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants