Skip to content

Add MainThread.SetCustomImplementation for custom platform backends#34230

Open
Redth wants to merge 4 commits intomainfrom
dev/redth/fix-34101
Open

Add MainThread.SetCustomImplementation for custom platform backends#34230
Redth wants to merge 4 commits intomainfrom
dev/redth/fix-34101

Conversation

@Redth
Copy link
Copy Markdown
Member

@Redth Redth commented Feb 25, 2026

Note

Are you waiting for the changes in this PR to be merged?
It would be very helpful if you could test the resulting artifacts from this PR and let us know in a comment if this change resolves your issue. Thank you!

Description

Fixes #34101

On custom platform backends (e.g. Linux/GTK), MainThread.BeginInvokeOnMainThread() and MainThread.InvokeOnMainThreadAsync() throw NotImplementedInReferenceAssemblyException because there is no platform-specific partial class implementation for the netstandard TFM.

This PR adds a public MainThread.SetCustomImplementation() method that allows custom backends to register their own IsMainThread and BeginInvokeOnMainThread implementations.

Changes

  • MainThread.shared.cs: Added static callback fields and SetCustomImplementation(Func<bool>, Action<Action>) public method
  • MainThread.netstandard.cs: Modified to check for custom callbacks before throwing NotImplementedInReferenceAssemblyException
  • PublicAPI.Unshipped.txt: Added new public API entry for all targets
  • MainThread_Tests.cs: Added 6 unit tests covering the new functionality

Usage

Custom platform backends can register their implementation during startup:

MainThread.SetCustomImplementation(
    isMainThread: () => /* check if on UI thread */,
    beginInvokeOnMainThread: action => /* dispatch to UI thread */);

For example, a GTK backend might use:

MainThread.SetCustomImplementation(
    isMainThread: () => GLib.MainContext.Default.IsOwner,
    beginInvokeOnMainThread: action => GLib.Idle.Add(() => { action(); return false; }));

On standard platforms (iOS, Android, Windows, MacCatalyst), the existing platform-specific implementations continue to be used unchanged.

Fixes #34101. On custom platform backends (e.g. Linux/GTK), MainThread
throws NotImplementedInReferenceAssemblyException because there is no
platform-specific partial class implementation. This adds a public
SetCustomImplementation method that allows custom backends to provide
their own IsMainThread and BeginInvokeOnMainThread implementations.

Changes:
- Add SetCustomImplementation(Func<bool>, Action<Action>) to MainThread
- Modify MainThread.netstandard.cs to use custom callbacks before throwing
- Add unit tests verifying the new functionality
- Update PublicAPI.Unshipped.txt for all targets

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings February 25, 2026 02:01
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds an opt-in mechanism for custom platform backends (e.g., Linux/GTK) to provide MainThread behavior when the portable (*.netstandard.cs) implementation would otherwise throw, addressing the “custom backend throws NotImplementedInReferenceAssemblyException” scenario.

Changes:

  • Added MainThread.SetCustomImplementation(Func<bool>, Action<Action>) and backing delegate fields.
  • Updated the netstandard MainThread implementation to use the custom delegates when present.
  • Added PublicAPI entries across TFMs and introduced new unit tests for the custom-implementation behavior.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
src/Essentials/src/MainThread/MainThread.shared.cs Adds public API + stores delegates for custom main-thread detection/dispatch.
src/Essentials/src/MainThread/MainThread.netstandard.cs Uses custom delegates before throwing the reference-assembly exception.
src/Essentials/src/PublicAPI/netstandard/PublicAPI.Unshipped.txt Adds new API entry (but has BOM/format concerns).
src/Essentials/src/PublicAPI/net/PublicAPI.Unshipped.txt Adds new API entry (but has BOM/format concerns).
src/Essentials/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt Adds new API entry (format appears inconsistent with shipped conventions).
src/Essentials/src/PublicAPI/net-tizen/PublicAPI.Unshipped.txt Adds new API entry (format appears inconsistent with shipped conventions).
src/Essentials/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt Adds new API entry (format appears inconsistent with shipped conventions).
src/Essentials/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt Adds new API entry (format appears inconsistent with shipped conventions).
src/Essentials/src/PublicAPI/net-android/PublicAPI.Unshipped.txt Adds new API entry (format appears inconsistent with shipped conventions).
src/Essentials/test/UnitTests/MainThread_Tests.cs Adds unit tests validating custom-implementation behavior and argument validation.

@@ -1 +1,2 @@
#nullable enable
static Microsoft.Maui.ApplicationModel.MainThread.SetCustomImplementation(System.Func<bool>! isMainThread, System.Action<System.Action!>! beginInvokeOnMainThread) -> void
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

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

PublicAPI formatting here is inconsistent with the rest of this TFM's shipped APIs (which use the ~static prefix and omit nullable ! annotations for MainThread members). As written, this entry likely won't match the API as generated from the current (nullable-oblivious) source. Update the entry to the correct PublicAPI format for this assembly (likely ~static ... without !).

Suggested change
static Microsoft.Maui.ApplicationModel.MainThread.SetCustomImplementation(System.Func<bool>! isMainThread, System.Action<System.Action!>! beginInvokeOnMainThread) -> void
~static Microsoft.Maui.ApplicationModel.MainThread.SetCustomImplementation(System.Func<bool> isMainThread, System.Action<System.Action> beginInvokeOnMainThread) -> void

Copilot uses AI. Check for mistakes.
@@ -1 +1,2 @@
#nullable enable
static Microsoft.Maui.ApplicationModel.MainThread.SetCustomImplementation(System.Func<bool>! isMainThread, System.Action<System.Action!>! beginInvokeOnMainThread) -> void
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

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

PublicAPI formatting here is inconsistent with the rest of this TFM's shipped APIs (which use the ~static prefix and omit nullable ! annotations for MainThread members). As written, this entry likely won't match the API as generated from the current (nullable-oblivious) source. Update the entry to the correct PublicAPI format for this assembly (likely ~static ... without !).

Suggested change
static Microsoft.Maui.ApplicationModel.MainThread.SetCustomImplementation(System.Func<bool>! isMainThread, System.Action<System.Action!>! beginInvokeOnMainThread) -> void
~static Microsoft.Maui.ApplicationModel.MainThread.SetCustomImplementation(System.Func<bool> isMainThread, System.Action<System.Action> beginInvokeOnMainThread) -> void

Copilot uses AI. Check for mistakes.
Comment on lines +24 to +25
s_isMainThreadImplementation = isMainThread ?? throw new ArgumentNullException(nameof(isMainThread));
s_beginInvokeOnMainThreadImplementation = beginInvokeOnMainThread ?? throw new ArgumentNullException(nameof(beginInvokeOnMainThread));
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

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

SetCustomImplementation mutates s_isMainThreadImplementation before validating beginInvokeOnMainThread. If beginInvokeOnMainThread is null, the method throws but leaves the class in a partially-updated state (IsMainThread may start returning a value instead of throwing). Validate both parameters first (or assign atomically) so the method is exception-safe and does not change state on failure.

Suggested change
s_isMainThreadImplementation = isMainThread ?? throw new ArgumentNullException(nameof(isMainThread));
s_beginInvokeOnMainThreadImplementation = beginInvokeOnMainThread ?? throw new ArgumentNullException(nameof(beginInvokeOnMainThread));
if (isMainThread is null)
throw new ArgumentNullException(nameof(isMainThread));
if (beginInvokeOnMainThread is null)
throw new ArgumentNullException(nameof(beginInvokeOnMainThread));
s_isMainThreadImplementation = isMainThread;
s_beginInvokeOnMainThreadImplementation = beginInvokeOnMainThread;

Copilot uses AI. Check for mistakes.
Comment on lines +69 to +76
// Use reflection to clear the static fields since there's no public reset API
var type = typeof(MainThread);
var isMainThreadField = type.GetField("s_isMainThreadImplementation",
System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic);
var beginInvokeField = type.GetField("s_beginInvokeOnMainThreadImplementation",
System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic);
isMainThreadField?.SetValue(null, null);
beginInvokeField?.SetValue(null, null);
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

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

These tests reset MainThread state by reflecting on private field names. This is brittle (renames/ILLink changes can break tests) and bypasses InternalsVisibleTo that the Essentials assembly already provides. Consider adding an internal reset helper on MainThread (accessible to Microsoft.Maui.Essentials.UnitTests) and call that instead of reflection.

Suggested change
// Use reflection to clear the static fields since there's no public reset API
var type = typeof(MainThread);
var isMainThreadField = type.GetField("s_isMainThreadImplementation",
System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic);
var beginInvokeField = type.GetField("s_beginInvokeOnMainThreadImplementation",
System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic);
isMainThreadField?.SetValue(null, null);
beginInvokeField?.SetValue(null, null);
// Use the internal reset helper on MainThread instead of reflection on private fields
MainThread.Reset();

Copilot uses AI. Check for mistakes.
@@ -1 +1,2 @@
#nullable enable
#nullable enable
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

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

The first line now contains a BOM character before #nullable enable (shows up as �#nullable enable). This frequently causes PublicAPI tooling diffs/failures. Please remove the BOM so the line is exactly #nullable enable.

Suggested change
#nullable enable
#nullable enable

Copilot uses AI. Check for mistakes.
@@ -1 +1,2 @@
#nullable enable
#nullable enable
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

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

The first line now contains a BOM character before #nullable enable. This frequently causes PublicAPI tooling diffs/failures. Please remove the BOM so the line is exactly #nullable enable.

Suggested change
#nullable enable
#nullable enable

Copilot uses AI. Check for mistakes.
@@ -1 +1,2 @@
#nullable enable
static Microsoft.Maui.ApplicationModel.MainThread.SetCustomImplementation(System.Func<bool>! isMainThread, System.Action<System.Action!>! beginInvokeOnMainThread) -> void
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

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

PublicAPI formatting here is inconsistent with the rest of this TFM's shipped APIs (which use the ~static prefix and omit nullable ! annotations for MainThread members). As written, this entry likely won't match the API as generated from the current (nullable-oblivious) source. Update the entry to the correct PublicAPI format for this assembly (likely ~static ... without !).

Suggested change
static Microsoft.Maui.ApplicationModel.MainThread.SetCustomImplementation(System.Func<bool>! isMainThread, System.Action<System.Action!>! beginInvokeOnMainThread) -> void
~static Microsoft.Maui.ApplicationModel.MainThread.SetCustomImplementation(System.Func<bool> isMainThread, System.Action<System.Action> beginInvokeOnMainThread) -> void

Copilot uses AI. Check for mistakes.
@@ -1 +1,2 @@
#nullable enable
static Microsoft.Maui.ApplicationModel.MainThread.SetCustomImplementation(System.Func<bool>! isMainThread, System.Action<System.Action!>! beginInvokeOnMainThread) -> void
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

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

PublicAPI formatting here is inconsistent with the rest of this TFM's shipped APIs (which use the ~static prefix and omit nullable ! annotations for MainThread members). As written, this entry likely won't match the API as generated from the current (nullable-oblivious) source. Update the entry to the correct PublicAPI format for this assembly (likely ~static ... without !).

Suggested change
static Microsoft.Maui.ApplicationModel.MainThread.SetCustomImplementation(System.Func<bool>! isMainThread, System.Action<System.Action!>! beginInvokeOnMainThread) -> void
~static Microsoft.Maui.ApplicationModel.MainThread.SetCustomImplementation(System.Func<bool> isMainThread, System.Action<System.Action> beginInvokeOnMainThread) -> void

Copilot uses AI. Check for mistakes.
@@ -1 +1,2 @@
#nullable enable
static Microsoft.Maui.ApplicationModel.MainThread.SetCustomImplementation(System.Func<bool>! isMainThread, System.Action<System.Action!>! beginInvokeOnMainThread) -> void
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

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

PublicAPI formatting here is inconsistent with the rest of this TFM's shipped APIs (which use the ~static prefix and omit nullable ! annotations for MainThread members). As written, this entry likely won't match the API as generated from the current (nullable-oblivious) source. Update the entry to the correct PublicAPI format for this assembly (likely ~static ... without !).

Suggested change
static Microsoft.Maui.ApplicationModel.MainThread.SetCustomImplementation(System.Func<bool>! isMainThread, System.Action<System.Action!>! beginInvokeOnMainThread) -> void
~static Microsoft.Maui.ApplicationModel.MainThread.SetCustomImplementation(System.Func<bool> isMainThread, System.Action<System.Action> beginInvokeOnMainThread) -> void

Copilot uses AI. Check for mistakes.
/// </summary>
public static partial class MainThread
{
static Func<bool> s_isMainThreadImplementation;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

do we use s_ ?

- Rename s_ prefixed fields to match Essentials naming conventions
- Validate both parameters before assigning for exception safety
- Add internal ResetCustomImplementation() to replace reflection in tests
- Fix PublicAPI format to use ~static prefix matching shipped conventions
- Remove BOM from net/ and netstandard/ PublicAPI.Unshipped.txt files

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 9, 2026

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 34230

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 34230"

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.

Comment on lines +30 to +31
customIsMainThreadImplementation = isMainThread;
customBeginInvokeOnMainThreadImplementation = beginInvokeOnMainThread;
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

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

The two static fields are assigned non-atomically. If SetCustomImplementation is called on one thread while another thread calls IsMainThread or BeginInvokeOnMainThread, there's a brief window where customIsMainThreadImplementation is set but customBeginInvokeOnMainThreadImplementation is still null. This could cause an inconsistent state where IsMainThread succeeds via the custom implementation but BeginInvokeOnMainThread falls through to PlatformBeginInvokeOnMainThread which still throws NotSupportedOrImplementedException because the second delegate hasn't been assigned yet.

Consider assigning both delegates atomically, for example by storing them as a single tuple or object reference (e.g., a record/struct containing both delegates), so the implementation is swapped in one atomic reference write.

Copilot uses AI. Check for mistakes.
Comment on lines +22 to +32
public static void SetCustomImplementation(Func<bool> isMainThread, Action<Action> beginInvokeOnMainThread)
{
if (isMainThread is null)
throw new ArgumentNullException(nameof(isMainThread));

if (beginInvokeOnMainThread is null)
throw new ArgumentNullException(nameof(beginInvokeOnMainThread));

customIsMainThreadImplementation = isMainThread;
customBeginInvokeOnMainThreadImplementation = beginInvokeOnMainThread;
}
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

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

On standard platform TFMs (Android, iOS, macOS, Windows, Tizen), calling SetCustomImplementation will silently set the static fields but they will never be read — the platform-specific PlatformIsMainThread and PlatformBeginInvokeOnMainThread implementations don't check them. This means the method succeeds without error but has no effect, which could be very confusing for users.

Consider either (1) making the method throw PlatformNotSupportedException on standard platforms (e.g., via a #if or a platform flag), or (2) documenting in the XML doc that this method only takes effect on the netstandard/bare net TFMs and is ignored on standard platforms, or (3) having the platform-specific implementations also check and prefer the custom implementation when set.

Copilot uses AI. Check for mistakes.
@MauiBot
Copy link
Copy Markdown
Collaborator

MauiBot commented Mar 23, 2026

🤖 AI Summary

📊 Expand Full Review72445e5 · Address PR review feedback
🔍 Pre-Flight — Context & Validation

Issue: #34101 - MainThread.BeginInvokeOnMainThread throws on custom platform backends - Common UI-thread marshaling pattern crashes; Dispatcher works but isn't the documented/recommended path
PR: #34230 - Add MainThread.SetCustomImplementation for custom platform backends
Platforms Affected: Custom platform backends / external TFMs (for example Linux/GTK); standard Android/iOS/Windows/MacCatalyst implementations are not the target scenario
Files Changed: 9 implementation, 1 test

Key Findings

  • The linked issue requests a way for non-standard backends to supply MainThread behavior or a fallback to MAUI Dispatcher; this PR chooses the explicit registration path via MainThread.SetCustomImplementation(Func<bool>, Action<Action>).
  • The PR changes only Essentials MainThread code plus PublicAPI entries and adds one unit-test file; there are no HostApp/UI/device tests in the PR, only unit coverage.
  • Earlier review feedback about PublicAPI formatting, exception safety, and using an internal reset helper appears addressed in the current diff.
  • Two unresolved review threads remain on the latest revision: the delegates are written as two separate static fields (possible transient inconsistency), and the new public API is advertised on standard TFMs even though the PR description says standard platform implementations remain unchanged.
  • No prior agent review artifact with phase output was found in PR comments; this run needs a full fresh review.

Edge Cases From Discussion

  • Concurrent callers could observe IsMainThread using the custom delegate while BeginInvokeOnMainThread still falls back to the platform partial path.
  • Callers on standard TFMs may assume SetCustomImplementation() changes behavior there, but the PR description claims Android/iOS/Windows/MacCatalyst keep using their existing implementations.

Fix Candidates

# Source Approach Test Result Files Changed Notes
PR PR #34230 Add a public custom registration API and use it from the netstandard implementation before throwing ⏳ PENDING (Gate) src/Essentials/src/MainThread/MainThread.shared.cs, src/Essentials/src/MainThread/MainThread.netstandard.cs, PublicAPI files, src/Essentials/test/UnitTests/MainThread_Tests.cs Original PR

🚦 Gate — Test Verification

Result: ❌ SKIPPED

Gate Result: ⚠️ SKIPPED

Platform: android
Mode: Full Verification requested, but no applicable HostApp/UI test exists for this PR

  • Tests FAIL without fix: N/A
  • Tests PASS with fix: N/A

Notes

  • PR Add MainThread.SetCustomImplementation for custom platform backends #34230 adds Essentials source changes plus one unit-test file (src/Essentials/test/UnitTests/MainThread_Tests.cs); it does not add or modify TestCases.HostApp / TestCases.Shared.Tests coverage.
  • Per the gate instructions, the fail-without-fix / pass-with-fix HostApp verification cannot be performed here, so gate is skipped instead of fabricated.
  • Supplemental evidence: the targeted MainThread_Tests unit tests ran successfully in the PR state (6/6 passed).

🔧 Fix — Analysis & Comparison

Fix Candidates

# Source Approach Test Result Files Changed Notes
1 try-fix Single immutable state object published atomically instead of two separate delegate fields ✅ PASS MainThread.shared.cs, MainThread.netstandard.cs, PublicAPI files Simplest safe fix for the torn-publication bug while preserving the PR's API shape
2 try-fix Ordered volatile publication gate using release/acquire semantics across two fields ✅ PASS MainThread.shared.cs, MainThread.netstandard.cs, PublicAPI files Smaller than a wrapper object, but correctness depends on subtle memory-ordering rules
3 try-fix Shared-layer override routing with lock-protected coherent delegate snapshot across all TFMs ✅ PASS MainThread.shared.cs, tests, PublicAPI files Best tested answer to both review concerns: no torn publication and no ignored override on standard TFMs
4 try-fix Replace delegate pair with an interface-based custom implementation object used from shared code ✅ PASS MainThread.shared.cs, tests, PublicAPI files Functionally sound, but adds more API/abstraction than the scenario needs
5 try-fix Unified shared MainThreadImplementation strategy reference initialized by platform bootstrap/static constructors ✅ PASS MainThread.shared.cs, MainThread.netstandard.cs, MainThread.android.cs, MainThread.ios.tvos.watchos.macos.cs, PublicAPI files Coherent architecture, but touches too many files for this bug
6 try-fix AsyncLocal-scoped custom implementation consulted across TFMs ✅ PASS MainThread.shared.cs, MainThread.netstandard.cs, platform partials, PublicAPI files Passes tests, but the semantics are wrong for a backend-wide registration API because state becomes async-flow scoped
PR PR #34230 Public SetCustomImplementation(Func<bool>, Action<Action>) with two separate backing delegate fields used by the netstandard partial ⚠️ Gate skipped; targeted unit tests on PR passed MainThread.shared.cs, MainThread.netstandard.cs, PublicAPI files, MainThread_Tests.cs Original PR; still leaves an atomicity race and an ambiguous cross-TFM contract

Cross-Pollination

Model Round New Ideas? Details
claude-opus-4.6 1 Yes Single immutable state object with atomic publication
claude-sonnet-4.6 1 Yes Release/acquire publication gate across two fields
gpt-5.3-codex 1 Yes Shared-layer override with lock-protected coherent snapshot
gemini-3-pro-preview 1 Yes Interface-based custom implementation object
claude-opus-4.6 2 No NO NEW IDEAS
claude-sonnet-4.6 2 No NO NEW IDEAS
gpt-5.3-codex 2 Yes Unified shared strategy reference initialized by platform bootstrap/static constructors
gemini-3-pro-preview 2 No NO NEW IDEAS
claude-opus-4.6 3 No NO NEW IDEAS
claude-sonnet-4.6 3 No NO NEW IDEAS
gpt-5.3-codex 3 No NO NEW IDEAS
gemini-3-pro-preview 3 Yes Proxy MainThread through IDispatcher / platform application services (not executed because the workflow hit the 3-round cap)

Exhausted: Yes
Selected Fix: Candidate #3 — it is the strongest tested alternative because it addresses both the torn-publication race and the "public but ignored on standard TFMs" concern, while staying smaller and clearer than candidates 4-6.


📋 Report — Final Recommendation

⚠️ Final Recommendation: REQUEST CHANGES

Phase Status

Phase Status Notes
Pre-Flight ✅ COMPLETE Issue #34101 targets custom/external TFMs; PR adds Essentials source changes plus unit tests
Gate ⚠️ SKIPPED No applicable HostApp/UI test exists for Android; targeted MainThread_Tests still passed locally
Try-Fix ✅ COMPLETE 6 attempts run, 6 passing candidates, 3 cross-pollination rounds completed
Report ✅ COMPLETE

Summary

The PR solves the original custom-backend gap and its added unit tests pass, but the current implementation still has two design problems: it publishes the custom MainThread delegates through two independent static writes, and it exposes a public API on every TFM even though only the netstandard path observes it. That makes the current fix harder to reason about and potentially misleading for callers on standard platforms.

Root Cause

SetCustomImplementation() currently installs the isMainThread and beginInvokeOnMainThread delegates separately in MainThread.shared.cs, while only MainThread.netstandard.cs reads them back. A concurrent caller can observe half-installed state, and callers on Android/iOS/Windows/MacCatalyst can successfully call the public API without it having any effect because their platform partials bypass the custom delegates entirely.

Fix Quality

The PR's overall direction is good and the test coverage is a solid start, but I do not think the current shape is ready as-is. The best tested alternative was Candidate #3, which moved the override decision into shared code and used a coherent snapshot so both review concerns disappeared in one place. If you want to keep the current narrower behavior, at minimum the delegate installation should still be made atomic (Candidate #1), and the standard-platform semantics should be clarified explicitly instead of remaining implicit.


@MauiBot MauiBot added s/agent-changes-requested AI agent recommends changes - found a better alternative or issues s/agent-gate-failed AI could not verify tests catch the bug s/agent-fix-win AI found a better alternative fix than the PR s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review) labels Mar 23, 2026
Redth and others added 2 commits April 1, 2026 19:16
# Conflicts:
#	src/Essentials/src/PublicAPI/net-android/PublicAPI.Unshipped.txt
#	src/Essentials/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt
#	src/Essentials/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt
#	src/Essentials/src/PublicAPI/net-tizen/PublicAPI.Unshipped.txt
#	src/Essentials/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt
#	src/Essentials/src/PublicAPI/net/PublicAPI.Unshipped.txt
#	src/Essentials/src/PublicAPI/netstandard/PublicAPI.Unshipped.txt
- Store both custom callbacks in a single sealed class (CustomMainThreadCallbacks)
  with volatile field for atomic reference swap, fixing the race condition where
  IsMainThread could use the custom impl while BeginInvokeOnMainThread still throws.
- Add <remarks> documenting that SetCustomImplementation only affects netstandard/net
  TFMs and has no effect on standard platform builds (Android, iOS, etc.).
- Validate both parameters before any state mutation (already correct, preserved).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 1, 2026

🧪 PR Test Evaluation

Overall Verdict: ✅ Tests are adequate

Unit tests are the right test type for this API-only change, and the 6 added tests cover the primary fix paths well — null argument validation, custom implementation dispatch for both IsMainThread and BeginInvokeOnMainThread, and the default exception behavior.

👍 / 👎 — Was this evaluation helpful? React to let us know!

📊 Expand Full Evaluation

PR Test Evaluation Report

PR: #34230
Test files evaluated: 1 (MainThread_Tests.cs)
Fix files: 2 (MainThread.shared.cs, MainThread.netstandard.cs)


Overall Verdict

Tests are adequate

The new unit tests cover the core scenarios of the SetCustomImplementation API: argument null guards, custom IsMainThread delegation, custom BeginInvokeOnMainThread delegation, and the no-implementation exception path. Minor gaps exist but are low risk.


1. Fix Coverage — ✅

The fix introduces a SetCustomImplementation(Func(bool), Action(Action)) public API and ResetCustomImplementation() internal method, plus a volatile CustomMainThreadCallbacks field on the MainThread class. The netstandard/net partial implementation reads customImplementation before falling back to NotSupportedOrImplementedException.

Tests exercise:

  • IsMainThread returns from custom callback (SetCustomImplementation_IsMainThread_Uses_Custom)
  • BeginInvokeOnMainThread routes through custom callback (SetCustomImplementation_BeginInvokeOnMainThread_Uses_Custom)
  • Default (no-impl) throws NotImplementedInReferenceAssemblyException for both properties
  • Null args throw ArgumentNullException

The tests would fail if the fix were reverted (custom impl wouldn't be invoked, and default behavior would differ from what's asserted).


2. Edge Cases & Gaps — ⚠️

Covered:

  • Null isMainThread argument validation
  • Null beginInvokeOnMainThread argument validation
  • Custom IsMainThread delegated correctly
  • Custom BeginInvokeOnMainThread called and action executed
  • Default behavior (no custom impl) throws expected exception

Missing:

  • Replacing an existing implementation: No test verifies that calling SetCustomImplementation a second time replaces the first one. The fix stores a new CustomMainThreadCallbacks object atomically, but this isn't tested.
  • Post-reset behavior: ResetCustomImplementation() is called in test cleanup via finally blocks, but no test explicitly asserts that after reset, MainThread.IsMainThread throws again (it's only implicitly relied on for isolation).
  • InvokeOnMainThreadAsync with custom impl: The async overloads (InvokeOnMainThreadAsync(Action), InvokeOnMainThreadAsync(Func(T)), etc.) are not tested with a custom implementation. These paths invoke BeginInvokeOnMainThread which routes through the custom callback — but it's worth verifying end-to-end.
  • Custom impl that reports IsMainThread = false: BeginInvokeOnMainThread short-circuits when IsMainThread returns true. The test for BeginInvokeOnMainThread uses isMainThread: () => false, which tests the dispatch path, but the "already on main thread" short-circuit with a custom impl is only implicitly covered (since IsMainThread delegates to PlatformIsMainThread which reads customImplementation).

3. Test Type Appropriateness — ✅

Current: Unit Tests (xUnit [Fact])
Recommendation: Correct type. This is pure logic — no platform-specific rendering, no native APIs, no visual UI. Unit tests are the lightest and most appropriate type for testing Func(bool) / Action(Action) delegation and null guard validation.


4. Convention Compliance — ✅

No issues found. Tests use [Fact] correctly, no anti-patterns detected, proper cleanup via finally block calling ClearCustomImplementation().


5. Flakiness Risk — ✅ Low

Pure synchronous unit tests with no timing dependencies, no async code in tests, no UI, no external resources. No Task.Delay or Thread.Sleep. Very stable.


6. Duplicate Coverage — ✅ No duplicates

No similar existing tests found. The MainThread_Tests.cs tests are newly added for the new SetCustomImplementation API.


7. Platform Scope — ✅

The fix explicitly targets the netstandard and bare net TFMs only. The XML docs state: "On standard platform TFMs (Android, iOS, MacCatalyst, Windows, Tizen), the built-in platform-specific implementations are always used and this method has no effect." Unit tests running on the host runtime appropriately cover the intended TFM scope.


8. Assertion Quality — ✅

Test Assertion Quality
IsMainThread_On_NetStandard_Without_Custom_Implementation Assert.Throws(NotImplementedInReferenceAssemblyException) ✅ Precise exception type
BeginInvokeOnMainThread_On_NetStandard_Without_Custom_Implementation Assert.Throws(NotImplementedInReferenceAssemblyException) ✅ Precise exception type
SetCustomImplementation_IsMainThread_Uses_Custom Assert.True(MainThread.IsMainThread) ✅ Correct result for () => true
SetCustomImplementation_BeginInvokeOnMainThread_Uses_Custom Assert.True(wasCalled) + Assert.True(actionExecuted) ✅ Both invocation and execution verified
SetCustomImplementation_Null_IsMainThread_Throws Assert.Throws(ArgumentNullException) ✅ Precise
SetCustomImplementation_Null_BeginInvoke_Throws Assert.Throws(ArgumentNullException) ✅ Precise

All assertions are specific and meaningful.


9. Fix-Test Alignment — ✅

Fix changes MainThread.SetCustomImplementation, MainThread.ResetCustomImplementation, and the netstandard partial reading customImplementation. Tests directly exercise MainThread.SetCustomImplementation, MainThread.IsMainThread, and MainThread.BeginInvokeOnMainThread — the exact API surface added/modified by the fix.


Recommendations

  1. Consider testing InvokeOnMainThreadAsync with custom impl — The async overloads use BeginInvokeOnMainThread internally, so a quick async test (e.g., await MainThread.InvokeOnMainThreadAsync(() => { }) with isMainThread: () => false) would strengthen end-to-end coverage. Low priority since the dispatch path is already covered.
  2. Consider adding a test for re-setting the implementation — Call SetCustomImplementation twice and verify the second one takes effect. This is a single-line fix to validate the atomic replacement semantics.
  3. Consider an explicit post-reset test — Verify that after ResetCustomImplementation(), the default exception is thrown again. Currently cleanup is correct but not explicitly asserted.

Warning

⚠️ Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • dc.services.visualstudio.com

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "dc.services.visualstudio.com"

See Network Configuration for more information.

Note

🔒 Integrity filtering filtered 1 item

Integrity filtering activated and filtered the following item during workflow execution.
This happens when a tool call accesses a resource that does not meet the required integrity or secrecy level of the workflow.

🧪 Test evaluation by Evaluate PR Tests

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

s/agent-changes-requested AI agent recommends changes - found a better alternative or issues s/agent-fix-win AI found a better alternative fix than the PR s/agent-gate-failed AI could not verify tests catch the bug s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review)

Projects

None yet

4 participants