The following method works as expected when called from a unit test or console app:
public Application GetApplication(string applicationName)
{
var application = _oktaApplicationApi.ListApplications(q: applicationName, limit: 1).SingleOrDefaultAsync().Result;
return application;
}
However, when this method is called from ASP.NET, it hangs.
To make this work with ASP.NET, I had to wrap the async
call with Task.Run()
like this:
public Application GetApplication(string applicationName)
{
Application application;
var t = Task.Run(async () =>
{
var a = await _oktaApplicationApi.ListApplications(q: applicationName, limit: 1).SingleOrDefaultAsync();
return a;
});
application = t.Result;
return application;
}
What is causing ASP.NET to hang? Is there a cleaner way to write the method call shown above that does not cause ASP.NET to hang?
Note: while I could make GetApplication()
asynchronous, I am calling this method from a custom MembershipProvider
which is not asynchronous.
Task.Run
is designed for UI frameworks, where Task.Run
will move the logic off of a UI thread and to a background thread. And it runs a Task that you can await on. You are not supposed to wait synchronously on it. To safely wait synchrnously, you need SynchronizationContext.
Task.Run
doesn't manage Synchronization Context, Here is an example.
You can use AsyncPump
(provided at the end), as following. This will prevent deadlock.
var application = AsyncPump.Run(() =>
_oktaApplicationApi.ListApplications(q: applicationName, limit: 1).SingleOrDefaultAsync());
AsyncPump.cs
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.Threading
{
/// <summary>Provides a pump that supports running asynchronous methods on the current thread.</summary>
public static class AsyncPump
{
/// <summary>Runs the specified asynchronous function.</summary>
/// <param name="func">The asynchronous function to execute.</param>
public static T Run<T>(Func<Task<T>> func)
{
if (func == null) throw new ArgumentNullException("func");
var prevCtx = SynchronizationContext.Current;
try
{
// Establish the new context
var syncCtx = new SingleThreadSynchronizationContext();
SynchronizationContext.SetSynchronizationContext(syncCtx);
// Invoke the function and alert the context to when it completes
var t = func();
if (t == null) throw new InvalidOperationException("No task provided.");
t.ContinueWith(delegate { syncCtx.Complete(); }, TaskScheduler.Default);
// Pump continuations and propagate any exceptions
syncCtx.RunOnCurrentThread();
return t.GetAwaiter().GetResult();
}
finally { SynchronizationContext.SetSynchronizationContext(prevCtx); }
}
/// <summary>Runs the specified asynchronous function.</summary>
/// <param name="func">The asynchronous function to execute.</param>
public static void Run(Func<Task> func)
{
if (func == null) throw new ArgumentNullException("func");
var prevCtx = SynchronizationContext.Current;
try
{
// Establish the new context
var syncCtx = new SingleThreadSynchronizationContext();
SynchronizationContext.SetSynchronizationContext(syncCtx);
// Invoke the function and alert the context to when it completes
var t = func();
if (t == null) throw new InvalidOperationException("No task provided.");
t.ContinueWith(delegate { syncCtx.Complete(); }, TaskScheduler.Default);
// Pump continuations and propagate any exceptions
syncCtx.RunOnCurrentThread();
t.GetAwaiter().GetResult();
}
finally { SynchronizationContext.SetSynchronizationContext(prevCtx); }
}
/// <summary>Provides a SynchronizationContext that's single-threaded.</summary>
private sealed class SingleThreadSynchronizationContext : SynchronizationContext
{
/// <summary>The queue of work items.</summary>
private readonly BlockingCollection<KeyValuePair<SendOrPostCallback, object>> m_queue =
new BlockingCollection<KeyValuePair<SendOrPostCallback, object>>();
/// <summary>The processing thread.</summary>
private readonly Thread m_thread = Thread.CurrentThread;
/// <summary>Dispatches an asynchronous message to the synchronization context.</summary>
/// <param name="d">The System.Threading.SendOrPostCallback delegate to call.</param>
/// <param name="state">The object passed to the delegate.</param>
public override void Post(SendOrPostCallback d, object state)
{
if (d == null) throw new ArgumentNullException("d");
m_queue.Add(new KeyValuePair<SendOrPostCallback, object>(d, state));
}
/// <summary>Not supported.</summary>
public override void Send(SendOrPostCallback d, object state)
{
throw new NotSupportedException("Synchronously sending is not supported.");
}
/// <summary>Runs an loop to process all queued work items.</summary>
public void RunOnCurrentThread()
{
foreach (var workItem in m_queue.GetConsumingEnumerable())
workItem.Key(workItem.Value);
}
/// <summary>Notifies the context that no more work will arrive.</summary>
public void Complete() { m_queue.CompleteAdding(); }
}
}
}