From fb29bb33b3baf4e1f75d668625e72c2eac30fba2 Mon Sep 17 00:00:00 2001 From: Brant Burnett Date: Tue, 13 Jun 2023 10:08:46 -0400 Subject: [PATCH] Add optional state parameters to AsyncHelper (PHNX-12680) (#4) Motivation ---------- In some scenarios this can reduce heap allocations of closures. Modifications ------------- Add the optional state parameter and matching unit tests. Drop .NET 5 target since it is EOL and target .NET 6. https://centeredge.atlassian.net/browse/PHNX-12680 --- .github/workflows/build.yml | 20 +- .github/workflows/cleanup-packages.yml | 3 +- .../CenterEdge.Async.Benchmarks.csproj | 4 +- .../AsyncHelperTests.cs | 718 ++++++++++++++++-- .../CenterEdge.Async.UnitTests.csproj | 13 +- src/CenterEdge.Async/AsyncHelper.cs | 162 +++- src/CenterEdge.Async/CenterEdge.Async.csproj | 4 +- 7 files changed, 851 insertions(+), 73 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0fe50be..d3a94ea 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,29 +12,29 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 + - name: Setup .NET Core + uses: actions/setup-dotnet@v3 + with: + dotnet-version: 6.0.x + - name: Install GitVersion - uses: gittools/actions/gitversion/setup@v0.9.10 + uses: gittools/actions/gitversion/setup@v0.9.15 with: versionSpec: "5.8.0" - name: Determine Version id: gitversion - uses: gittools/actions/gitversion/execute@v0.9.10 + uses: gittools/actions/gitversion/execute@v0.9.15 with: useConfigFile: true configFilePath: "GitVersion.yml" - - name: Setup .NET Core - uses: actions/setup-dotnet@v1 - with: - dotnet-version: 6.0.x - # Cache packages for faster subsequent runs - - uses: actions/cache@v2 + - uses: actions/cache@v3 with: path: ~/.nuget/packages key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }} @@ -44,7 +44,7 @@ jobs: - name: Restore working-directory: ./src run: >- - dotnet nuget add source --username USERNAME --password ${{ secrets.GH_PACKAGES_TOKEN }} --store-password-in-clear-text --name github https://nuget.pkg.github.com/CenterEdge/index.json + dotnet nuget add source --name github https://nuget.pkg.github.com/CenterEdge/index.json && dotnet restore ./CenterEdge.Async.sln - name: Build diff --git a/.github/workflows/cleanup-packages.yml b/.github/workflows/cleanup-packages.yml index 9656b08..77e1281 100644 --- a/.github/workflows/cleanup-packages.yml +++ b/.github/workflows/cleanup-packages.yml @@ -13,8 +13,9 @@ jobs: packages: write steps: - - uses: actions/delete-package-versions@v2 + - uses: actions/delete-package-versions@v4 with: package-name: CenterEdge.Async + package-type: nuget min-versions-to-keep: 30 ignore-versions: ^(?!.*ci-pr).*$ diff --git a/src/CenterEdge.Async.Benchmarks/CenterEdge.Async.Benchmarks.csproj b/src/CenterEdge.Async.Benchmarks/CenterEdge.Async.Benchmarks.csproj index dc90130..ea23682 100644 --- a/src/CenterEdge.Async.Benchmarks/CenterEdge.Async.Benchmarks.csproj +++ b/src/CenterEdge.Async.Benchmarks/CenterEdge.Async.Benchmarks.csproj @@ -14,8 +14,8 @@ false - - + + diff --git a/src/CenterEdge.Async.UnitTests/AsyncHelperTests.cs b/src/CenterEdge.Async.UnitTests/AsyncHelperTests.cs index 089e84b..0f1efc8 100644 --- a/src/CenterEdge.Async.UnitTests/AsyncHelperTests.cs +++ b/src/CenterEdge.Async.UnitTests/AsyncHelperTests.cs @@ -168,32 +168,509 @@ public void RunSync_Task_DanglingContinuations_HandledOnParentSyncContext() #endregion + #region RunSyncWithState_Task + + [Fact] + public void RunSyncWithState_Task_DoesAllTasks() + { + // Arrange + + var i = 0; + + // Act + AsyncHelper.RunSync((Func)(async _ => + { + i += 1; + await Task.Delay(10); + i += 1; + await Task.Delay(10); + i += 1; + }), 1); + + // Assert + + Assert.Equal(3, 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 + + // 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(); + } +#pragma warning restore CS4014 + + return Task.CompletedTask; + }, 1); + + // Assert + + await Task.Delay(500); + Assert.Equal(3, i); + } + + [Fact] + public void RunSyncWithState_Task_ConfigureAwaitFalse_DoesAllTasks() + { + // Arrange + + var i = 0; + + // Act + AsyncHelper.RunSync((Func)(async _ => + { + i += 1; + await Task.Delay(10).ConfigureAwait(false); + i += 1; + await Task.Delay(10).ConfigureAwait(false); + i += 1; + }), 1); + + // Assert + + Assert.Equal(3, i); + } + + [Fact] + public void RunSyncWithState_Task_ExceptionAfterAwait_ThrowsException() + { + // Act/Assert + Assert.Throws(() => + AsyncHelper.RunSync((Func)(async _ => + { + await Task.Delay(10); + + throw new InvalidOperationException(); + }), 1)); + } + + [Fact] + public void RunSyncWithState_Task_ExceptionBeforeAwait_ThrowsException() + { + // Act/Assert + Assert.Throws(() => + AsyncHelper.RunSync((Func)(_ => throw new InvalidOperationException()), 1)); + } + + [Fact] + public void RunSyncWithState_Task_ThrowsException_ResetsSyncContext() + { + // Arrange + + var sync = new SynchronizationContext(); + SynchronizationContext.SetSynchronizationContext(sync); + + // Act + try + { + AsyncHelper.RunSync((Func)(_ => throw new InvalidOperationException()), 1); + } + catch (InvalidOperationException) + { + // Expected + } + + // Assert + + Assert.Equal(sync, SynchronizationContext.Current); + } + + [Fact] + public void RunSyncWithState_Task_DanglingContinuations_HandledOnParentSyncContext() + { + // Arrange + + var mockSync = new Mock { CallBase = true }; + SynchronizationContext.SetSynchronizationContext(mockSync.Object); + + var called = false; + + // Act + AsyncHelper.RunSync((Func)(async _ => + { + await Task.Yield(); + +#pragma warning disable 4014 + DelayedActionAsync(TimeSpan.FromMilliseconds(400), () => called = true); +#pragma warning restore 4014 + }), 1); + + // Assert + + Assert.False(called); + + Thread.Sleep(500); + + Assert.True(called); + + mockSync.Verify( + m => m.Post(It.IsAny(), It.IsAny()), + Times.Once); + } + + #endregion + #region RunSync_ValueTask [Fact] - public void RunSync_ValueTask_DoesAllTasks() + public void RunSync_ValueTask_DoesAllTasks() + { + // Arrange + + var i = 0; + + // Act + AsyncHelper.RunSync((Func)(async () => + { + i += 1; + await Task.Delay(10); + i += 1; + await Task.Delay(10); + i += 1; + })); + + // Assert + + 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 + + // Arrange + + var i = 0; + + async Task IncrementAsync() + { + await Task.Yield(); + Interlocked.Increment(ref i); + } + + // Act + AsyncHelper.RunSync(() => + { +#pragma warning disable CS4014 + for (var j = 0; j < 3; j++) + { + var _ = IncrementAsync(); + } +#pragma warning restore CS4014 + + return new ValueTask(); + }); + + // Assert + + await Task.Delay(500); + Assert.Equal(3, i); + } + + [Fact] + public void RunSync_ValueTask_ConfigureAwaitFalse_DoesAllTasks() + { + // Arrange + + var i = 0; + + // Act + AsyncHelper.RunSync((Func)(async () => + { + i += 1; + await Task.Delay(10).ConfigureAwait(false); + i += 1; + await Task.Delay(10).ConfigureAwait(false); + i += 1; + })); + + // Assert + + Assert.Equal(3, i); + } + + [Fact] + public void RunSync_ValueTask_ExceptionAfterAwait_ThrowsException() + { + // Act/Assert + Assert.Throws(() => + AsyncHelper.RunSync((Func)(async () => + { + await Task.Delay(10); + + throw new InvalidOperationException(); + }))); + } + + [Fact] + public void RunSync_ValueTask_ExceptionBeforeAwait_ThrowsException() + { + // Act/Assert + Assert.Throws(() => + AsyncHelper.RunSync((Func)(() => throw new InvalidOperationException()))); + } + + [Fact] + public void RunSync_ValueTask_ThrowsException_ResetsSyncContext() + { + // Arrange + + var sync = new SynchronizationContext(); + SynchronizationContext.SetSynchronizationContext(sync); + + // Act + try + { + AsyncHelper.RunSync((Func)(() => throw new InvalidOperationException())); + } + catch (InvalidOperationException) + { + // Expected + } + + // Assert + + Assert.Equal(sync, SynchronizationContext.Current); + } + + [Fact] + public void RunSync_ValueTask_DanglingContinuations_HandledOnParentSyncContext() + { + // Arrange + + var mockSync = new Mock { CallBase = true }; + SynchronizationContext.SetSynchronizationContext(mockSync.Object); + + var called = false; + + // Act + AsyncHelper.RunSync((Func)(async () => + { + await Task.Yield(); + +#pragma warning disable 4014 + DelayedActionAsync(TimeSpan.FromMilliseconds(400), () => called = true); +#pragma warning restore 4014 + })); + + // Assert + + Assert.False(called); + + Thread.Sleep(500); + + Assert.True(called); + + mockSync.Verify( + m => m.Post(It.IsAny(), It.IsAny()), + Times.Once); + } + + #endregion + + #region RunSyncWithState_ValueTask + + [Fact] + public void RunSyncWithState_ValueTask_DoesAllTasks() + { + // Arrange + + var i = 0; + + // Act + AsyncHelper.RunSync((Func)(async state => + { + i += 1; + await Task.Delay(10); + i += 1; + await Task.Delay(10); + i += 1; + }), 1); + + // Assert + + 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 + + // 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(); + } +#pragma warning restore CS4014 + + return new ValueTask(); + }, 1); + + // Assert + + await Task.Delay(500); + Assert.Equal(3, i); + } + + [Fact] + public void RunSyncWithState_ValueTask_ConfigureAwaitFalse_DoesAllTasks() + { + // Arrange + + var i = 0; + + // Act + AsyncHelper.RunSync((Func)(async state => + { + i += 1; + await Task.Delay(10).ConfigureAwait(false); + i += 1; + await Task.Delay(10).ConfigureAwait(false); + i += 1; + }), 1); + + // Assert + + Assert.Equal(3, i); + } + + [Fact] + public void RunSyncWithState_ValueTask_ExceptionAfterAwait_ThrowsException() + { + // Act/Assert + Assert.Throws(() => + AsyncHelper.RunSync((Func)(async state => + { + await Task.Delay(10); + + throw new InvalidOperationException(); + }), 1)); + } + + [Fact] + public void RunSyncWithState_ValueTask_ExceptionBeforeAwait_ThrowsException() + { + // Act/Assert + Assert.Throws(() => + AsyncHelper.RunSync((Func)(_ => throw new InvalidOperationException()), 1)); + } + + [Fact] + public void RunSyncWithState_ValueTask_ThrowsException_ResetsSyncContext() { // Arrange - var i = 0; + var sync = new SynchronizationContext(); + SynchronizationContext.SetSynchronizationContext(sync); // Act - AsyncHelper.RunSync((Func)(async () => + try { - i += 1; + AsyncHelper.RunSync((Func)(_ => throw new InvalidOperationException()), 1); + } + catch (InvalidOperationException) + { + // Expected + } + + // Assert + + Assert.Equal(sync, SynchronizationContext.Current); + } + + [Fact] + public void RunSyncWithState_ValueTask_DanglingContinuations_HandledOnParentSyncContext() + { + // Arrange + + var mockSync = new Mock { CallBase = true }; + SynchronizationContext.SetSynchronizationContext(mockSync.Object); + + var called = false; + + // Act + AsyncHelper.RunSync((Func)(async state => + { + await Task.Yield(); + +#pragma warning disable 4014 + DelayedActionAsync(TimeSpan.FromMilliseconds(400), () => called = true); +#pragma warning restore 4014 + }), 1); + + // Assert + + Assert.False(called); + + Thread.Sleep(500); + + Assert.True(called); + + mockSync.Verify( + m => m.Post(It.IsAny(), It.IsAny()), + Times.Once); + } + + #endregion + + #region RunSync_TaskT + + [Fact] + public void RunSync_TaskT_DoesAllTasks() + { + // Act + var result = AsyncHelper.RunSync((Func>)(async () => + { + var i = 1; await Task.Delay(10); i += 1; await Task.Delay(10); i += 1; + return i; })); // Assert - Assert.Equal(3, i); + Assert.Equal(3, result); } [Fact] - public async Task RunSync_ValueTask_StartsTasksAndCompletesSynchronously_DoesAllTasks() + 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 @@ -218,7 +695,7 @@ async Task IncrementAsync() } #pragma warning restore CS4014 - return new ValueTask(); + return Task.FromResult(true); }); // Assert @@ -228,33 +705,30 @@ async Task IncrementAsync() } [Fact] - public void RunSync_ValueTask_ConfigureAwaitFalse_DoesAllTasks() + public void RunSync_TaskT_ConfigureAwaitFalse_DoesAllTasks() { - // Arrange - - var i = 0; - // Act - AsyncHelper.RunSync((Func)(async () => + var result = AsyncHelper.RunSync((Func>)(async () => { - i += 1; + 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, i); + Assert.Equal(3, result); } [Fact] - public void RunSync_ValueTask_ExceptionAfterAwait_ThrowsException() + public void RunSync_TaskT_ExceptionAfterAwait_ThrowsException() { // Act/Assert Assert.Throws(() => - AsyncHelper.RunSync((Func)(async () => + AsyncHelper.RunSync((Func>)(async () => { await Task.Delay(10); @@ -263,15 +737,15 @@ public void RunSync_ValueTask_ExceptionAfterAwait_ThrowsException() } [Fact] - public void RunSync_ValueTask_ExceptionBeforeAwait_ThrowsException() + public void RunSync_TaskT_ExceptionBeforeAwait_ThrowsException() { // Act/Assert Assert.Throws(() => - AsyncHelper.RunSync((Func)(() => throw new InvalidOperationException()))); + AsyncHelper.RunSync((Func>)(() => throw new InvalidOperationException()))); } [Fact] - public void RunSync_ValueTask_ThrowsException_ResetsSyncContext() + public void RunSync_TaskT_ThrowsException_ResetsSyncContext() { // Arrange @@ -281,7 +755,7 @@ public void RunSync_ValueTask_ThrowsException_ResetsSyncContext() // Act try { - AsyncHelper.RunSync((Func)(() => throw new InvalidOperationException())); + AsyncHelper.RunSync((Func>)(() => throw new InvalidOperationException())); } catch (InvalidOperationException) { @@ -294,7 +768,7 @@ public void RunSync_ValueTask_ThrowsException_ResetsSyncContext() } [Fact] - public void RunSync_ValueTask_DanglingContinuations_HandledOnParentSyncContext() + public void RunSync_TaskT_DanglingContinuations_HandledOnParentSyncContext() { // Arrange @@ -304,13 +778,15 @@ public void RunSync_ValueTask_DanglingContinuations_HandledOnParentSyncContext() var called = false; // Act - AsyncHelper.RunSync((Func)(async () => + AsyncHelper.RunSync((Func>)(async () => { await Task.Yield(); #pragma warning disable 4014 DelayedActionAsync(TimeSpan.FromMilliseconds(400), () => called = true); #pragma warning restore 4014 + + return 0; })); // Assert @@ -328,13 +804,13 @@ public void RunSync_ValueTask_DanglingContinuations_HandledOnParentSyncContext() #endregion - #region RunSync_TaskT + #region RunSyncWithState_TaskT [Fact] - public void RunSync_TaskT_DoesAllTasks() + public void RunSyncWithState_TaskT_DoesAllTasks() { // Act - var result = AsyncHelper.RunSync((Func>)(async () => + var result = AsyncHelper.RunSync((Func>)(async state => { var i = 1; await Task.Delay(10); @@ -342,7 +818,7 @@ public void RunSync_TaskT_DoesAllTasks() await Task.Delay(10); i += 1; return i; - })); + }), 1); // Assert @@ -350,7 +826,7 @@ public void RunSync_TaskT_DoesAllTasks() } [Fact] - public async Task RunSync_TaskT_StartsTasksAndCompletesSynchronously_DoesAllTasks() + 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 @@ -366,7 +842,7 @@ async Task IncrementAsync() } // Act - AsyncHelper.RunSync(() => + AsyncHelper.RunSync(state => { #pragma warning disable CS4014 for (var j = 0; j < 3; j++) @@ -376,7 +852,7 @@ async Task IncrementAsync() #pragma warning restore CS4014 return Task.FromResult(true); - }); + }, 1); // Assert @@ -385,10 +861,10 @@ async Task IncrementAsync() } [Fact] - public void RunSync_TaskT_ConfigureAwaitFalse_DoesAllTasks() + public void RunSyncWithState_TaskT_ConfigureAwaitFalse_DoesAllTasks() { // Act - var result = AsyncHelper.RunSync((Func>)(async () => + var result = AsyncHelper.RunSync((Func>)(async state => { var i = 1; await Task.Delay(10).ConfigureAwait(false); @@ -396,7 +872,7 @@ public void RunSync_TaskT_ConfigureAwaitFalse_DoesAllTasks() await Task.Delay(10).ConfigureAwait(false); i += 1; return i; - })); + }), 1); // Assert @@ -404,28 +880,28 @@ public void RunSync_TaskT_ConfigureAwaitFalse_DoesAllTasks() } [Fact] - public void RunSync_TaskT_ExceptionAfterAwait_ThrowsException() + public void RunSyncWithState_TaskT_ExceptionAfterAwait_ThrowsException() { // Act/Assert Assert.Throws(() => - AsyncHelper.RunSync((Func>)(async () => + AsyncHelper.RunSync((Func>)(async state => { await Task.Delay(10); throw new InvalidOperationException(); - }))); + }), 1)); } [Fact] - public void RunSync_TaskT_ExceptionBeforeAwait_ThrowsException() + public void RunSyncWithState_TaskT_ExceptionBeforeAwait_ThrowsException() { // Act/Assert Assert.Throws(() => - AsyncHelper.RunSync((Func>)(() => throw new InvalidOperationException()))); + AsyncHelper.RunSync((Func>)(_ => throw new InvalidOperationException()), 1)); } [Fact] - public void RunSync_TaskT_ThrowsException_ResetsSyncContext() + public void RunSyncWithState_TaskT_ThrowsException_ResetsSyncContext() { // Arrange @@ -435,7 +911,7 @@ public void RunSync_TaskT_ThrowsException_ResetsSyncContext() // Act try { - AsyncHelper.RunSync((Func>)(() => throw new InvalidOperationException())); + AsyncHelper.RunSync((Func>)(_ => throw new InvalidOperationException()), 1); } catch (InvalidOperationException) { @@ -448,7 +924,7 @@ public void RunSync_TaskT_ThrowsException_ResetsSyncContext() } [Fact] - public void RunSync_TaskT_DanglingContinuations_HandledOnParentSyncContext() + public void RunSyncWithState_TaskT_DanglingContinuations_HandledOnParentSyncContext() { // Arrange @@ -458,7 +934,7 @@ public void RunSync_TaskT_DanglingContinuations_HandledOnParentSyncContext() var called = false; // Act - AsyncHelper.RunSync((Func>)(async () => + AsyncHelper.RunSync((Func>)(async state => { await Task.Yield(); @@ -467,7 +943,7 @@ public void RunSync_TaskT_DanglingContinuations_HandledOnParentSyncContext() #pragma warning restore 4014 return 0; - })); + }), 1); // Assert @@ -640,6 +1116,162 @@ public void RunSync_ValueTaskT_DanglingContinuations_HandledOnParentSyncContext( #endregion + #region RunSyncWithState_ValueTaskT + + [Fact] + public void RunSyncWithState_ValueTaskT_DoesAllTasks() + { + // Act + var result = AsyncHelper.RunSync((Func>)(async state => + { + var i = 1; + await Task.Delay(10); + i += 1; + await Task.Delay(10); + i += 1; + return i; + }), 1); + + // Assert + + 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 + + // 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(); + } +#pragma warning restore CS4014 + + return new ValueTask(true); + }, 1); + + // Assert + + await Task.Delay(500); + Assert.Equal(3, i); + } + + [Fact] + public void RunSyncWithState_ValueTaskT_ConfigureAwaitFalse_DoesAllTasks() + { + // Act + var result = AsyncHelper.RunSync((Func>)(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_ValueTaskT_ExceptionAfterAwait_ThrowsException() + { + // Act/Assert + Assert.Throws(() => + AsyncHelper.RunSync((Func>)(async state => + { + await Task.Delay(10); + + throw new InvalidOperationException(); + }), 1)); + } + + [Fact] + public void RunSyncWithState_ValueTaskT_ExceptionBeforeAwait_ThrowsException() + { + // Act/Assert + Assert.Throws(() => + AsyncHelper.RunSync((Func>)(_ => throw new InvalidOperationException()), 1)); + } + + [Fact] + public void RunSyncWithState_ValueTaskT_ThrowsException_ResetsSyncContext() + { + // Arrange + + var sync = new SynchronizationContext(); + SynchronizationContext.SetSynchronizationContext(sync); + + // Act + try + { + AsyncHelper.RunSync((Func>)(_ => throw new InvalidOperationException()), 1); + } + catch (InvalidOperationException) + { + // Expected + } + + // Assert + + Assert.Equal(sync, SynchronizationContext.Current); + } + + [Fact] + public void RunSyncWithState_ValueTaskT_DanglingContinuations_HandledOnParentSyncContext() + { + // Arrange + + var mockSync = new Mock { CallBase = true }; + SynchronizationContext.SetSynchronizationContext(mockSync.Object); + + var called = false; + + // Act + AsyncHelper.RunSync((Func>)(async state => + { + await Task.Yield(); + +#pragma warning disable 4014 + DelayedActionAsync(TimeSpan.FromMilliseconds(400), () => called = true); +#pragma warning restore 4014 + + return 0; + }), 1); + + // Assert + + Assert.False(called); + + Thread.Sleep(500); + + Assert.True(called); + + mockSync.Verify( + m => m.Post(It.IsAny(), It.IsAny()), + Times.Once); + } + + #endregion + #region Helpers private static readonly AsyncLocal asyncLocalField = new(); diff --git a/src/CenterEdge.Async.UnitTests/CenterEdge.Async.UnitTests.csproj b/src/CenterEdge.Async.UnitTests/CenterEdge.Async.UnitTests.csproj index 34fe6e9..ae3f758 100644 --- a/src/CenterEdge.Async.UnitTests/CenterEdge.Async.UnitTests.csproj +++ b/src/CenterEdge.Async.UnitTests/CenterEdge.Async.UnitTests.csproj @@ -10,19 +10,18 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/CenterEdge.Async/AsyncHelper.cs b/src/CenterEdge.Async/AsyncHelper.cs index f0dfb0e..4fbea06 100644 --- a/src/CenterEdge.Async/AsyncHelper.cs +++ b/src/CenterEdge.Async/AsyncHelper.cs @@ -19,9 +19,9 @@ public static class AsyncHelper /// /// Executes an async method with no return value synchronously. /// - /// method to execute + /// method to execute. /// - /// DO NOT use this methods unless absolutely necessary. Calling async code from sync code is an anti-pattern + /// DO NOT use this method unless absolutely necessary. Calling async code from sync code is an anti-pattern /// in most cases. This method is provided to assist in gradual conversion from sync to async code. /// public static void RunSync(Func task) @@ -51,12 +51,48 @@ public static void RunSync(Func task) } } + /// + /// Executes an async method with no return value synchronously. + /// + /// method to execute. + /// State to pass to the method. + /// + /// DO NOT use this method unless absolutely necessary. Calling async code from sync code is an anti-pattern + /// in most cases. This method is provided to assist in gradual conversion from sync to async code. + /// + public static void RunSync(Func task, TState state) + { + var oldContext = SynchronizationContext.Current; + using var synch = new ExclusiveSynchronizationContext(oldContext); + SynchronizationContext.SetSynchronizationContext(synch); + try + { + var awaiter = task(state).GetAwaiter(); + + if (!awaiter.IsCompleted) + { + synch.Run(awaiter); + } + else + { + synch.RunAlreadyComplete(); + } + + // Throw any exception returned by the task + awaiter.GetResult(); + } + finally + { + SynchronizationContext.SetSynchronizationContext(oldContext); + } + } + /// /// Executes an async method which has a void return value synchronously. /// - /// method to execute + /// method to execute. /// - /// DO NOT use this methods unless absolutely necessary. Calling async code from sync code is an anti-pattern + /// DO NOT use this method unless absolutely necessary. Calling async code from sync code is an anti-pattern /// in most cases. This method is provided to assist in gradual conversion from sync to async code. /// public static void RunSync(Func task) @@ -86,13 +122,49 @@ public static void RunSync(Func task) } } + /// + /// Executes an async method which has a void return value synchronously. + /// + /// method to execute. + /// State to pass to the method. + /// + /// DO NOT use this method unless absolutely necessary. Calling async code from sync code is an anti-pattern + /// in most cases. This method is provided to assist in gradual conversion from sync to async code. + /// + public static void RunSync(Func task, TState state) + { + var oldContext = SynchronizationContext.Current; + using var synch = new ExclusiveSynchronizationContext(oldContext); + SynchronizationContext.SetSynchronizationContext(synch); + try + { + var awaiter = task(state).GetAwaiter(); + + if (!awaiter.IsCompleted) + { + synch.Run(awaiter); + } + else + { + synch.RunAlreadyComplete(); + } + + // Throw any exception returned by the task + awaiter.GetResult(); + } + finally + { + SynchronizationContext.SetSynchronizationContext(oldContext); + } + } + /// /// Executes an async method which has a void return value synchronously. /// - /// method to execute + /// method to execute. /// The asynchronous result. /// - /// DO NOT use this methods unless absolutely necessary. Calling async code from sync code is an anti-pattern + /// DO NOT use this method unless absolutely necessary. Calling async code from sync code is an anti-pattern /// in most cases. This method is provided to assist in gradual conversion from sync to async code. /// public static T RunSync(Func> task) @@ -122,13 +194,50 @@ public static T RunSync(Func> task) } } + /// + /// Executes an async method which has a void return value synchronously. + /// + /// method to execute. + /// State to pass to the method. + /// The asynchronous result. + /// + /// DO NOT use this method unless absolutely necessary. Calling async code from sync code is an anti-pattern + /// in most cases. This method is provided to assist in gradual conversion from sync to async code. + /// + public static T RunSync(Func> task, TState state) + { + var oldContext = SynchronizationContext.Current; + using var synch = new ExclusiveSynchronizationContext>(oldContext); + SynchronizationContext.SetSynchronizationContext(synch); + try + { + var awaiter = task(state).GetAwaiter(); + + if (!awaiter.IsCompleted) + { + synch.Run(awaiter); + } + else + { + synch.RunAlreadyComplete(); + } + + // Throw any exception returned by the task or return the result + return awaiter.GetResult(); + } + finally + { + SynchronizationContext.SetSynchronizationContext(oldContext); + } + } + /// /// Executes an async method which has a void return value synchronously. /// - /// method to execute + /// method to execute. /// The asynchronous result. /// - /// DO NOT use this methods unless absolutely necessary. Calling async code from sync code is an anti-pattern + /// DO NOT use this method unless absolutely necessary. Calling async code from sync code is an anti-pattern /// in most cases. This method is provided to assist in gradual conversion from sync to async code. /// public static T RunSync(Func> task) @@ -158,6 +267,43 @@ public static T RunSync(Func> task) } } + /// + /// Executes an async method which has a void return value synchronously. + /// + /// method to execute. + /// State to pass to the method. + /// The asynchronous result. + /// + /// DO NOT use this method unless absolutely necessary. Calling async code from sync code is an anti-pattern + /// in most cases. This method is provided to assist in gradual conversion from sync to async code. + /// + public static T RunSync(Func> task, TState state) + { + var oldContext = SynchronizationContext.Current; + using var synch = new ExclusiveSynchronizationContext>(oldContext); + SynchronizationContext.SetSynchronizationContext(synch); + try + { + var awaiter = task(state).GetAwaiter(); + + if (!awaiter.IsCompleted) + { + synch.Run(awaiter); + } + else + { + synch.RunAlreadyComplete(); + } + + // Throw any exception returned by the task or return the result + return awaiter.GetResult(); + } + finally + { + SynchronizationContext.SetSynchronizationContext(oldContext); + } + } + // Note: Sealing this class can help JIT make non-virtual method calls and inlined method calls for virtual methods private sealed class ExclusiveSynchronizationContext : SynchronizationContext, IDisposable where TAwaiter : struct, ICriticalNotifyCompletion diff --git a/src/CenterEdge.Async/CenterEdge.Async.csproj b/src/CenterEdge.Async/CenterEdge.Async.csproj index 3da05da..8989e79 100644 --- a/src/CenterEdge.Async/CenterEdge.Async.csproj +++ b/src/CenterEdge.Async/CenterEdge.Async.csproj @@ -1,7 +1,7 @@  - netstandard2.0;netstandard2.1;net5.0 + netstandard2.0;netstandard2.1;net6.0 10 enable @@ -16,7 +16,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive