From 64659c0044109c0713bf718065ce96afe4d7ead2 Mon Sep 17 00:00:00 2001 From: David Ellingsworth Date: Wed, 18 Mar 2026 10:35:48 -0400 Subject: [PATCH 1/5] Do not call ConfigureAwait(false) from within the CustomSynchronizationContext. --- src/RestSharp/AsyncHelpers.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/RestSharp/AsyncHelpers.cs b/src/RestSharp/AsyncHelpers.cs index 5d3db9db4..24ec1ded3 100644 --- a/src/RestSharp/AsyncHelpers.cs +++ b/src/RestSharp/AsyncHelpers.cs @@ -97,9 +97,12 @@ public void Run() { return; + // This method is only called from within this custom context for the initial task. async void PostCallback(object? _) { try { - await _task().ConfigureAwait(false); + // Do not call ConfigureAwait(false) here to ensure all continuations are + // queued on this context, not the thread pool. + await _task(); } catch (Exception exception) { _caughtException = ExceptionDispatchInfo.Capture(exception); From 511ce62eb782ff91bc5d9d7dee5fd99b8a9004ba Mon Sep 17 00:00:00 2001 From: David Ellingsworth Date: Wed, 18 Mar 2026 10:39:39 -0400 Subject: [PATCH 2/5] Internalize the context handling into the Run method of the CustomSynchronizationContext. This simplifies the RunSync method and clarifies the logic of the CustomSynchronizationContext. --- src/RestSharp/AsyncHelpers.cs | 39 +++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/src/RestSharp/AsyncHelpers.cs b/src/RestSharp/AsyncHelpers.cs index 24ec1ded3..cf86a8ee9 100644 --- a/src/RestSharp/AsyncHelpers.cs +++ b/src/RestSharp/AsyncHelpers.cs @@ -25,16 +25,9 @@ static class AsyncHelpers { /// /// Callback for asynchronous task to run static void RunSync(Func task) { - var currentContext = SynchronizationContext.Current; var customContext = new CustomSynchronizationContext(task); - try { - SynchronizationContext.SetSynchronizationContext(customContext); - customContext.Run(); - } - finally { - SynchronizationContext.SetSynchronizationContext(currentContext); - } + customContext.Run(); } /// @@ -80,20 +73,30 @@ public override void Post(SendOrPostCallback function, object? state) { /// Enqueues the function to be executed and executes all resulting continuations until it is completely done /// public void Run() { - Post(PostCallback, null); + var currentContext = SynchronizationContext.Current; + + try { + SynchronizationContext.SetSynchronizationContext(this); + + Post(PostCallback, null); - while (!_done) { - if (_items.TryDequeue(out var task)) { - task.Item1(task.Item2); - if (_caughtException == null) { - continue; + while (!_done) { + if (_items.TryDequeue(out var task)) { + task.Item1(task.Item2); + if (_caughtException == null) { + continue; + } + _caughtException.Throw(); + } + else { + _workItemsWaiting.WaitOne(); } - _caughtException.Throw(); - } - else { - _workItemsWaiting.WaitOne(); } } + finally { + SynchronizationContext.SetSynchronizationContext(currentContext); + } + return; From 39e089f33966e1ebc17d54624dfe5ea38498d2ca Mon Sep 17 00:00:00 2001 From: David Ellingsworth Date: Wed, 18 Mar 2026 13:34:35 -0400 Subject: [PATCH 3/5] The CustomSyncrhonizationContext is designed to run a single task. As such, the Run method should never be called more than once. Make the constructor and Run method private and add a public static Run method to run a single task on an instance of the context. --- src/RestSharp/AsyncHelpers.cs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/RestSharp/AsyncHelpers.cs b/src/RestSharp/AsyncHelpers.cs index cf86a8ee9..c520d0fa8 100644 --- a/src/RestSharp/AsyncHelpers.cs +++ b/src/RestSharp/AsyncHelpers.cs @@ -25,9 +25,7 @@ static class AsyncHelpers { /// /// Callback for asynchronous task to run static void RunSync(Func task) { - var customContext = new CustomSynchronizationContext(task); - - customContext.Run(); + CustomSynchronizationContext.Run(task); } /// @@ -56,7 +54,7 @@ class CustomSynchronizationContext : SynchronizationContext { /// Constructor for the custom context /// /// Task to execute - public CustomSynchronizationContext(Func task) => + private CustomSynchronizationContext(Func task) => _task = task ?? throw new ArgumentNullException(nameof(task), "Please remember to pass a Task to be executed"); /// @@ -72,7 +70,7 @@ public override void Post(SendOrPostCallback function, object? state) { /// /// Enqueues the function to be executed and executes all resulting continuations until it is completely done /// - public void Run() { + private void Run() { var currentContext = SynchronizationContext.Current; try { @@ -117,6 +115,12 @@ async void PostCallback(object? _) { } } + public static void Run(Func task) { + var customContext = new CustomSynchronizationContext(task); + + customContext.Run(); + } + /// /// When overridden in a derived class, dispatches a synchronous message to a synchronization context. /// From ac183d54dd0db7181530cd8fca518af850066285 Mon Sep 17 00:00:00 2001 From: David Ellingsworth Date: Thu, 19 Mar 2026 14:38:51 -0400 Subject: [PATCH 4/5] Set the done flag once the task completes. --- src/RestSharp/AsyncHelpers.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/RestSharp/AsyncHelpers.cs b/src/RestSharp/AsyncHelpers.cs index c520d0fa8..2c2b18274 100644 --- a/src/RestSharp/AsyncHelpers.cs +++ b/src/RestSharp/AsyncHelpers.cs @@ -103,15 +103,12 @@ async void PostCallback(object? _) { try { // Do not call ConfigureAwait(false) here to ensure all continuations are // queued on this context, not the thread pool. - await _task(); + await _task().ContinueWith(_ => _done = true); } catch (Exception exception) { _caughtException = ExceptionDispatchInfo.Capture(exception); throw; } - finally { - Post(_ => _done = true, null); - } } } From 113594e2a771b9f58041e3f5fee9c5c5d605ae20 Mon Sep 17 00:00:00 2001 From: David Ellingsworth Date: Mon, 20 Apr 2026 15:57:58 -0400 Subject: [PATCH 5/5] Remove the CustomSynchronizationContext and instead use Task.Run to run async methods safely from the Thread Pool. --- src/RestSharp/AsyncHelpers.cs | 109 +--------------------------------- 1 file changed, 3 insertions(+), 106 deletions(-) diff --git a/src/RestSharp/AsyncHelpers.cs b/src/RestSharp/AsyncHelpers.cs index 2c2b18274..ea4e5f77d 100644 --- a/src/RestSharp/AsyncHelpers.cs +++ b/src/RestSharp/AsyncHelpers.cs @@ -14,20 +14,9 @@ // // Adapted from Rebus -using System.Collections.Concurrent; -using System.Runtime.ExceptionServices; - namespace RestSharp; static class AsyncHelpers { - /// - /// Executes a task synchronously on the calling thread by installing a temporary synchronization context that queues continuations - /// - /// Callback for asynchronous task to run - static void RunSync(Func task) { - CustomSynchronizationContext.Run(task); - } - /// /// Executes a task synchronously on the calling thread by installing a temporary synchronization context that queues continuations /// @@ -35,100 +24,8 @@ static void RunSync(Func task) { /// Return type for the task /// Return value from the task public static T RunSync(Func> task) { - T result = default!; - RunSync(async () => { result = await task(); }); - return result; - } - - /// - /// Synchronization context that can be "pumped" in order to have it execute continuations posted back to it - /// - class CustomSynchronizationContext : SynchronizationContext { - readonly ConcurrentQueue> _items = new(); - readonly AutoResetEvent _workItemsWaiting = new(false); - readonly Func _task; - ExceptionDispatchInfo? _caughtException; - bool _done; - - /// - /// Constructor for the custom context - /// - /// Task to execute - private CustomSynchronizationContext(Func task) => - _task = task ?? throw new ArgumentNullException(nameof(task), "Please remember to pass a Task to be executed"); - - /// - /// When overridden in a derived class, dispatches an asynchronous message to a synchronization context. - /// - /// Callback function - /// Callback state - public override void Post(SendOrPostCallback function, object? state) { - _items.Enqueue(Tuple.Create(function, state)); - _workItemsWaiting.Set(); - } - - /// - /// Enqueues the function to be executed and executes all resulting continuations until it is completely done - /// - private void Run() { - var currentContext = SynchronizationContext.Current; - - try { - SynchronizationContext.SetSynchronizationContext(this); - - Post(PostCallback, null); - - while (!_done) { - if (_items.TryDequeue(out var task)) { - task.Item1(task.Item2); - if (_caughtException == null) { - continue; - } - _caughtException.Throw(); - } - else { - _workItemsWaiting.WaitOne(); - } - } - } - finally { - SynchronizationContext.SetSynchronizationContext(currentContext); - } - - - return; - - // This method is only called from within this custom context for the initial task. - async void PostCallback(object? _) { - try { - // Do not call ConfigureAwait(false) here to ensure all continuations are - // queued on this context, not the thread pool. - await _task().ContinueWith(_ => _done = true); - } - catch (Exception exception) { - _caughtException = ExceptionDispatchInfo.Capture(exception); - throw; - } - } - } - - public static void Run(Func task) { - var customContext = new CustomSynchronizationContext(task); - - customContext.Run(); - } - - /// - /// When overridden in a derived class, dispatches a synchronous message to a synchronization context. - /// - /// Callback function - /// Callback state - public override void Send(SendOrPostCallback function, object? state) => throw new NotSupportedException("Cannot send to same thread"); - - /// - /// When overridden in a derived class, creates a copy of the synchronization context. Not needed, so just return ourselves. - /// - /// Copy of the context - public override SynchronizationContext CreateCopy() => this; + var t = Task.Run(async () => await task().ConfigureAwait(false)); + + return t.GetAwaiter().GetResult(); } } \ No newline at end of file