From 1fe9909f44fddd642a2d1eca8c22e10b0651d92b Mon Sep 17 00:00:00 2001 From: Brant Burnett Date: Tue, 13 Jun 2023 08:29:24 -0400 Subject: [PATCH 1/6] Add optional state parameters to AsyncHelper (PHNX-12680) Motivation ---------- In some scenarios this can reduce heap allocations of closures. Modifications ------------- Add the optional state parameter and matching unit tests. --- .github/workflows/build.yml | 18 +- .github/workflows/cleanup-packages.yml | 3 +- .../AsyncHelperTests.cs | 718 ++++++++++++++++-- src/CenterEdge.Async/AsyncHelper.cs | 154 +++- 4 files changed, 836 insertions(+), 57 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0fe50be..277d1fe 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') }} 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.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/AsyncHelper.cs b/src/CenterEdge.Async/AsyncHelper.cs index f0dfb0e..cc071f4 100644 --- a/src/CenterEdge.Async/AsyncHelper.cs +++ b/src/CenterEdge.Async/AsyncHelper.cs @@ -19,7 +19,7 @@ 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 /// in most cases. This method is provided to assist in gradual conversion from sync to async code. @@ -51,10 +51,46 @@ 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 methods 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 /// in most cases. This method is provided to assist in gradual conversion from sync to async code. @@ -86,10 +122,46 @@ 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 methods 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 @@ -122,10 +194,47 @@ 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 methods 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 @@ -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 methods 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 From 46f241d0551b495096df0fc81eb56326ed903f99 Mon Sep 17 00:00:00 2001 From: Brant Burnett Date: Tue, 13 Jun 2023 08:31:31 -0400 Subject: [PATCH 2/6] try this --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 277d1fe..d3a94ea 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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 From 36299b2d440b1d4267414769d5d3a81c650a238f Mon Sep 17 00:00:00 2001 From: Brant Burnett Date: Tue, 13 Jun 2023 08:32:49 -0400 Subject: [PATCH 3/6] Target .net 6 --- src/CenterEdge.Async/CenterEdge.Async.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CenterEdge.Async/CenterEdge.Async.csproj b/src/CenterEdge.Async/CenterEdge.Async.csproj index 3da05da..8ca89f6 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 From d5377de1ce2b12880154c96e594f2b09abae2edd Mon Sep 17 00:00:00 2001 From: Brant Burnett Date: Tue, 13 Jun 2023 08:34:49 -0400 Subject: [PATCH 4/6] update pkgs --- .../CenterEdge.Async.Benchmarks.csproj | 4 ++-- .../CenterEdge.Async.UnitTests.csproj | 12 ++++++------ src/CenterEdge.Async/CenterEdge.Async.csproj | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) 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/CenterEdge.Async.UnitTests.csproj b/src/CenterEdge.Async.UnitTests/CenterEdge.Async.UnitTests.csproj index 34fe6e9..9de3a7a 100644 --- a/src/CenterEdge.Async.UnitTests/CenterEdge.Async.UnitTests.csproj +++ b/src/CenterEdge.Async.UnitTests/CenterEdge.Async.UnitTests.csproj @@ -10,19 +10,19 @@ - + 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/CenterEdge.Async.csproj b/src/CenterEdge.Async/CenterEdge.Async.csproj index 8ca89f6..8989e79 100644 --- a/src/CenterEdge.Async/CenterEdge.Async.csproj +++ b/src/CenterEdge.Async/CenterEdge.Async.csproj @@ -16,7 +16,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive From 59728ffcefdff07e66800a05406846f60067911b Mon Sep 17 00:00:00 2001 From: Brant Burnett Date: Tue, 13 Jun 2023 08:35:48 -0400 Subject: [PATCH 5/6] Fix dupe --- src/CenterEdge.Async.UnitTests/CenterEdge.Async.UnitTests.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/src/CenterEdge.Async.UnitTests/CenterEdge.Async.UnitTests.csproj b/src/CenterEdge.Async.UnitTests/CenterEdge.Async.UnitTests.csproj index 9de3a7a..ae3f758 100644 --- a/src/CenterEdge.Async.UnitTests/CenterEdge.Async.UnitTests.csproj +++ b/src/CenterEdge.Async.UnitTests/CenterEdge.Async.UnitTests.csproj @@ -19,7 +19,6 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - From cb15aed216c940b06be7aa27dcbd80483bdbdf58 Mon Sep 17 00:00:00 2001 From: Brant Burnett Date: Tue, 13 Jun 2023 09:57:18 -0400 Subject: [PATCH 6/6] fix typo --- src/CenterEdge.Async/AsyncHelper.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/CenterEdge.Async/AsyncHelper.cs b/src/CenterEdge.Async/AsyncHelper.cs index cc071f4..4fbea06 100644 --- a/src/CenterEdge.Async/AsyncHelper.cs +++ b/src/CenterEdge.Async/AsyncHelper.cs @@ -21,7 +21,7 @@ public static class AsyncHelper /// /// 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) @@ -57,7 +57,7 @@ public static void RunSync(Func task) /// method to execute. /// State to pass to the method. /// - /// 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, TState state) @@ -92,7 +92,7 @@ public static void RunSync(Func task, TState state) /// /// 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) @@ -128,7 +128,7 @@ public static void RunSync(Func task) /// method to execute. /// State to pass to the method. /// - /// 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, TState state) @@ -164,7 +164,7 @@ public static void RunSync(Func task, TState state) /// 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) @@ -201,7 +201,7 @@ public static T RunSync(Func> task) /// State to pass to the method. /// 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, TState state) @@ -237,7 +237,7 @@ public static T RunSync(Func> task, TState state) /// 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) @@ -274,7 +274,7 @@ public static T RunSync(Func> task) /// State to pass to the method. /// 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, TState state)