Stephen Toub's AsyncLazy<T> implementation is pretty nice and concise, but there are a couple of things that I don't like:
In case the asynchronous operation fails, the error is cached, and will be propagated to all future awaiters of the AsyncLazy<T> instance. There is no way to un-cache the cached Task, so that the asynchronous operation can be retried.
The asynchronous delegate is invoked on the ThreadPool context. There is no way to invoke it on the current context.
The Lazy<Task<T>> combination generates warnings in the latest version of the Visual Studio 2019 (16.8.2). It seems that this combination can produce deadlocks in some scenarios.
In the unfortunate case that the asynchronous delegate passed as argument to the Lazy<Task<T>> constructor has not a proper asynchronous implementation, and instead blocks the calling thread, all threads that will await the Lazy<Task<T>> instance will get blocked until the completion of the delegate. This is a direct consequence of how the Lazy<T> type works. This type was never designed for supporting asynchronous operations in any way.
The first issue has been addressed by Stephen Cleary's AsyncLazy<T> implementation (part of the AsyncEx library), that accepts a RetryOnFailure flag in its constructor. The second issue has also been addressed by the same implementation (ExecuteOnCallingThread flag). AFAIK the third and the fourth issues have not been addressed.
Below is an attempt to address all of these issues. This implementation instead of being based on a Lazy<Task<T>>, it uses a transient nested task (Task<Task<T>>) internally as wrapper.
/// <summary>
/// Represents a single asynchronous operation that is started on first demand.
/// In case of failure the error is not cached, and the operation is restarted
/// (retried) later on demand.
/// </summary>
public class AsyncLazy<TResult>
{
private Func<Task<TResult>> _factory;
private Task<TResult> _task;
public AsyncLazy(Func<Task<TResult>> factory)
{
_factory = factory ?? throw new ArgumentNullException(nameof(factory));
}
public Task<TResult> Task
{
get
{
var currentTask = Volatile.Read(ref _task);
if (currentTask == null)
{
Task<TResult> newTask = null;
var newTaskTask = new Task<Task<TResult>>(async () =>
{
try
{
var result = await _factory().ConfigureAwait(false);
_factory = null; // No longer needed (let it get recycled)
return result;
}
catch
{
_ = Interlocked.CompareExchange(ref _task, null, newTask);
throw;
}
});
newTask = newTaskTask.Unwrap();
currentTask = Interlocked
.CompareExchange(ref _task, newTask, null) ?? newTask;
if (currentTask == newTask)
newTaskTask.RunSynchronously(TaskScheduler.Default);
}
return currentTask;
}
}
public TaskAwaiter<TResult> GetAwaiter() { return this.Task.GetAwaiter(); }
}
Usage example:
var deferredTask = new AsyncLazy<string>(async () =>
{
return await _httpClient.GetStringAsync("https://stackoverflow.com");
});
//... (the operation has not started yet)
string html = await deferredTask;
The factory delegate is invoked on the current thread. If you prefer to invoke it on the ThreadPool, just replace the RunSynchronously with the Start. Normally an asynchronous delegate is expected to return quickly, so invoking on the current thread shouldn't be a problem. As a bonus it opens the possibility of interacting with thread-affine components, like UI controls, from inside the delegate.