[ci-fix] Use DoNotWrapExceptions across ReflectionMemberAccessor Invoke paths#129212
[ci-fix] Use DoNotWrapExceptions across ReflectionMemberAccessor Invoke paths#129212github-actions[bot] wants to merge 10 commits into
Conversation
… accessors On Mono/tvOS (AOT environments), System.Text.Json uses ReflectionMemberAccessor which calls MethodInfo.Invoke() for property getters/setters. This wraps any exception thrown by the property in TargetInvocationException, unlike CoreCLR which uses IL-emitted delegates that propagate exceptions directly. The constructor delegates in the same class already unwrap TargetInvocationException; this commit adds the same treatment to CreatePropertyGetter and CreatePropertySetter using ExceptionDispatchInfo.Capture to preserve the original stack trace. Fixes the tvOS Mono test failure where AsyncEnumerableTests.SerializeAsyncEnumerable_PartialItemFailure tests expect InvalidOperationException but receive TargetInvocationException. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: kotlarmilos <11523312+kotlarmilos@users.noreply.github.com>
Co-authored-by: kotlarmilos <11523312+kotlarmilos@users.noreply.github.com>
Co-authored-by: kotlarmilos <11523312+kotlarmilos@users.noreply.github.com>
Co-authored-by: kotlarmilos <11523312+kotlarmilos@users.noreply.github.com>
Co-authored-by: kotlarmilos <11523312+kotlarmilos@users.noreply.github.com>
Co-authored-by: kotlarmilos <11523312+kotlarmilos@users.noreply.github.com>
Co-authored-by: kotlarmilos <11523312+kotlarmilos@users.noreply.github.com>
…essor Invoke paths Apply BindingFlags.DoNotWrapExceptions on NET and a single stack preserving ExceptionDispatchInfo based catch and rethrow on non NET across every user code Invoke site, and revert the unrelated union accessor null check for a separate PR. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Updates System.Text.Json’s reflection-based MemberAccessor implementation to avoid TargetInvocationException wrapping when invoking constructors/methods via reflection, aligning exception propagation with the ReflectionEmit-based accessor paths.
Changes:
- Use
BindingFlags.DoNotWrapExceptionsfor reflectionInvokecalls when targeting#if NET. - On non-
NETTFMs, unwrapTargetInvocationException.InnerExceptionviaExceptionDispatchInfoto preserve the original exception and stack. - Apply this across parameterless/parameterized constructors, add-method delegates, and property getter/setter delegates.
Show a summary per file
| File | Description |
|---|---|
| src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ReflectionMemberAccessor.cs | Adjusts reflection invocation patterns to avoid exception wrapping and preserve original exception behavior. |
Copilot's findings
- Files reviewed: 1/1 changed files
- Comments generated: 1
| return () => | ||
| { | ||
| #if NET | ||
| return ctorInfo.Invoke( | ||
| BindingFlags.DoNotWrapExceptions, | ||
| binder: null, | ||
| parameters: null, | ||
| culture: null); | ||
| #else |
|
Workflow state for the Holistic Review Orchestrator. {
"version": 5,
"last_dispatched_commit": "c3d9f506f9c16a96845011d9b213ec9e5d243f19",
"last_dispatched_base_ref": "main",
"last_dispatched_base_sha": "24547a76ba95bee359f4c0b58dd98976973aa797",
"last_reviewed_commit": "c3d9f506f9c16a96845011d9b213ec9e5d243f19",
"last_reviewed_base_ref": "main",
"last_reviewed_base_sha": "24547a76ba95bee359f4c0b58dd98976973aa797",
"last_recorded_worker_run_id": "29679151104",
"review_attempt_commit": "",
"review_attempt_base_ref": "",
"review_attempt_count": 0,
"max_review_attempts": 5,
"review_history_format": "holistic-review-disclosure-v1",
"review_history": [
{
"commit": "c3d9f506f9c16a96845011d9b213ec9e5d243f19",
"review_id": 4730524249
}
]
} |
There was a problem hiding this comment.
Holistic Review
Motivation: Justified. On Mono/tvOS AOT, System.Text.Json uses ReflectionMemberAccessor, whose MethodBase.Invoke wraps target exceptions in TargetInvocationException, whereas CoreCLR's ReflectionEmitMemberAccessor emits direct IL calls that propagate the original exception. This divergence caused AsyncEnumerableTests.SerializeAsyncEnumerable_*_PartialItemFailure_* to observe TargetInvocationException instead of the expected InvalidOperationException on tvos-arm64 Mono (KBE #128766). The problem is real and the platform-dependent behavior gap is a legitimate correctness issue.
Approach: Sound and consistent with the codebase. On NET, every reflection Invoke call site now passes BindingFlags.DoNotWrapExceptions, which matches the exception semantics of the emit-based accessor (verified: Mono's RuntimeType.Mono.cs honors this flag). The #else (netstandard/netfx) fallbacks replace the previous throw e.InnerException ?? e with ExceptionDispatchInfo.Capture(e.InnerException).Throw(), which is a strict improvement because it preserves the original stack trace rather than resetting it. The when (e.InnerException is not null) guard is correct: if there is no inner exception, the original TargetInvocationException now propagates unchanged instead of being rethrown as itself, which is fine. Void-returning delegates (add-method, setter) correctly omit the throw; // unreachable.
Summary: NET and fallback paths. My only reservation is empirical: the fix's effectiveness on the actual failing platform (tvOS Mono AOT) depends on CI confirmation, since the author states full-matrix validation still relies on CI legs and local validation was targeted. A human should confirm the relevant Mono/tvOS CI leg now passes before merge. No blocking code defects found.
Detailed Findings
✅ Correctness — Exception propagation aligned with emit accessor
Applying BindingFlags.DoNotWrapExceptions on NET makes ReflectionMemberAccessor propagate the underlying exception directly, matching ReflectionEmitMemberAccessor's direct-IL behavior. All eight Invoke sites (parameterless/parameterized/single-parameter constructors, add-method delegate, both property getters, property setter) are updated uniformly, so the behavior is now consistent across every member-access path rather than only the constructor sites originally implicated.
✅ Correctness — Fallback path is a strict improvement
The pre-existing constructor fallbacks used throw e.InnerException ?? e, which rethrows the inner exception with a reset stack trace. Switching to ExceptionDispatchInfo.Capture(e.InnerException).Throw() preserves the original stack trace. The newly added when (e.InnerException is not null) filter means a TargetInvocationException with no inner exception now propagates as-is rather than being caught and rethrown — acceptable, since that case is not the scenario being fixed.
✅ Test coverage — Existing regression tests exercise the path
No new test is added, which is appropriate: the failing AsyncEnumerableTests.SerializeAsyncEnumerable_*_PartialItemFailure_PriorItemsAreFullyWritten tests already assert the expected exception type and directly cover the corrected behavior. The value is in those tests now passing under the reflection-based accessor.
⚠️ Verification — Platform validation pending
The author notes local validation was targeted and full tvOS Mono AOT validation depends on CI. Since the entire motivation is a platform-specific behavior gap, confirmation from the relevant Mono/tvOS CI leg (not just CoreCLR, which already used the emit accessor and passed) is the key remaining check before merge.
Note
This review was generated by this repository's Holistic Review agentic workflow to complement the built-in Copilot review.
Generated by Holistic Review · 77.3 AIC · ⌖ 10.9 AIC · ⊞ 10K
Workflow artifact: ci-fix
Artifact kind: help
Linked KBE: #128766
> [!NOTE]
> This is an AI/Copilot-generated fix updated after PR feedback and validated with targeted local build/test runs.
Root cause (best analysis)
AsyncEnumerableTests.SerializeAsyncEnumerable_*_PartialItemFailure_PriorItemsAreFullyWrittenfails on tvos-arm64 AllSubsets_Mono because the test expectsInvalidOperationExceptionbut receivesTargetInvocationException.The failing log line:
On Mono/tvOS (AOT environments),
System.Text.JsonusesReflectionMemberAccessor, where reflectionInvokecan wrap target exceptions. CoreCLR commonly usesReflectionEmitMemberAccessor, which propagates underlying exceptions directly.Attempted fix
Updated
ReflectionMemberAccessorto applyBindingFlags.DoNotWrapExceptionsonNETtargets across relevant reflectionInvokecall sites in the file (including property accessors and otherInvokeusages raised in review), while preserving non-NETcompatibility paths.Additional polish from feedback:
ReflectionEmitMemberAccessorfor add-method delegate behavior.What is unverified / where I need help
Validation
./build.sh clr+libs -rc release✅./build.sh clr+libs+host -rc release -lc release✅dotnet build src/libraries/System.Text.Json/src/System.Text.Json.csproj -c Release✅dotnet build /t:test src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj -p:Configuration=Release /p:XunitMethodName=System.Text.Json.Serialization.Tests.AsyncEnumerableTests_AsyncPipeSerializer.SerializeAsyncEnumerable_TopLevelValues_PartialItemFailure_PriorItemsAreFullyWritten✅Evidence
Help wanted
area-System.Text.Json):@StephenMolloy,@dotnet/area-system-text-jsonFiled by
ci-failure-fix. Comment here or on the workflow file to suggest changes;ci-failure-scan-feedbackreads in-scope feedback daily and opens (or updates) a PR with prompt edits.> [!WARNING]
>
> Generated by CI Outer-Loop Failure Fixer · ● 35M · ◷