You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Consolidates three findings on one file, UtilitiesCS/Threading/UiThread.cs, that were filed separately as #784, #787, and #788 after the #781 and #782 reviews. (1) Init() accepts a non-STA caller and installs that worker's non-pumping dispatcher and context into set-once process-global state (#787). (2) Init() consumes its single-shot latch before Initialize() runs, so a failed first attempt can never be retried, and the naive re-arm was measured to regress in #782 (#788). (3) SynchronizationContextAwaiter.IsCompleted compares contexts by reference, so any context captured inside a WPF dispatcher operation always posts instead of continuing inline on the UI thread (#784). All three touch the same initialization and awaiter code and should ship as one change with one test suite.
Environment
OS/version: Windows 11 Pro 10.0.26200
Runtime: .NET Framework 4.8 VSTO add-in hosted by Outlook desktop; main at 04a54e68
Data source or fixture: QuickFiler.Test/Controllers/QfcHomeControllerRunAsyncTests.cs:329 (MTA caller of UiThread.Init(false))
Steps to Reproduce
Bug: uithread-init-accepts-non-sta-callers #787: call UiThread.Init(false) from an MTA thread (the in-repo instance is the test at QfcHomeControllerRunAsyncTests.cs:329). It returns normally and every later UiThread.Dispatcher / UiSyncContext / UiThreadId read marshals onto a thread with no message loop.
Bug: uithread-init-latch-not-rearmed-after-failed-initialize #788: arrange for Initialize() to throw (headless or non-STA), call Init(), fix the condition, call Init() again. The second call is a no-op because _loaded.CheckAndSetFirstCall at UiThread.cs:36 was consumed before Initialize() ran.
Bug: uithread-synccontext-awaiter-always-posts-for-dispatcher-built-viewers #784: construct an ItemViewer through ItemViewerQueue.Dequeue (inside UiThread.Dispatcher.Invoke, so UiSyncContext is a DispatcherSynchronizationContext), then on the UI thread evaluate viewer.UiSyncContext.GetAwaiter().IsCompleted. It is false, so the continuation posts instead of running inline.
Expected Behavior
Init() rejects a non-STA caller with a named InvalidOperationException before capturing anything.
IsCompleted is true when the caller already runs on the owning UI thread, regardless of which SynchronizationContext instance is ambient.
Actual Behavior
See the three reproduction steps. #787 succeeds silently and poisons the globals for the process lifetime; #788 leaves UiThread.Dispatcher throwing an exception that names Init() as the remedy while Init() is a no-op; #784 adds one queued hop per await and changes ordering relative to already-queued UI work.
Medium, carried from #787: in production ThisAddIn.cs:35-40 is the only Init() caller and runs on the Outlook STA, so the hazards are reachable today only from test code, but a worker-thread read of the lazy accessors before startup completes would poison the process. #784 and #788 are Low individually.
Summary
Consolidates three findings on one file,
UtilitiesCS/Threading/UiThread.cs, that were filed separately as #784, #787, and #788 after the #781 and #782 reviews. (1)Init()accepts a non-STA caller and installs that worker's non-pumping dispatcher and context into set-once process-global state (#787). (2)Init()consumes its single-shot latch beforeInitialize()runs, so a failed first attempt can never be retried, and the naive re-arm was measured to regress in #782 (#788). (3)SynchronizationContextAwaiter.IsCompletedcompares contexts by reference, so any context captured inside a WPF dispatcher operation always posts instead of continuing inline on the UI thread (#784). All three touch the same initialization and awaiter code and should ship as one change with one test suite.Environment
mainat04a54e68vstest.console.exe <test assemblies> /InIsolation; runtime probe in the Bug: breadcrumb-ui-boundary-guard-rejects-dispatcher-built-viewers #781 feature folder (evidence/other/dispatcher-synccontext-probe.2026-09-05T10-40.md)QuickFiler.Test/Controllers/QfcHomeControllerRunAsyncTests.cs:329(MTA caller ofUiThread.Init(false))Steps to Reproduce
UiThread.Init(false)from an MTA thread (the in-repo instance is the test atQfcHomeControllerRunAsyncTests.cs:329). It returns normally and every laterUiThread.Dispatcher/UiSyncContext/UiThreadIdread marshals onto a thread with no message loop.Initialize()to throw (headless or non-STA), callInit(), fix the condition, callInit()again. The second call is a no-op because_loaded.CheckAndSetFirstCallatUiThread.cs:36was consumed beforeInitialize()ran.ItemViewerthroughItemViewerQueue.Dequeue(insideUiThread.Dispatcher.Invoke, soUiSyncContextis aDispatcherSynchronizationContext), then on the UI thread evaluateviewer.UiSyncContext.GetAwaiter().IsCompleted. It isfalse, so the continuation posts instead of running inline.Expected Behavior
Init()rejects a non-STA caller with a namedInvalidOperationExceptionbefore capturing anything.Initialize()leaves the latch re-armed so a laterInit()retries, without reintroducing the regression Refactor: pr-778-post-merge-review-residuals #782 measured.IsCompletedis true when the caller already runs on the owning UI thread, regardless of whichSynchronizationContextinstance is ambient.Actual Behavior
See the three reproduction steps. #787 succeeds silently and poisons the globals for the process lifetime; #788 leaves
UiThread.Dispatcherthrowing an exception that namesInit()as the remedy whileInit()is a no-op; #784 adds one queued hop per await and changes ordering relative to already-queued UI work.Logs / Screenshots
UtilitiesCS/Threading/UiThread.csline 100 (verified 2026-09-05):public bool IsCompleted => _context == SynchronizationContext.Current;(reference comparison). Probe result:Invoke ctx == outer ambient : Falseon .NET Framework 4.8 STA. Bug: uithread-init-accepts-non-sta-callers #787 and Bug: uithread-init-latch-not-rearmed-after-failed-initialize #788 are missing-precondition and ordering defects with no diagnostic output.Impact / Severity
Medium, carried from #787: in production
ThisAddIn.cs:35-40is the onlyInit()caller and runs on the Outlook STA, so the hazards are reachable today only from test code, but a worker-thread read of the lazy accessors before startup completes would poison the process. #784 and #788 are Low individually.Source
From: docs/features/potential/2026-09-07-uithread-init-contract-residuals-784-787-788.md