From 43ab45a87691cec0a2a04670bdfe9b54d80c5b62 Mon Sep 17 00:00:00 2001 From: Brant Burnett Date: Fri, 24 Oct 2025 09:51:49 -0400 Subject: [PATCH 1/2] Provide visibility to running synchronously Motivation ---------- There are cases in complex systems where it is helpful for code to know that it is running within a call to AsyncHelper.RunSync. Furthermore, retrieving the previously replaced synchronization context can also be helpful. For example, a call to Form.ShowDialog in a WinForms app may want to reinstall the WindowsFormsSynchronizationContext since a new message pump will be running to process continuations. Modifications ------------- - Add IsRunningSynchronously property - Add GetReplacedSynchronizationContext method - Track the fact we're in the call to RunSync and processing work on its calling thread using a thread static --- .../AsyncHelperTests.cs | 2034 +++++++++-------- .../CenterEdge.Async.UnitTests.csproj | 2 +- src/CenterEdge.Async/AsyncHelper.cs | 90 + .../ExclusiveSynchronizationContext.cs | 6 +- 4 files changed, 1218 insertions(+), 914 deletions(-) diff --git a/src/CenterEdge.Async.UnitTests/AsyncHelperTests.cs b/src/CenterEdge.Async.UnitTests/AsyncHelperTests.cs index 7ea5b98..19c520e 100644 --- a/src/CenterEdge.Async.UnitTests/AsyncHelperTests.cs +++ b/src/CenterEdge.Async.UnitTests/AsyncHelperTests.cs @@ -8,1285 +8,1497 @@ // that will interfere with test parallelization. #pragma warning disable xUnit1030 -namespace CenterEdge.Async.UnitTests +namespace CenterEdge.Async.UnitTests; + +public class AsyncHelperTests { - public class AsyncHelperTests + #region IsRunningSynchronously + + [Fact] + public void IsRunningSynchronously_NoContext_False() + { + // Act + + var result = AsyncHelper.IsRunningSynchronously; + + // Assert + + Assert.False(result); + } + + [Fact] + public void IsRunningSynchronously_NoContextRunSync_TrueBeforeAwait() + { + // Act + + AsyncHelper.RunSync(async () => + { + Assert.True(AsyncHelper.IsRunningSynchronously); + await Task.Delay(10); + Assert.False(AsyncHelper.IsRunningSynchronously); + }); + } + + [Fact] + public void IsRunningSynchronously_FakeContext_False() + { + // Arrange + + using var _ = FakeSynchronizationContext.Install(); + + // Act + + var result = AsyncHelper.IsRunningSynchronously; + + // Assert + + Assert.False(result); + } + + [Fact] + public void IsRunningSynchronously_InSyncContext_True() + { + // Arrange + + using var _ = FakeSynchronizationContext.Install(); + + // Act + + AsyncHelper.RunSync(async () => + { + Assert.True(AsyncHelper.IsRunningSynchronously); + await Task.Delay(10); + Assert.True(AsyncHelper.IsRunningSynchronously); + }); + } + + [Fact] + public void IsRunningSynchronously_OnThreadPool_False() { - #region RunSync_Task + // Arrange + + using var _ = FakeSynchronizationContext.Install(); + + // Act - [Fact] - public void RunSync_Task_DoesAllTasks() + AsyncHelper.RunSync(async () => + { + Assert.True(AsyncHelper.IsRunningSynchronously); + await Task.Delay(10).ConfigureAwait(false); + Assert.False(AsyncHelper.IsRunningSynchronously); + }); + } + + #endregion + + #region GetReplacedSynchronizationContext + + [Fact] + public void GetReplacedSynchronizationContext_NoContext_Null() + { + // Act + + var result = AsyncHelper.GetReplacedSynchronizationContext(); + + // Assert + + Assert.Null(result); + } + + [Fact] + public void GetReplacedSynchronizationContext_DefaultContextRunSync_Null() + { + var previousSyncContext = SynchronizationContext.Current; + try { // Arrange - var i = 0; + var newContext = new SynchronizationContext(); + SynchronizationContext.SetSynchronizationContext(newContext); // Act + AsyncHelper.RunSync(async () => { - i += 1; + Assert.Null(AsyncHelper.GetReplacedSynchronizationContext()); await Task.Delay(10); - i += 1; - await Task.Delay(10); - i += 1; + Assert.Null(AsyncHelper.GetReplacedSynchronizationContext()); }); + } + finally + { + SynchronizationContext.SetSynchronizationContext(previousSyncContext); + } + } - // Assert + [Fact] + public void GetReplacedSynchronizationContext_FakeContext_Null() + { + // Arrange - Assert.Equal(3, i); - } + using var _ = FakeSynchronizationContext.Install(); - [Fact] - public async Task RunSync_StartsTasksAndCompletesSynchronously_DoesAllTasks() - { - // Replicates the case where continuations are queued but the main task completes synchronously - // so the work must be removed from the queue + // Act - // Arrange + var result = AsyncHelper.GetReplacedSynchronizationContext(); - var i = 0; + // Assert - async Task IncrementAsync() - { - await Task.Yield(); - Interlocked.Increment(ref i); - } + Assert.Null(result); + } - // Act - AsyncHelper.RunSync(() => - { -#pragma warning disable CS4014 - for (var j = 0; j < 3; j++) - { - var _ = IncrementAsync(); - } -#pragma warning restore CS4014 + [Fact] + public void GetReplacedSynchronizationContext_InSyncContext_GetsParent() + { + // Arrange - return Task.CompletedTask; - }); + using var _ = FakeSynchronizationContext.Install(); - // Assert + // Act - await Task.Delay(500, TestContext.Current.CancellationToken); - Assert.Equal(3, i); - } + AsyncHelper.RunSync(async () => + { + Assert.IsType(AsyncHelper.GetReplacedSynchronizationContext()); + await Task.Delay(10); + Assert.IsType(AsyncHelper.GetReplacedSynchronizationContext()); + }); + } + + [Fact] + public void GetReplacedSynchronizationContext_OnThreadPool_Null() + { + // Arrange + + using var _ = FakeSynchronizationContext.Install(); + + // Act - [Fact] - public void RunSync_Task_ConfigureAwaitFalse_DoesAllTasks() + AsyncHelper.RunSync(async () => { - // Arrange + Assert.IsType(AsyncHelper.GetReplacedSynchronizationContext()); + await Task.Delay(10).ConfigureAwait(false); + Assert.Null(AsyncHelper.GetReplacedSynchronizationContext()); + }); + } - var i = 0; + [Fact] + public void GetReplacedSynchronizationContext_DoubleNested_Recurses() + { + // Arrange + + using var _ = FakeSynchronizationContext.Install(); + + // Act + + AsyncHelper.RunSync(async () => + { + Assert.IsType(AsyncHelper.GetReplacedSynchronizationContext()); + await Task.Delay(10); - // Act AsyncHelper.RunSync(async () => { - i += 1; - await Task.Delay(10).ConfigureAwait(false); - i += 1; - await Task.Delay(10).ConfigureAwait(false); - i += 1; + Assert.IsType(AsyncHelper.GetReplacedSynchronizationContext()); }); + }); + } - // Assert + #endregion - Assert.Equal(3, i); - } + #region RunSync_Task + + [Fact] + public void RunSync_Task_DoesAllTasks() + { + // Arrange + + var i = 0; - [Fact] - public void RunSync_Task_ExceptionAfterAwait_ThrowsException() + // Act + AsyncHelper.RunSync(async () => { - // Act/Assert - Assert.Throws(() => - AsyncHelper.RunSync(async () => - { - await Task.Delay(10); + i += 1; + await Task.Delay(10); + i += 1; + await Task.Delay(10); + i += 1; + }); - throw new InvalidOperationException(); - })); - } + // Assert + + Assert.Equal(3, i); + } - [Fact] - public void RunSync_Task_ExceptionBeforeAwait_ThrowsException() + [Fact] + public async Task RunSync_StartsTasksAndCompletesSynchronously_DoesAllTasks() + { + // Replicates the case where continuations are queued but the main task completes synchronously + // so the work must be removed from the queue + + // Arrange + + var i = 0; + + async Task IncrementAsync() { - // Act/Assert - Assert.Throws(() => - AsyncHelper.RunSync(() => throw new InvalidOperationException())); + await Task.Yield(); + Interlocked.Increment(ref i); } - [Fact] - public void RunSync_Task_ThrowsException_ResetsSyncContext() + // Act + AsyncHelper.RunSync(() => { - // Arrange - - var sync = new SynchronizationContext(); - SynchronizationContext.SetSynchronizationContext(sync); - - // Act - try - { - AsyncHelper.RunSync(() => throw new InvalidOperationException()); - } - catch (InvalidOperationException) +#pragma warning disable CS4014 + for (var j = 0; j < 3; j++) { - // Expected + var _ = IncrementAsync(); } +#pragma warning restore CS4014 - // Assert + return Task.CompletedTask; + }); - Assert.Equal(sync, SynchronizationContext.Current); - } + // Assert + + await Task.Delay(500, TestContext.Current.CancellationToken); + Assert.Equal(3, i); + } - [Fact] - public void RunSync_Task_DanglingContinuations_HandledOnParentSyncContext() + [Fact] + public void RunSync_Task_ConfigureAwaitFalse_DoesAllTasks() + { + // Arrange + + var i = 0; + + // Act + AsyncHelper.RunSync(async () => { - // Arrange + i += 1; + await Task.Delay(10).ConfigureAwait(false); + i += 1; + await Task.Delay(10).ConfigureAwait(false); + i += 1; + }); - var mockSync = new Mock { CallBase = true }; - SynchronizationContext.SetSynchronizationContext(mockSync.Object); + // Assert - var called = false; + Assert.Equal(3, i); + } - // Act + [Fact] + public void RunSync_Task_ExceptionAfterAwait_ThrowsException() + { + // Act/Assert + Assert.Throws(() => AsyncHelper.RunSync(async () => { - await Task.Yield(); - -#pragma warning disable 4014 - DelayedActionAsync(TimeSpan.FromMilliseconds(400), () => called = true); -#pragma warning restore 4014 - }); + await Task.Delay(10); - // Assert + throw new InvalidOperationException(); + })); + } - Assert.False(called); + [Fact] + public void RunSync_Task_ExceptionBeforeAwait_ThrowsException() + { + // Act/Assert + Assert.Throws(() => + AsyncHelper.RunSync(() => throw new InvalidOperationException())); + } - Thread.Sleep(500); + [Fact] + public void RunSync_Task_ThrowsException_ResetsSyncContext() + { + // Arrange - Assert.True(called); + var sync = new SynchronizationContext(); + SynchronizationContext.SetSynchronizationContext(sync); - mockSync.Verify( - m => m.Post(It.IsAny(), It.IsAny()), - Times.Once); + // Act + try + { + AsyncHelper.RunSync(() => throw new InvalidOperationException()); + } + catch (InvalidOperationException) + { + // Expected } - #endregion + // Assert - #region RunSyncWithState_Task + Assert.Equal(sync, SynchronizationContext.Current); + } - [Fact] - public void RunSyncWithState_Task_DoesAllTasks() + [Fact] + public void RunSync_Task_DanglingContinuations_HandledOnParentSyncContext() + { + // Arrange + + var mockSync = new Mock { CallBase = true }; + SynchronizationContext.SetSynchronizationContext(mockSync.Object); + + var called = false; + + // Act + AsyncHelper.RunSync(async () => { - // Arrange + await Task.Yield(); - var i = 0; +#pragma warning disable 4014 + DelayedActionAsync(TimeSpan.FromMilliseconds(400), () => called = true); +#pragma warning restore 4014 + }); - // Act - AsyncHelper.RunSync(async _ => - { - i += 1; - await Task.Delay(10); - i += 1; - await Task.Delay(10); - i += 1; - }, 1); + // Assert - // Assert + Assert.False(called); - Assert.Equal(3, i); - } + Thread.Sleep(500); + + Assert.True(called); + + mockSync.Verify( + m => m.Post(It.IsAny(), It.IsAny()), + Times.Once); + } + + #endregion + + #region RunSyncWithState_Task + + [Fact] + public void RunSyncWithState_Task_DoesAllTasks() + { + // Arrange - [Fact] - public async Task RunSyncWithState_StartsTasksAndCompletesSynchronously_DoesAllTasks() + var i = 0; + + // Act + AsyncHelper.RunSync(async _ => { - // Replicates the case where continuations are queued but the main task completes synchronously - // so the work must be removed from the queue + i += 1; + await Task.Delay(10); + i += 1; + await Task.Delay(10); + i += 1; + }, 1); - // Arrange + // Assert - var i = 0; + Assert.Equal(3, i); + } - async Task IncrementAsync() - { - await Task.Yield(); - Interlocked.Increment(ref i); - } + [Fact] + public async Task RunSyncWithState_StartsTasksAndCompletesSynchronously_DoesAllTasks() + { + // Replicates the case where continuations are queued but the main task completes synchronously + // so the work must be removed from the queue - // Act - AsyncHelper.RunSync(state => - { + // Arrange + + var i = 0; + + async Task IncrementAsync() + { + await Task.Yield(); + Interlocked.Increment(ref i); + } + + // Act + AsyncHelper.RunSync(state => + { #pragma warning disable CS4014 - for (var j = 0; j < 3; j++) - { - var _ = IncrementAsync(); - } + for (var j = 0; j < 3; j++) + { + var _ = IncrementAsync(); + } #pragma warning restore CS4014 - return Task.CompletedTask; - }, 1); + return Task.CompletedTask; + }, 1); - // Assert + // Assert - await Task.Delay(500, TestContext.Current.CancellationToken); - Assert.Equal(3, i); - } + await Task.Delay(500, TestContext.Current.CancellationToken); + Assert.Equal(3, i); + } + + [Fact] + public void RunSyncWithState_Task_ConfigureAwaitFalse_DoesAllTasks() + { + // Arrange + + var i = 0; - [Fact] - public void RunSyncWithState_Task_ConfigureAwaitFalse_DoesAllTasks() + // Act + AsyncHelper.RunSync(async _ => { - // Arrange + i += 1; + await Task.Delay(10).ConfigureAwait(false); + i += 1; + await Task.Delay(10).ConfigureAwait(false); + i += 1; + }, 1); - var i = 0; + // Assert - // Act + Assert.Equal(3, i); + } + + [Fact] + public void RunSyncWithState_Task_ExceptionAfterAwait_ThrowsException() + { + // Act/Assert + Assert.Throws(() => AsyncHelper.RunSync(async _ => { - i += 1; - await Task.Delay(10).ConfigureAwait(false); - i += 1; - await Task.Delay(10).ConfigureAwait(false); - i += 1; - }, 1); + await Task.Delay(10); - // Assert + throw new InvalidOperationException(); + }, 1)); + } - Assert.Equal(3, i); - } + [Fact] + public void RunSyncWithState_Task_ExceptionBeforeAwait_ThrowsException() + { + // Act/Assert + Assert.Throws(() => + AsyncHelper.RunSync(_ => throw new InvalidOperationException(), 1)); + } - [Fact] - public void RunSyncWithState_Task_ExceptionAfterAwait_ThrowsException() - { - // Act/Assert - Assert.Throws(() => - AsyncHelper.RunSync(async _ => - { - await Task.Delay(10); + [Fact] + public void RunSyncWithState_Task_ThrowsException_ResetsSyncContext() + { + // Arrange - throw new InvalidOperationException(); - }, 1)); - } + var sync = new SynchronizationContext(); + SynchronizationContext.SetSynchronizationContext(sync); - [Fact] - public void RunSyncWithState_Task_ExceptionBeforeAwait_ThrowsException() + // Act + try { - // Act/Assert - Assert.Throws(() => - AsyncHelper.RunSync(_ => throw new InvalidOperationException(), 1)); + AsyncHelper.RunSync(_ => throw new InvalidOperationException(), 1); } - - [Fact] - public void RunSyncWithState_Task_ThrowsException_ResetsSyncContext() + catch (InvalidOperationException) { - // Arrange - - var sync = new SynchronizationContext(); - SynchronizationContext.SetSynchronizationContext(sync); - - // Act - try - { - AsyncHelper.RunSync(_ => throw new InvalidOperationException(), 1); - } - catch (InvalidOperationException) - { - // Expected - } + // Expected + } - // Assert + // Assert - Assert.Equal(sync, SynchronizationContext.Current); - } + Assert.Equal(sync, SynchronizationContext.Current); + } - [Fact] - public void RunSyncWithState_Task_DanglingContinuations_HandledOnParentSyncContext() - { - // Arrange + [Fact] + public void RunSyncWithState_Task_DanglingContinuations_HandledOnParentSyncContext() + { + // Arrange - var mockSync = new Mock { CallBase = true }; - SynchronizationContext.SetSynchronizationContext(mockSync.Object); + var mockSync = new Mock { CallBase = true }; + SynchronizationContext.SetSynchronizationContext(mockSync.Object); - var called = false; + var called = false; - // Act - AsyncHelper.RunSync(async _ => - { - await Task.Yield(); + // Act + AsyncHelper.RunSync(async _ => + { + await Task.Yield(); #pragma warning disable 4014 - DelayedActionAsync(TimeSpan.FromMilliseconds(400), () => called = true); + DelayedActionAsync(TimeSpan.FromMilliseconds(400), () => called = true); #pragma warning restore 4014 - }, 1); + }, 1); - // Assert + // Assert - Assert.False(called); + Assert.False(called); - Thread.Sleep(500); + Thread.Sleep(500); - Assert.True(called); + Assert.True(called); - mockSync.Verify( - m => m.Post(It.IsAny(), It.IsAny()), - Times.Once); - } + mockSync.Verify( + m => m.Post(It.IsAny(), It.IsAny()), + Times.Once); + } - #endregion + #endregion - #region RunSync_ValueTask + #region RunSync_ValueTask - [Fact] - public void RunSync_ValueTask_DoesAllTasks() - { - // Arrange + [Fact] + public void RunSync_ValueTask_DoesAllTasks() + { + // Arrange - var i = 0; + var i = 0; - // Act - AsyncHelper.RunSync(async ValueTask () => - { - i += 1; - await Task.Delay(10); - i += 1; - await Task.Delay(10); - i += 1; - }); + // Act + AsyncHelper.RunSync(async ValueTask () => + { + i += 1; + await Task.Delay(10); + i += 1; + await Task.Delay(10); + i += 1; + }); - // Assert + // Assert - Assert.Equal(3, i); - } + Assert.Equal(3, i); + } - [Fact] - public async Task RunSync_ValueTask_StartsTasksAndCompletesSynchronously_DoesAllTasks() - { - // Replicates the case where continuations are queued but the main task completes synchronously - // so the work must be removed from the queue + [Fact] + public async Task RunSync_ValueTask_StartsTasksAndCompletesSynchronously_DoesAllTasks() + { + // Replicates the case where continuations are queued but the main task completes synchronously + // so the work must be removed from the queue - // Arrange + // Arrange - var i = 0; + var i = 0; - async Task IncrementAsync() - { - await Task.Yield(); - Interlocked.Increment(ref i); - } + async Task IncrementAsync() + { + await Task.Yield(); + Interlocked.Increment(ref i); + } - // Act - AsyncHelper.RunSync(() => - { + // Act + AsyncHelper.RunSync(() => + { #pragma warning disable CS4014 - for (var j = 0; j < 3; j++) - { - var _ = IncrementAsync(); - } + for (var j = 0; j < 3; j++) + { + var _ = IncrementAsync(); + } #pragma warning restore CS4014 - return new ValueTask(); - }); + return new ValueTask(); + }); - // Assert + // Assert - await Task.Delay(500, TestContext.Current.CancellationToken); - Assert.Equal(3, i); - } + await Task.Delay(500, TestContext.Current.CancellationToken); + Assert.Equal(3, i); + } + + [Fact] + public void RunSync_ValueTask_ConfigureAwaitFalse_DoesAllTasks() + { + // Arrange - [Fact] - public void RunSync_ValueTask_ConfigureAwaitFalse_DoesAllTasks() + var i = 0; + + // Act + AsyncHelper.RunSync(async ValueTask () => { - // Arrange + i += 1; + await Task.Delay(10).ConfigureAwait(false); + i += 1; + await Task.Delay(10).ConfigureAwait(false); + i += 1; + }); - var i = 0; + // Assert - // Act + Assert.Equal(3, i); + } + + [Fact] + public void RunSync_ValueTask_ExceptionAfterAwait_ThrowsException() + { + // Act/Assert + Assert.Throws(() => AsyncHelper.RunSync(async ValueTask () => { - i += 1; - await Task.Delay(10).ConfigureAwait(false); - i += 1; - await Task.Delay(10).ConfigureAwait(false); - i += 1; - }); + await Task.Delay(10); - // Assert + throw new InvalidOperationException(); + })); + } - Assert.Equal(3, i); - } + [Fact] + public void RunSync_ValueTask_ExceptionBeforeAwait_ThrowsException() + { + // Act/Assert + Assert.Throws(() => + AsyncHelper.RunSync(ValueTask () => throw new InvalidOperationException())); + } - [Fact] - public void RunSync_ValueTask_ExceptionAfterAwait_ThrowsException() - { - // Act/Assert - Assert.Throws(() => - AsyncHelper.RunSync(async ValueTask () => - { - await Task.Delay(10); + [Fact] + public void RunSync_ValueTask_ThrowsException_ResetsSyncContext() + { + // Arrange - throw new InvalidOperationException(); - })); - } + var sync = new SynchronizationContext(); + SynchronizationContext.SetSynchronizationContext(sync); - [Fact] - public void RunSync_ValueTask_ExceptionBeforeAwait_ThrowsException() + // Act + try { - // Act/Assert - Assert.Throws(() => - AsyncHelper.RunSync(ValueTask () => throw new InvalidOperationException())); + AsyncHelper.RunSync(ValueTask () => throw new InvalidOperationException()); } - - [Fact] - public void RunSync_ValueTask_ThrowsException_ResetsSyncContext() + catch (InvalidOperationException) { - // Arrange - - var sync = new SynchronizationContext(); - SynchronizationContext.SetSynchronizationContext(sync); - - // Act - try - { - AsyncHelper.RunSync(ValueTask () => throw new InvalidOperationException()); - } - catch (InvalidOperationException) - { - // Expected - } + // Expected + } - // Assert + // Assert - Assert.Equal(sync, SynchronizationContext.Current); - } + Assert.Equal(sync, SynchronizationContext.Current); + } - [Fact] - public void RunSync_ValueTask_DanglingContinuations_HandledOnParentSyncContext() - { - // Arrange + [Fact] + public void RunSync_ValueTask_DanglingContinuations_HandledOnParentSyncContext() + { + // Arrange - var mockSync = new Mock { CallBase = true }; - SynchronizationContext.SetSynchronizationContext(mockSync.Object); + var mockSync = new Mock { CallBase = true }; + SynchronizationContext.SetSynchronizationContext(mockSync.Object); - var called = false; + var called = false; - // Act - AsyncHelper.RunSync(async ValueTask () => - { - await Task.Yield(); + // Act + AsyncHelper.RunSync(async ValueTask () => + { + await Task.Yield(); #pragma warning disable 4014 - DelayedActionAsync(TimeSpan.FromMilliseconds(400), () => called = true); + DelayedActionAsync(TimeSpan.FromMilliseconds(400), () => called = true); #pragma warning restore 4014 - }); + }); - // Assert + // Assert - Assert.False(called); + Assert.False(called); - Thread.Sleep(500); + Thread.Sleep(500); - Assert.True(called); + Assert.True(called); - mockSync.Verify( - m => m.Post(It.IsAny(), It.IsAny()), - Times.Once); - } + mockSync.Verify( + m => m.Post(It.IsAny(), It.IsAny()), + Times.Once); + } - #endregion + #endregion - #region RunSyncWithState_ValueTask + #region RunSyncWithState_ValueTask - [Fact] - public void RunSyncWithState_ValueTask_DoesAllTasks() - { - // Arrange + [Fact] + public void RunSyncWithState_ValueTask_DoesAllTasks() + { + // Arrange - var i = 0; + var i = 0; - // Act - AsyncHelper.RunSync(async ValueTask (state) => - { - i += 1; - await Task.Delay(10); - i += 1; - await Task.Delay(10); - i += 1; - }, 1); + // Act + AsyncHelper.RunSync(async ValueTask (state) => + { + i += 1; + await Task.Delay(10); + i += 1; + await Task.Delay(10); + i += 1; + }, 1); - // Assert + // Assert - Assert.Equal(3, i); - } + Assert.Equal(3, i); + } - [Fact] - public async Task RunSyncWithState_ValueTask_StartsTasksAndCompletesSynchronously_DoesAllTasks() - { - // Replicates the case where continuations are queued but the main task completes synchronously - // so the work must be removed from the queue + [Fact] + public async Task RunSyncWithState_ValueTask_StartsTasksAndCompletesSynchronously_DoesAllTasks() + { + // Replicates the case where continuations are queued but the main task completes synchronously + // so the work must be removed from the queue - // Arrange + // Arrange - var i = 0; + var i = 0; - async Task IncrementAsync() - { - await Task.Yield(); - Interlocked.Increment(ref i); - } + async Task IncrementAsync() + { + await Task.Yield(); + Interlocked.Increment(ref i); + } - // Act - AsyncHelper.RunSync(state => - { + // Act + AsyncHelper.RunSync(state => + { #pragma warning disable CS4014 - for (var j = 0; j < 3; j++) - { - var _ = IncrementAsync(); - } + for (var j = 0; j < 3; j++) + { + var _ = IncrementAsync(); + } #pragma warning restore CS4014 - return new ValueTask(); - }, 1); + return new ValueTask(); + }, 1); - // Assert + // Assert - await Task.Delay(500, TestContext.Current.CancellationToken); - Assert.Equal(3, i); - } + await Task.Delay(500, TestContext.Current.CancellationToken); + Assert.Equal(3, i); + } + + [Fact] + public void RunSyncWithState_ValueTask_ConfigureAwaitFalse_DoesAllTasks() + { + // Arrange - [Fact] - public void RunSyncWithState_ValueTask_ConfigureAwaitFalse_DoesAllTasks() + var i = 0; + + // Act + AsyncHelper.RunSync(async ValueTask (state) => { - // Arrange + i += 1; + await Task.Delay(10).ConfigureAwait(false); + i += 1; + await Task.Delay(10).ConfigureAwait(false); + i += 1; + }, 1); - var i = 0; + // Assert - // Act + Assert.Equal(3, i); + } + + [Fact] + public void RunSyncWithState_ValueTask_ExceptionAfterAwait_ThrowsException() + { + // Act/Assert + Assert.Throws(() => AsyncHelper.RunSync(async ValueTask (state) => { - i += 1; - await Task.Delay(10).ConfigureAwait(false); - i += 1; - await Task.Delay(10).ConfigureAwait(false); - i += 1; - }, 1); + await Task.Delay(10); - // Assert + throw new InvalidOperationException(); + }, 1)); + } - Assert.Equal(3, i); - } + [Fact] + public void RunSyncWithState_ValueTask_ExceptionBeforeAwait_ThrowsException() + { + // Act/Assert + Assert.Throws(() => + AsyncHelper.RunSync(ValueTask (_) => throw new InvalidOperationException(), 1)); + } - [Fact] - public void RunSyncWithState_ValueTask_ExceptionAfterAwait_ThrowsException() - { - // Act/Assert - Assert.Throws(() => - AsyncHelper.RunSync(async ValueTask (state) => - { - await Task.Delay(10); + [Fact] + public void RunSyncWithState_ValueTask_ThrowsException_ResetsSyncContext() + { + // Arrange - throw new InvalidOperationException(); - }, 1)); - } + var sync = new SynchronizationContext(); + SynchronizationContext.SetSynchronizationContext(sync); - [Fact] - public void RunSyncWithState_ValueTask_ExceptionBeforeAwait_ThrowsException() + // Act + try { - // Act/Assert - Assert.Throws(() => - AsyncHelper.RunSync(ValueTask (_) => throw new InvalidOperationException(), 1)); + AsyncHelper.RunSync(ValueTask (_) => throw new InvalidOperationException(), 1); } - - [Fact] - public void RunSyncWithState_ValueTask_ThrowsException_ResetsSyncContext() + catch (InvalidOperationException) { - // Arrange - - var sync = new SynchronizationContext(); - SynchronizationContext.SetSynchronizationContext(sync); - - // Act - try - { - AsyncHelper.RunSync(ValueTask (_) => throw new InvalidOperationException(), 1); - } - catch (InvalidOperationException) - { - // Expected - } + // Expected + } - // Assert + // Assert - Assert.Equal(sync, SynchronizationContext.Current); - } + Assert.Equal(sync, SynchronizationContext.Current); + } - [Fact] - public void RunSyncWithState_ValueTask_DanglingContinuations_HandledOnParentSyncContext() - { - // Arrange + [Fact] + public void RunSyncWithState_ValueTask_DanglingContinuations_HandledOnParentSyncContext() + { + // Arrange - var mockSync = new Mock { CallBase = true }; - SynchronizationContext.SetSynchronizationContext(mockSync.Object); + var mockSync = new Mock { CallBase = true }; + SynchronizationContext.SetSynchronizationContext(mockSync.Object); - var called = false; + var called = false; - // Act - AsyncHelper.RunSync(async ValueTask (state) => - { - await Task.Yield(); + // Act + AsyncHelper.RunSync(async ValueTask (state) => + { + await Task.Yield(); #pragma warning disable 4014 - DelayedActionAsync(TimeSpan.FromMilliseconds(400), () => called = true); + DelayedActionAsync(TimeSpan.FromMilliseconds(400), () => called = true); #pragma warning restore 4014 - }, 1); + }, 1); - // Assert + // Assert - Assert.False(called); + Assert.False(called); - Thread.Sleep(500); + Thread.Sleep(500); - Assert.True(called); + Assert.True(called); - mockSync.Verify( - m => m.Post(It.IsAny(), It.IsAny()), - Times.Once); - } + mockSync.Verify( + m => m.Post(It.IsAny(), It.IsAny()), + Times.Once); + } - #endregion + #endregion - #region RunSync_TaskT + #region RunSync_TaskT - [Fact] - public void RunSync_TaskT_DoesAllTasks() + [Fact] + public void RunSync_TaskT_DoesAllTasks() + { + // Act + var result = AsyncHelper.RunSync(async () => { - // Act - var result = AsyncHelper.RunSync(async () => - { - var i = 1; - await Task.Delay(10); - i += 1; - await Task.Delay(10); - i += 1; - return i; - }); + var i = 1; + await Task.Delay(10); + i += 1; + await Task.Delay(10); + i += 1; + return i; + }); - // Assert + // Assert - Assert.Equal(3, result); - } + Assert.Equal(3, result); + } - [Fact] - public async Task RunSync_TaskT_StartsTasksAndCompletesSynchronously_DoesAllTasks() - { - // Replicates the case where continuations are queued but the main task completes synchronously - // so the work must be removed from the queue + [Fact] + public async Task RunSync_TaskT_StartsTasksAndCompletesSynchronously_DoesAllTasks() + { + // Replicates the case where continuations are queued but the main task completes synchronously + // so the work must be removed from the queue - // Arrange + // Arrange - var i = 0; + var i = 0; - async Task IncrementAsync() - { - await Task.Yield(); - Interlocked.Increment(ref i); - } + async Task IncrementAsync() + { + await Task.Yield(); + Interlocked.Increment(ref i); + } - // Act - AsyncHelper.RunSync(() => - { + // Act + AsyncHelper.RunSync(() => + { #pragma warning disable CS4014 - for (var j = 0; j < 3; j++) - { - var _ = IncrementAsync(); - } + for (var j = 0; j < 3; j++) + { + var _ = IncrementAsync(); + } #pragma warning restore CS4014 - return Task.FromResult(true); - }); + return Task.FromResult(true); + }); - // Assert + // Assert - await Task.Delay(500, TestContext.Current.CancellationToken); - Assert.Equal(3, i); - } + await Task.Delay(500, TestContext.Current.CancellationToken); + Assert.Equal(3, i); + } - [Fact] - public void RunSync_TaskT_ConfigureAwaitFalse_DoesAllTasks() + [Fact] + public void RunSync_TaskT_ConfigureAwaitFalse_DoesAllTasks() + { + // Act + var result = AsyncHelper.RunSync(async () => { - // Act - var result = AsyncHelper.RunSync(async () => + var i = 1; + await Task.Delay(10).ConfigureAwait(false); + i += 1; + await Task.Delay(10).ConfigureAwait(false); + i += 1; + return i; + }); + + // Assert + + Assert.Equal(3, result); + } + + [Fact] + public void RunSync_TaskT_ExceptionAfterAwait_ThrowsException() + { + // Act/Assert + Assert.Throws(() => + AsyncHelper.RunSync(async Task () => { - var i = 1; - await Task.Delay(10).ConfigureAwait(false); - i += 1; - await Task.Delay(10).ConfigureAwait(false); - i += 1; - return i; - }); + await Task.Delay(10); - // Assert + throw new InvalidOperationException(); + })); + } - Assert.Equal(3, result); - } + [Fact] + public void RunSync_TaskT_ExceptionBeforeAwait_ThrowsException() + { + // Act/Assert + Assert.Throws(() => + AsyncHelper.RunSync(Task () => throw new InvalidOperationException())); + } - [Fact] - public void RunSync_TaskT_ExceptionAfterAwait_ThrowsException() - { - // Act/Assert - Assert.Throws(() => - AsyncHelper.RunSync(async Task () => - { - await Task.Delay(10); + [Fact] + public void RunSync_TaskT_ThrowsException_ResetsSyncContext() + { + // Arrange - throw new InvalidOperationException(); - })); - } + var sync = new SynchronizationContext(); + SynchronizationContext.SetSynchronizationContext(sync); - [Fact] - public void RunSync_TaskT_ExceptionBeforeAwait_ThrowsException() + // Act + try { - // Act/Assert - Assert.Throws(() => - AsyncHelper.RunSync(Task () => throw new InvalidOperationException())); + AsyncHelper.RunSync(Task () => throw new InvalidOperationException()); } - - [Fact] - public void RunSync_TaskT_ThrowsException_ResetsSyncContext() + catch (InvalidOperationException) { - // Arrange - - var sync = new SynchronizationContext(); - SynchronizationContext.SetSynchronizationContext(sync); - - // Act - try - { - AsyncHelper.RunSync(Task () => throw new InvalidOperationException()); - } - catch (InvalidOperationException) - { - // Expected - } + // Expected + } - // Assert + // Assert - Assert.Equal(sync, SynchronizationContext.Current); - } + Assert.Equal(sync, SynchronizationContext.Current); + } - [Fact] - public void RunSync_TaskT_DanglingContinuations_HandledOnParentSyncContext() - { - // Arrange + [Fact] + public void RunSync_TaskT_DanglingContinuations_HandledOnParentSyncContext() + { + // Arrange - var mockSync = new Mock { CallBase = true }; - SynchronizationContext.SetSynchronizationContext(mockSync.Object); + var mockSync = new Mock { CallBase = true }; + SynchronizationContext.SetSynchronizationContext(mockSync.Object); - var called = false; + var called = false; - // Act - AsyncHelper.RunSync(async () => - { - await Task.Yield(); + // Act + AsyncHelper.RunSync(async () => + { + await Task.Yield(); #pragma warning disable 4014 - DelayedActionAsync(TimeSpan.FromMilliseconds(400), () => called = true); + DelayedActionAsync(TimeSpan.FromMilliseconds(400), () => called = true); #pragma warning restore 4014 - return 0; - }); + return 0; + }); - // Assert + // Assert - Assert.False(called); + Assert.False(called); - Thread.Sleep(500); + Thread.Sleep(500); - Assert.True(called); + Assert.True(called); - mockSync.Verify( - m => m.Post(It.IsAny(), It.IsAny()), - Times.Once); - } + mockSync.Verify( + m => m.Post(It.IsAny(), It.IsAny()), + Times.Once); + } - #endregion + #endregion - #region RunSyncWithState_TaskT + #region RunSyncWithState_TaskT - [Fact] - public void RunSyncWithState_TaskT_DoesAllTasks() + [Fact] + public void RunSyncWithState_TaskT_DoesAllTasks() + { + // Act + var result = AsyncHelper.RunSync(async state => { - // Act - var result = AsyncHelper.RunSync(async state => - { - var i = 1; - await Task.Delay(10); - i += 1; - await Task.Delay(10); - i += 1; - return i; - }, 1); + var i = 1; + await Task.Delay(10); + i += 1; + await Task.Delay(10); + i += 1; + return i; + }, 1); - // Assert + // Assert - Assert.Equal(3, result); - } + Assert.Equal(3, result); + } - [Fact] - public async Task RunSyncWithState_TaskT_StartsTasksAndCompletesSynchronously_DoesAllTasks() - { - // Replicates the case where continuations are queued but the main task completes synchronously - // so the work must be removed from the queue + [Fact] + public async Task RunSyncWithState_TaskT_StartsTasksAndCompletesSynchronously_DoesAllTasks() + { + // Replicates the case where continuations are queued but the main task completes synchronously + // so the work must be removed from the queue - // Arrange + // Arrange - var i = 0; + var i = 0; - async Task IncrementAsync() - { - await Task.Yield(); - Interlocked.Increment(ref i); - } + async Task IncrementAsync() + { + await Task.Yield(); + Interlocked.Increment(ref i); + } - // Act - AsyncHelper.RunSync(state => - { + // Act + AsyncHelper.RunSync(state => + { #pragma warning disable CS4014 - for (var j = 0; j < 3; j++) - { - var _ = IncrementAsync(); - } + for (var j = 0; j < 3; j++) + { + var _ = IncrementAsync(); + } #pragma warning restore CS4014 - return Task.FromResult(true); - }, 1); + return Task.FromResult(true); + }, 1); - // Assert + // Assert - await Task.Delay(500, TestContext.Current.CancellationToken); - Assert.Equal(3, i); - } + await Task.Delay(500, TestContext.Current.CancellationToken); + Assert.Equal(3, i); + } - [Fact] - public void RunSyncWithState_TaskT_ConfigureAwaitFalse_DoesAllTasks() + [Fact] + public void RunSyncWithState_TaskT_ConfigureAwaitFalse_DoesAllTasks() + { + // Act + var result = AsyncHelper.RunSync(async state => { - // Act - var result = AsyncHelper.RunSync(async state => + var i = 1; + await Task.Delay(10).ConfigureAwait(false); + i += 1; + await Task.Delay(10).ConfigureAwait(false); + i += 1; + return i; + }, 1); + + // Assert + + Assert.Equal(3, result); + } + + [Fact] + public void RunSyncWithState_TaskT_ExceptionAfterAwait_ThrowsException() + { + // Act/Assert + Assert.Throws(() => + AsyncHelper.RunSync(async state => { - var i = 1; - await Task.Delay(10).ConfigureAwait(false); - i += 1; - await Task.Delay(10).ConfigureAwait(false); - i += 1; - return i; - }, 1); + await Task.Delay(10); - // Assert + throw new InvalidOperationException(); + }, 1)); + } - Assert.Equal(3, result); - } + [Fact] + public void RunSyncWithState_TaskT_ExceptionBeforeAwait_ThrowsException() + { + // Act/Assert + Assert.Throws(() => + AsyncHelper.RunSync(_ => throw new InvalidOperationException(), 1)); + } - [Fact] - public void RunSyncWithState_TaskT_ExceptionAfterAwait_ThrowsException() - { - // Act/Assert - Assert.Throws(() => - AsyncHelper.RunSync(async state => - { - await Task.Delay(10); + [Fact] + public void RunSyncWithState_TaskT_ThrowsException_ResetsSyncContext() + { + // Arrange - throw new InvalidOperationException(); - }, 1)); - } + var sync = new SynchronizationContext(); + SynchronizationContext.SetSynchronizationContext(sync); - [Fact] - public void RunSyncWithState_TaskT_ExceptionBeforeAwait_ThrowsException() + // Act + try { - // Act/Assert - Assert.Throws(() => - AsyncHelper.RunSync(_ => throw new InvalidOperationException(), 1)); + AsyncHelper.RunSync(_ => throw new InvalidOperationException(), 1); } - - [Fact] - public void RunSyncWithState_TaskT_ThrowsException_ResetsSyncContext() + catch (InvalidOperationException) { - // Arrange - - var sync = new SynchronizationContext(); - SynchronizationContext.SetSynchronizationContext(sync); - - // Act - try - { - AsyncHelper.RunSync(_ => throw new InvalidOperationException(), 1); - } - catch (InvalidOperationException) - { - // Expected - } + // Expected + } - // Assert + // Assert - Assert.Equal(sync, SynchronizationContext.Current); - } + Assert.Equal(sync, SynchronizationContext.Current); + } - [Fact] - public void RunSyncWithState_TaskT_DanglingContinuations_HandledOnParentSyncContext() - { - // Arrange + [Fact] + public void RunSyncWithState_TaskT_DanglingContinuations_HandledOnParentSyncContext() + { + // Arrange - var mockSync = new Mock { CallBase = true }; - SynchronizationContext.SetSynchronizationContext(mockSync.Object); + var mockSync = new Mock { CallBase = true }; + SynchronizationContext.SetSynchronizationContext(mockSync.Object); - var called = false; + var called = false; - // Act - AsyncHelper.RunSync(async state => - { - await Task.Yield(); + // Act + AsyncHelper.RunSync(async state => + { + await Task.Yield(); #pragma warning disable 4014 - DelayedActionAsync(TimeSpan.FromMilliseconds(400), () => called = true); + DelayedActionAsync(TimeSpan.FromMilliseconds(400), () => called = true); #pragma warning restore 4014 - return 0; - }, 1); + return 0; + }, 1); - // Assert + // Assert - Assert.False(called); + Assert.False(called); - Thread.Sleep(500); + Thread.Sleep(500); - Assert.True(called); + Assert.True(called); - mockSync.Verify( - m => m.Post(It.IsAny(), It.IsAny()), - Times.Once); - } + mockSync.Verify( + m => m.Post(It.IsAny(), It.IsAny()), + Times.Once); + } - #endregion + #endregion - #region RunSync_ValueTaskT + #region RunSync_ValueTaskT - [Fact] - public void RunSync_ValueTaskT_DoesAllTasks() + [Fact] + public void RunSync_ValueTaskT_DoesAllTasks() + { + // Act + var result = AsyncHelper.RunSync(async ValueTask () => { - // Act - var result = AsyncHelper.RunSync(async ValueTask () => - { - var i = 1; - await Task.Delay(10); - i += 1; - await Task.Delay(10); - i += 1; - return i; - }); + var i = 1; + await Task.Delay(10); + i += 1; + await Task.Delay(10); + i += 1; + return i; + }); - // Assert + // Assert - Assert.Equal(3, result); - } + Assert.Equal(3, result); + } - [Fact] - public async Task RunSync_ValueTaskT_StartsTasksAndCompletesSynchronously_DoesAllTasks() - { - // Replicates the case where continuations are queued but the main task completes synchronously - // so the work must be removed from the queue + [Fact] + public async Task RunSync_ValueTaskT_StartsTasksAndCompletesSynchronously_DoesAllTasks() + { + // Replicates the case where continuations are queued but the main task completes synchronously + // so the work must be removed from the queue - // Arrange + // Arrange - var i = 0; + var i = 0; - async Task IncrementAsync() - { - await Task.Yield(); - Interlocked.Increment(ref i); - } + async Task IncrementAsync() + { + await Task.Yield(); + Interlocked.Increment(ref i); + } - // Act - AsyncHelper.RunSync(() => - { + // Act + AsyncHelper.RunSync(() => + { #pragma warning disable CS4014 - for (var j = 0; j < 3; j++) - { - var _ = IncrementAsync(); - } + for (var j = 0; j < 3; j++) + { + var _ = IncrementAsync(); + } #pragma warning restore CS4014 - return new ValueTask(true); - }); + return new ValueTask(true); + }); - // Assert + // Assert - await Task.Delay(500, TestContext.Current.CancellationToken); - Assert.Equal(3, i); - } + await Task.Delay(500, TestContext.Current.CancellationToken); + Assert.Equal(3, i); + } - [Fact] - public void RunSync_ValueTaskT_ConfigureAwaitFalse_DoesAllTasks() + [Fact] + public void RunSync_ValueTaskT_ConfigureAwaitFalse_DoesAllTasks() + { + // Act + var result = AsyncHelper.RunSync(async ValueTask () => { - // Act - var result = AsyncHelper.RunSync(async ValueTask () => + var i = 1; + await Task.Delay(10).ConfigureAwait(false); + i += 1; + await Task.Delay(10).ConfigureAwait(false); + i += 1; + return i; + }); + + // Assert + + Assert.Equal(3, result); + } + + [Fact] + public void RunSync_ValueTaskT_ExceptionAfterAwait_ThrowsException() + { + // Act/Assert + Assert.Throws(() => + AsyncHelper.RunSync(async ValueTask () => { - var i = 1; - await Task.Delay(10).ConfigureAwait(false); - i += 1; - await Task.Delay(10).ConfigureAwait(false); - i += 1; - return i; - }); + await Task.Delay(10); - // Assert + throw new InvalidOperationException(); + })); + } - Assert.Equal(3, result); - } + [Fact] + public void RunSync_ValueTaskT_ExceptionBeforeAwait_ThrowsException() + { + // Act/Assert + Assert.Throws(() => + AsyncHelper.RunSync(ValueTask () => throw new InvalidOperationException())); + } - [Fact] - public void RunSync_ValueTaskT_ExceptionAfterAwait_ThrowsException() - { - // Act/Assert - Assert.Throws(() => - AsyncHelper.RunSync(async ValueTask () => - { - await Task.Delay(10); + [Fact] + public void RunSync_ValueTaskT_ThrowsException_ResetsSyncContext() + { + // Arrange - throw new InvalidOperationException(); - })); - } + var sync = new SynchronizationContext(); + SynchronizationContext.SetSynchronizationContext(sync); - [Fact] - public void RunSync_ValueTaskT_ExceptionBeforeAwait_ThrowsException() + // Act + try { - // Act/Assert - Assert.Throws(() => - AsyncHelper.RunSync(ValueTask () => throw new InvalidOperationException())); + AsyncHelper.RunSync(ValueTask () => throw new InvalidOperationException()); } - - [Fact] - public void RunSync_ValueTaskT_ThrowsException_ResetsSyncContext() + catch (InvalidOperationException) { - // Arrange - - var sync = new SynchronizationContext(); - SynchronizationContext.SetSynchronizationContext(sync); - - // Act - try - { - AsyncHelper.RunSync(ValueTask () => throw new InvalidOperationException()); - } - catch (InvalidOperationException) - { - // Expected - } + // Expected + } - // Assert + // Assert - Assert.Equal(sync, SynchronizationContext.Current); - } + Assert.Equal(sync, SynchronizationContext.Current); + } - [Fact] - public void RunSync_ValueTaskT_DanglingContinuations_HandledOnParentSyncContext() - { - // Arrange + [Fact] + public void RunSync_ValueTaskT_DanglingContinuations_HandledOnParentSyncContext() + { + // Arrange - var mockSync = new Mock { CallBase = true }; - SynchronizationContext.SetSynchronizationContext(mockSync.Object); + var mockSync = new Mock { CallBase = true }; + SynchronizationContext.SetSynchronizationContext(mockSync.Object); - var called = false; + var called = false; - // Act - AsyncHelper.RunSync(async ValueTask () => - { - await Task.Yield(); + // Act + AsyncHelper.RunSync(async ValueTask () => + { + await Task.Yield(); #pragma warning disable 4014 - DelayedActionAsync(TimeSpan.FromMilliseconds(400), () => called = true); + DelayedActionAsync(TimeSpan.FromMilliseconds(400), () => called = true); #pragma warning restore 4014 - return 0; - }); + return 0; + }); - // Assert + // Assert - Assert.False(called); + Assert.False(called); - Thread.Sleep(500); + Thread.Sleep(500); - Assert.True(called); + Assert.True(called); - mockSync.Verify( - m => m.Post(It.IsAny(), It.IsAny()), - Times.Once); - } + mockSync.Verify( + m => m.Post(It.IsAny(), It.IsAny()), + Times.Once); + } - #endregion + #endregion - #region RunSyncWithState_ValueTaskT + #region RunSyncWithState_ValueTaskT - [Fact] - public void RunSyncWithState_ValueTaskT_DoesAllTasks() + [Fact] + public void RunSyncWithState_ValueTaskT_DoesAllTasks() + { + // Act + var result = AsyncHelper.RunSync(async ValueTask (state) => { - // Act - var result = AsyncHelper.RunSync(async ValueTask (state) => - { - var i = 1; - await Task.Delay(10); - i += 1; - await Task.Delay(10); - i += 1; - return i; - }, 1); + var i = 1; + await Task.Delay(10); + i += 1; + await Task.Delay(10); + i += 1; + return i; + }, 1); - // Assert + // Assert - Assert.Equal(3, result); - } + Assert.Equal(3, result); + } - [Fact] - public async Task RunSyncWithState_ValueTaskT_StartsTasksAndCompletesSynchronously_DoesAllTasks() - { - // Replicates the case where continuations are queued but the main task completes synchronously - // so the work must be removed from the queue + [Fact] + public async Task RunSyncWithState_ValueTaskT_StartsTasksAndCompletesSynchronously_DoesAllTasks() + { + // Replicates the case where continuations are queued but the main task completes synchronously + // so the work must be removed from the queue - // Arrange + // Arrange - var i = 0; + var i = 0; - async Task IncrementAsync() - { - await Task.Yield(); - Interlocked.Increment(ref i); - } + async Task IncrementAsync() + { + await Task.Yield(); + Interlocked.Increment(ref i); + } - // Act - AsyncHelper.RunSync(state => - { + // Act + AsyncHelper.RunSync(state => + { #pragma warning disable CS4014 - for (var j = 0; j < 3; j++) - { - var _ = IncrementAsync(); - } + for (var j = 0; j < 3; j++) + { + var _ = IncrementAsync(); + } #pragma warning restore CS4014 - return new ValueTask(true); - }, 1); + return new ValueTask(true); + }, 1); - // Assert + // Assert - await Task.Delay(500, TestContext.Current.CancellationToken); - Assert.Equal(3, i); - } + await Task.Delay(500, TestContext.Current.CancellationToken); + Assert.Equal(3, i); + } - [Fact] - public void RunSyncWithState_ValueTaskT_ConfigureAwaitFalse_DoesAllTasks() + [Fact] + public void RunSyncWithState_ValueTaskT_ConfigureAwaitFalse_DoesAllTasks() + { + // Act + var result = AsyncHelper.RunSync(async ValueTask (state) => { - // Act - var result = AsyncHelper.RunSync(async ValueTask (state) => + var i = 1; + await Task.Delay(10).ConfigureAwait(false); + i += 1; + await Task.Delay(10).ConfigureAwait(false); + i += 1; + return i; + }, 1); + + // Assert + + Assert.Equal(3, result); + } + + [Fact] + public void RunSyncWithState_ValueTaskT_ExceptionAfterAwait_ThrowsException() + { + // Act/Assert + Assert.Throws(() => + AsyncHelper.RunSync(async ValueTask (state) => { - var i = 1; - await Task.Delay(10).ConfigureAwait(false); - i += 1; - await Task.Delay(10).ConfigureAwait(false); - i += 1; - return i; - }, 1); + await Task.Delay(10); - // Assert + throw new InvalidOperationException(); + }, 1)); + } - Assert.Equal(3, result); - } + [Fact] + public void RunSyncWithState_ValueTaskT_ExceptionBeforeAwait_ThrowsException() + { + // Act/Assert + Assert.Throws(() => + AsyncHelper.RunSync(ValueTask (_) => throw new InvalidOperationException(), 1)); + } - [Fact] - public void RunSyncWithState_ValueTaskT_ExceptionAfterAwait_ThrowsException() - { - // Act/Assert - Assert.Throws(() => - AsyncHelper.RunSync(async ValueTask (state) => - { - await Task.Delay(10); + [Fact] + public void RunSyncWithState_ValueTaskT_ThrowsException_ResetsSyncContext() + { + // Arrange - throw new InvalidOperationException(); - }, 1)); - } + var sync = new SynchronizationContext(); + SynchronizationContext.SetSynchronizationContext(sync); - [Fact] - public void RunSyncWithState_ValueTaskT_ExceptionBeforeAwait_ThrowsException() + // Act + try { - // Act/Assert - Assert.Throws(() => - AsyncHelper.RunSync(ValueTask (_) => throw new InvalidOperationException(), 1)); + AsyncHelper.RunSync(ValueTask (_) => throw new InvalidOperationException(), 1); } - - [Fact] - public void RunSyncWithState_ValueTaskT_ThrowsException_ResetsSyncContext() + catch (InvalidOperationException) { - // Arrange - - var sync = new SynchronizationContext(); - SynchronizationContext.SetSynchronizationContext(sync); - - // Act - try - { - AsyncHelper.RunSync(ValueTask (_) => throw new InvalidOperationException(), 1); - } - catch (InvalidOperationException) - { - // Expected - } + // Expected + } - // Assert + // Assert - Assert.Equal(sync, SynchronizationContext.Current); - } + Assert.Equal(sync, SynchronizationContext.Current); + } - [Fact] - public void RunSyncWithState_ValueTaskT_DanglingContinuations_HandledOnParentSyncContext() - { - // Arrange + [Fact] + public void RunSyncWithState_ValueTaskT_DanglingContinuations_HandledOnParentSyncContext() + { + // Arrange - var mockSync = new Mock { CallBase = true }; - SynchronizationContext.SetSynchronizationContext(mockSync.Object); + var mockSync = new Mock { CallBase = true }; + SynchronizationContext.SetSynchronizationContext(mockSync.Object); - var called = false; + var called = false; - // Act - AsyncHelper.RunSync(async ValueTask (state) => - { - await Task.Yield(); + // Act + AsyncHelper.RunSync(async ValueTask (state) => + { + await Task.Yield(); #pragma warning disable 4014 - DelayedActionAsync(TimeSpan.FromMilliseconds(400), () => called = true); + DelayedActionAsync(TimeSpan.FromMilliseconds(400), () => called = true); #pragma warning restore 4014 - return 0; - }, 1); + return 0; + }, 1); - // Assert + // Assert - Assert.False(called); + Assert.False(called); - Thread.Sleep(500); + Thread.Sleep(500); - Assert.True(called); + Assert.True(called); - mockSync.Verify( - m => m.Post(It.IsAny(), It.IsAny()), - Times.Once); - } + mockSync.Verify( + m => m.Post(It.IsAny(), It.IsAny()), + Times.Once); + } + + #endregion - #endregion + #region Helpers - #region Helpers + private static readonly AsyncLocal asyncLocalField = new(); + + private static async Task DelayedActionAsync(TimeSpan delay, Action action) + { + await Task.Delay(delay); - private static readonly AsyncLocal asyncLocalField = new(); + action.Invoke(); + } - private static async Task DelayedActionAsync(TimeSpan delay, Action action) + private sealed class FakeSynchronizationContext : SynchronizationContext + { + public static IDisposable Install() { - await Task.Delay(delay); + var previousContext = Current; + var fakeContext = new FakeSynchronizationContext(); - action.Invoke(); + SetSynchronizationContext(fakeContext); + + return new RestoreContext(previousContext); } - #endregion + private sealed class RestoreContext(SynchronizationContext? context) : IDisposable + { + public void Dispose() + { + SetSynchronizationContext(context); + } + } } + + #endregion } diff --git a/src/CenterEdge.Async.UnitTests/CenterEdge.Async.UnitTests.csproj b/src/CenterEdge.Async.UnitTests/CenterEdge.Async.UnitTests.csproj index 69193d7..fd50290 100644 --- a/src/CenterEdge.Async.UnitTests/CenterEdge.Async.UnitTests.csproj +++ b/src/CenterEdge.Async.UnitTests/CenterEdge.Async.UnitTests.csproj @@ -5,7 +5,7 @@ Exe 13 - warnings + enable false diff --git a/src/CenterEdge.Async/AsyncHelper.cs b/src/CenterEdge.Async/AsyncHelper.cs index 8b1e3dc..a60d321 100644 --- a/src/CenterEdge.Async/AsyncHelper.cs +++ b/src/CenterEdge.Async/AsyncHelper.cs @@ -14,6 +14,9 @@ namespace CenterEdge.Async; /// public static class AsyncHelper { + [ThreadStatic] + private static bool t_InRunSync; + // ValueTask-based overloads include OverloadResolutionPriority(-1) so that C# 13 and later will prefer the Task-based // overloads by default when both are applicable. This occurs when passing an async lambda directly to the RunSync method // without an explicit return type, for example: @@ -34,6 +37,65 @@ private static bool IsDeadlockSafe(SynchronizationContext? currentSynchronizatio (currentSynchronizationContext is null || currentSynchronizationContext.GetType() == typeof(SynchronizationContext)) && ReferenceEquals(TaskScheduler.Current, TaskScheduler.Default); + /// + /// Gets a value indicating whether the current operation is executing synchronously within a call to + /// or a similar overload. + /// + /// + /// + /// This will return for nested operations that are running on the thread pool, such as + /// if ConfigureAwait(false) has been used and the continuation has moved to the thread pool. + /// + /// + /// If the calling thread has no synchronization context, this property will return only + /// for the initial synchronous portion of the operation before any awaits that yield to the thread pool. + /// + /// + public static bool IsRunningSynchronously => t_InRunSync; + + /// + /// If is , gets the previously installed + /// that was replaced. Otherwise, returns . + /// + /// + /// If operating within multiple nested calls to or similar overloads, this method + /// recurses to the outermost context and returns the that was + /// replaced by the first call to in the call stack. Always returns + /// if the replaced context is the default . + /// + public static SynchronizationContext? GetReplacedSynchronizationContext() + { + // This check isn't the same as the check for IsRunningSynchronously. When in the IsDeadlockSafe path + // t_InRunSync could be true while SynchronizationContext.Current is unchanged. However, that will + // only happen if there is no SynchronizationContext to begin with, in which case we want to return null anyway. + + var context = SynchronizationContext.Current; + if (context is not ExclusiveSynchronizationContext exclusiveSynchronizationContext) + { + // Not running within RunSync + return null; + } + + while (true) + { + var parentContext = exclusiveSynchronizationContext.ParentSynchronizationContext; + if (parentContext is ExclusiveSynchronizationContext parentExclusiveSynchronizationContext) + { + // Recurse to the outer context + exclusiveSynchronizationContext = parentExclusiveSynchronizationContext; + } + else + { + // Return the parent context, but only if it's not the default SynchronizationContext. + // This provides consistency with await behaviors, which revert the context to null after an + // await if the context is the default one. + return parentContext is not null && !ReferenceEquals(parentContext.GetType(), typeof(SynchronizationContext)) + ? parentContext + : null; + } + } + } + /// /// Executes an async method with no return value synchronously. /// @@ -68,6 +130,8 @@ public static void RunSync(Func task, TState state) } #endif + using var _ = EnterRunSync(); + var oldContext = SynchronizationContext.Current; if (IsDeadlockSafe(oldContext)) @@ -146,6 +210,8 @@ public static void RunSync(Func task, TState state) } #endif + using var _ = EnterRunSync(); + var oldContext = SynchronizationContext.Current; if (IsDeadlockSafe(oldContext)) @@ -226,6 +292,8 @@ public static T RunSync(Func> task, TState state) } #endif + using var _ = EnterRunSync(); + var oldContext = SynchronizationContext.Current; if (IsDeadlockSafe(oldContext)) @@ -305,6 +373,8 @@ public static T RunSync(Func> task, TState state } #endif + using var _ = EnterRunSync(); + var oldContext = SynchronizationContext.Current; if (IsDeadlockSafe(oldContext)) @@ -337,4 +407,24 @@ public static T RunSync(Func> task, TState state SynchronizationContext.SetSynchronizationContext(oldContext); } } + + // Provides a lightweight mechanism for using statements to cleanup the thread-static t_InRunSync flag. + + private static InRunSyncCleanup EnterRunSync() + { + var previousState = t_InRunSync; + t_InRunSync = true; + return new InRunSyncCleanup(previousState); + } + + private readonly ref struct InRunSyncCleanup(bool previousState) : IDisposable + { + public void Dispose() + { + if (!previousState) + { + t_InRunSync = false; + } + } + } } diff --git a/src/CenterEdge.Async/ExclusiveSynchronizationContext.cs b/src/CenterEdge.Async/ExclusiveSynchronizationContext.cs index 3497cc9..ef814da 100644 --- a/src/CenterEdge.Async/ExclusiveSynchronizationContext.cs +++ b/src/CenterEdge.Async/ExclusiveSynchronizationContext.cs @@ -13,6 +13,8 @@ internal sealed class ExclusiveSynchronizationContext( { private readonly BlockingCollection _items = []; + public SynchronizationContext? ParentSynchronizationContext => parentSynchronizationContext; + public override void Send(SendOrPostCallback d, object? state) { throw new NotSupportedException("We cannot send to our same thread"); @@ -96,9 +98,9 @@ public void RunAlreadyComplete() // Executes a work item on the parent SynchronizationContext or on the thread pool if there is not one private void ExecuteOnParent(SendOrPostCallback callback, object? state) { - if (parentSynchronizationContext != null) + if (ParentSynchronizationContext != null) { - parentSynchronizationContext.Post(callback, state); + ParentSynchronizationContext.Post(callback, state); } else { From 698159040d6f0384a3a325cf55d21edbdab97a4f Mon Sep 17 00:00:00 2001 From: Brant Burnett Date: Fri, 24 Oct 2025 10:10:29 -0400 Subject: [PATCH 2/2] Update src/CenterEdge.Async/AsyncHelper.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Brant Burnett --- src/CenterEdge.Async/AsyncHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CenterEdge.Async/AsyncHelper.cs b/src/CenterEdge.Async/AsyncHelper.cs index a60d321..c462f7c 100644 --- a/src/CenterEdge.Async/AsyncHelper.cs +++ b/src/CenterEdge.Async/AsyncHelper.cs @@ -408,7 +408,7 @@ public static T RunSync(Func> task, TState state } } - // Provides a lightweight mechanism for using statements to cleanup the thread-static t_InRunSync flag. + // Provides a lightweight mechanism for using statements to clean up the thread-static t_InRunSync flag. private static InRunSyncCleanup EnterRunSync() {