Make DynamicData trimming/NativeAOT-safe via DynamicallyAccessedMembers - #9832

Merged
Amaury Levé (Evangelink) merged 3 commits into
mainfrom
dev/amauryleve/literate-journey
Jul 11, 2026
Merged

Make DynamicData trimming/NativeAOT-safe via DynamicallyAccessedMembers#9832
Amaury Levé (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, truthfulIL2073 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>
CopilotAI review requested due to automatic review settings July 10, 2026 15:56

CopilotAI 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
FileDescription
InternalAPI.Unshipped.txtTracks new internal symbols.
DynamicDataOperations.csAdds trimming requirements and declaring-type helper.
DynamicDataAttribute.csAnnotates constructors, storage, and display-name types.

Review details

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

@github-actionsgithub-actionsBot 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

#DimensionSeverityFinding
1Public API Surface🔴 MajorAdding [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.
2Trimming Correctness🟡 SuggestionThe IL2073 suppression on GetTestMethodDeclaringType is justified today because [TestClass] roots the type. Consider enriching the justification string to mention that invariant explicitly.
3Over-preservationi️ InfoRequiredMemberTypes 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.

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

CopilotAI 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

@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:

FileLineClass
FileLoggerTests.cs465SynchronousLoopStartingTask.RunLongRunning
FileLoggerTests.cs490NeverCompletingTask.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")]publicTaskRunLongRunning(Func<Task>action,stringname,CancellationTokencancellationToken)=>_inner.RunLongRunning(action,name,cancellationToken);// Option 2: Suppress the warning (preferred for test code)
#pragma warning disable CA1416// Validate platform compatibilitypublicTaskRunLongRunning(Func<Task>action,stringname,CancellationTokencancellationToken)=>_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>
CopilotAI review requested due to automatic review settings July 11, 2026 12:31
@Evangelink
Amaury Levé (Evangelink) merged commit 01014ae into mainJul 11, 2026
25 of 35 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/literate-journey branch July 11, 2026 12:33

CopilotAI 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

Amaury Levé (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 freeto 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

@Evangelink
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Make DynamicData trimming/NativeAOT-safe via DynamicallyAccessedMembers - #9832

Merged
Amaury Levé (Evangelink) merged 3 commits into
mainfrom
dev/amauryleve/literate-journey
Jul 11, 2026
Merged

Make DynamicData trimming/NativeAOT-safe via DynamicallyAccessedMembers#9832
Amaury Levé (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, truthfulIL2073 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>
CopilotAI review requested due to automatic review settings July 10, 2026 15:56

CopilotAI 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
FileDescription
InternalAPI.Unshipped.txtTracks new internal symbols.
DynamicDataOperations.csAdds trimming requirements and declaring-type helper.
DynamicDataAttribute.csAnnotates constructors, storage, and display-name types.

Review details

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

@github-actionsgithub-actionsBot 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

#DimensionSeverityFinding
1Public API Surface🔴 MajorAdding [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.
2Trimming Correctness🟡 SuggestionThe IL2073 suppression on GetTestMethodDeclaringType is justified today because [TestClass] roots the type. Consider enriching the justification string to mention that invariant explicitly.
3Over-preservationi️ InfoRequiredMemberTypes 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.

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

CopilotAI 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

@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:

FileLineClass
FileLoggerTests.cs465SynchronousLoopStartingTask.RunLongRunning
FileLoggerTests.cs490NeverCompletingTask.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")]publicTaskRunLongRunning(Func<Task>action,stringname,CancellationTokencancellationToken)=>_inner.RunLongRunning(action,name,cancellationToken);// Option 2: Suppress the warning (preferred for test code)
#pragma warning disable CA1416// Validate platform compatibilitypublicTaskRunLongRunning(Func<Task>action,stringname,CancellationTokencancellationToken)=>_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>
CopilotAI review requested due to automatic review settings July 11, 2026 12:31
@Evangelink
Amaury Levé (Evangelink) merged commit 01014ae into mainJul 11, 2026
25 of 35 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/literate-journey branch July 11, 2026 12:33

CopilotAI 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

Amaury Levé (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 freeto 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

@Evangelink
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Make DynamicData trimming/NativeAOT-safe via DynamicallyAccessedMembers - #9832

Merged
Amaury Levé (Evangelink) merged 3 commits into
mainfrom
dev/amauryleve/literate-journey
Jul 11, 2026
Merged

Make DynamicData trimming/NativeAOT-safe via DynamicallyAccessedMembers#9832
Amaury Levé (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, truthfulIL2073 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>
CopilotAI review requested due to automatic review settings July 10, 2026 15:56

CopilotAI 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
FileDescription
InternalAPI.Unshipped.txtTracks new internal symbols.
DynamicDataOperations.csAdds trimming requirements and declaring-type helper.
DynamicDataAttribute.csAnnotates constructors, storage, and display-name types.

Review details

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

@github-actionsgithub-actionsBot 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

#DimensionSeverityFinding
1Public API Surface🔴 MajorAdding [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.
2Trimming Correctness🟡 SuggestionThe IL2073 suppression on GetTestMethodDeclaringType is justified today because [TestClass] roots the type. Consider enriching the justification string to mention that invariant explicitly.
3Over-preservationi️ InfoRequiredMemberTypes 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.

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

CopilotAI 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

@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:

FileLineClass
FileLoggerTests.cs465SynchronousLoopStartingTask.RunLongRunning
FileLoggerTests.cs490NeverCompletingTask.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")]publicTaskRunLongRunning(Func<Task>action,stringname,CancellationTokencancellationToken)=>_inner.RunLongRunning(action,name,cancellationToken);// Option 2: Suppress the warning (preferred for test code)
#pragma warning disable CA1416// Validate platform compatibilitypublicTaskRunLongRunning(Func<Task>action,stringname,CancellationTokencancellationToken)=>_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>
CopilotAI review requested due to automatic review settings July 11, 2026 12:31
@Evangelink
Amaury Levé (Evangelink) merged commit 01014ae into mainJul 11, 2026
25 of 35 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/literate-journey branch July 11, 2026 12:33

CopilotAI 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

Amaury Levé (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 freeto 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

@Evangelink
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Make DynamicData trimming/NativeAOT-safe via DynamicallyAccessedMembers - #9832

Merged
Amaury Levé (Evangelink) merged 3 commits into
mainfrom
dev/amauryleve/literate-journey
Jul 11, 2026
Merged

Make DynamicData trimming/NativeAOT-safe via DynamicallyAccessedMembers#9832
Amaury Levé (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, truthfulIL2073 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>
CopilotAI review requested due to automatic review settings July 10, 2026 15:56

CopilotAI 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
FileDescription
InternalAPI.Unshipped.txtTracks new internal symbols.
DynamicDataOperations.csAdds trimming requirements and declaring-type helper.
DynamicDataAttribute.csAnnotates constructors, storage, and display-name types.

Review details

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

@github-actionsgithub-actionsBot 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

#DimensionSeverityFinding
1Public API Surface🔴 MajorAdding [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.
2Trimming Correctness🟡 SuggestionThe IL2073 suppression on GetTestMethodDeclaringType is justified today because [TestClass] roots the type. Consider enriching the justification string to mention that invariant explicitly.
3Over-preservationi️ InfoRequiredMemberTypes 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.

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

CopilotAI 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

@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:

FileLineClass
FileLoggerTests.cs465SynchronousLoopStartingTask.RunLongRunning
FileLoggerTests.cs490NeverCompletingTask.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")]publicTaskRunLongRunning(Func<Task>action,stringname,CancellationTokencancellationToken)=>_inner.RunLongRunning(action,name,cancellationToken);// Option 2: Suppress the warning (preferred for test code)
#pragma warning disable CA1416// Validate platform compatibilitypublicTaskRunLongRunning(Func<Task>action,stringname,CancellationTokencancellationToken)=>_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>
CopilotAI review requested due to automatic review settings July 11, 2026 12:31
@Evangelink
Amaury Levé (Evangelink) merged commit 01014ae into mainJul 11, 2026
25 of 35 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/literate-journey branch July 11, 2026 12:33

CopilotAI 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

Amaury Levé (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 freeto 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

@Evangelink
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Make DynamicData trimming/NativeAOT-safe via DynamicallyAccessedMembers - #9832

Merged
Amaury Levé (Evangelink) merged 3 commits into
mainfrom
dev/amauryleve/literate-journey
Jul 11, 2026
Merged

Make DynamicData trimming/NativeAOT-safe via DynamicallyAccessedMembers#9832
Amaury Levé (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, truthfulIL2073 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>
CopilotAI review requested due to automatic review settings July 10, 2026 15:56

CopilotAI 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
FileDescription
InternalAPI.Unshipped.txtTracks new internal symbols.
DynamicDataOperations.csAdds trimming requirements and declaring-type helper.
DynamicDataAttribute.csAnnotates constructors, storage, and display-name types.

Review details

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

@github-actionsgithub-actionsBot 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

#DimensionSeverityFinding
1Public API Surface🔴 MajorAdding [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.
2Trimming Correctness🟡 SuggestionThe IL2073 suppression on GetTestMethodDeclaringType is justified today because [TestClass] roots the type. Consider enriching the justification string to mention that invariant explicitly.
3Over-preservationi️ InfoRequiredMemberTypes 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.

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

CopilotAI 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

@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:

FileLineClass
FileLoggerTests.cs465SynchronousLoopStartingTask.RunLongRunning
FileLoggerTests.cs490NeverCompletingTask.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")]publicTaskRunLongRunning(Func<Task>action,stringname,CancellationTokencancellationToken)=>_inner.RunLongRunning(action,name,cancellationToken);// Option 2: Suppress the warning (preferred for test code)
#pragma warning disable CA1416// Validate platform compatibilitypublicTaskRunLongRunning(Func<Task>action,stringname,CancellationTokencancellationToken)=>_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>
CopilotAI review requested due to automatic review settings July 11, 2026 12:31
@Evangelink
Amaury Levé (Evangelink) merged commit 01014ae into mainJul 11, 2026
25 of 35 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/literate-journey branch July 11, 2026 12:33

CopilotAI 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

Amaury Levé (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 freeto 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

@Evangelink
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Make DynamicData trimming/NativeAOT-safe via DynamicallyAccessedMembers - #9832

Merged
Amaury Levé (Evangelink) merged 3 commits into
mainfrom
dev/amauryleve/literate-journey
Jul 11, 2026
Merged

Make DynamicData trimming/NativeAOT-safe via DynamicallyAccessedMembers#9832
Amaury Levé (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, truthfulIL2073 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>
CopilotAI review requested due to automatic review settings July 10, 2026 15:56

CopilotAI 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
FileDescription
InternalAPI.Unshipped.txtTracks new internal symbols.
DynamicDataOperations.csAdds trimming requirements and declaring-type helper.
DynamicDataAttribute.csAnnotates constructors, storage, and display-name types.

Review details

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

@github-actionsgithub-actionsBot 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

#DimensionSeverityFinding
1Public API Surface🔴 MajorAdding [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.
2Trimming Correctness🟡 SuggestionThe IL2073 suppression on GetTestMethodDeclaringType is justified today because [TestClass] roots the type. Consider enriching the justification string to mention that invariant explicitly.
3Over-preservationi️ InfoRequiredMemberTypes 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.

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

CopilotAI 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

@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:

FileLineClass
FileLoggerTests.cs465SynchronousLoopStartingTask.RunLongRunning
FileLoggerTests.cs490NeverCompletingTask.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")]publicTaskRunLongRunning(Func<Task>action,stringname,CancellationTokencancellationToken)=>_inner.RunLongRunning(action,name,cancellationToken);// Option 2: Suppress the warning (preferred for test code)
#pragma warning disable CA1416// Validate platform compatibilitypublicTaskRunLongRunning(Func<Task>action,stringname,CancellationTokencancellationToken)=>_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>
CopilotAI review requested due to automatic review settings July 11, 2026 12:31
@Evangelink
Amaury Levé (Evangelink) merged commit 01014ae into mainJul 11, 2026
25 of 35 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/literate-journey branch July 11, 2026 12:33

CopilotAI 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

Amaury Levé (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 freeto 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

@Evangelink
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Make DynamicData trimming/NativeAOT-safe via DynamicallyAccessedMembers - #9832

Merged
Amaury Levé (Evangelink) merged 3 commits into
mainfrom
dev/amauryleve/literate-journey
Jul 11, 2026
Merged

Make DynamicData trimming/NativeAOT-safe via DynamicallyAccessedMembers#9832
Amaury Levé (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, truthfulIL2073 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>
CopilotAI review requested due to automatic review settings July 10, 2026 15:56

CopilotAI 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
FileDescription
InternalAPI.Unshipped.txtTracks new internal symbols.
DynamicDataOperations.csAdds trimming requirements and declaring-type helper.
DynamicDataAttribute.csAnnotates constructors, storage, and display-name types.

Review details

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

@github-actionsgithub-actionsBot 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

#DimensionSeverityFinding
1Public API Surface🔴 MajorAdding [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.
2Trimming Correctness🟡 SuggestionThe IL2073 suppression on GetTestMethodDeclaringType is justified today because [TestClass] roots the type. Consider enriching the justification string to mention that invariant explicitly.
3Over-preservationi️ InfoRequiredMemberTypes 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.

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

CopilotAI 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

@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:

FileLineClass
FileLoggerTests.cs465SynchronousLoopStartingTask.RunLongRunning
FileLoggerTests.cs490NeverCompletingTask.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")]publicTaskRunLongRunning(Func<Task>action,stringname,CancellationTokencancellationToken)=>_inner.RunLongRunning(action,name,cancellationToken);// Option 2: Suppress the warning (preferred for test code)
#pragma warning disable CA1416// Validate platform compatibilitypublicTaskRunLongRunning(Func<Task>action,stringname,CancellationTokencancellationToken)=>_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>
CopilotAI review requested due to automatic review settings July 11, 2026 12:31
@Evangelink
Amaury Levé (Evangelink) merged commit 01014ae into mainJul 11, 2026
25 of 35 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/literate-journey branch July 11, 2026 12:33

CopilotAI 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

Amaury Levé (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 freeto 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

@Evangelink
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Make DynamicData trimming/NativeAOT-safe via DynamicallyAccessedMembers - #9832

Merged
Amaury Levé (Evangelink) merged 3 commits into
mainfrom
dev/amauryleve/literate-journey
Jul 11, 2026
Merged

Make DynamicData trimming/NativeAOT-safe via DynamicallyAccessedMembers#9832
Amaury Levé (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, truthfulIL2073 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>
CopilotAI review requested due to automatic review settings July 10, 2026 15:56

CopilotAI 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
FileDescription
InternalAPI.Unshipped.txtTracks new internal symbols.
DynamicDataOperations.csAdds trimming requirements and declaring-type helper.
DynamicDataAttribute.csAnnotates constructors, storage, and display-name types.

Review details

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

@github-actionsgithub-actionsBot 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

#DimensionSeverityFinding
1Public API Surface🔴 MajorAdding [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.
2Trimming Correctness🟡 SuggestionThe IL2073 suppression on GetTestMethodDeclaringType is justified today because [TestClass] roots the type. Consider enriching the justification string to mention that invariant explicitly.
3Over-preservationi️ InfoRequiredMemberTypes 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.

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

CopilotAI 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

@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:

FileLineClass
FileLoggerTests.cs465SynchronousLoopStartingTask.RunLongRunning
FileLoggerTests.cs490NeverCompletingTask.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")]publicTaskRunLongRunning(Func<Task>action,stringname,CancellationTokencancellationToken)=>_inner.RunLongRunning(action,name,cancellationToken);// Option 2: Suppress the warning (preferred for test code)
#pragma warning disable CA1416// Validate platform compatibilitypublicTaskRunLongRunning(Func<Task>action,stringname,CancellationTokencancellationToken)=>_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>
CopilotAI review requested due to automatic review settings July 11, 2026 12:31
@Evangelink
Amaury Levé (Evangelink) merged commit 01014ae into mainJul 11, 2026
25 of 35 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/literate-journey branch July 11, 2026 12:33

CopilotAI 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

Amaury Levé (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 freeto 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

@Evangelink