I have a need for a simple AsyncLazy<T>
which behaves exactly like Lazy<T>
but correctly supports handling exceptions and avoids caching them.
Specifically the issue I am running into is as follows:
I can write a piece of code as such:
public class TestClass
{
private int i = 0;
public TestClass()
{
this.LazyProperty = new Lazy<string>(() =>
{
if (i == 0)
throw new Exception("My exception");
return "Hello World";
}, LazyThreadSafetyMode.PublicationOnly);
}
public void DoSomething()
{
try
{
var res = this.LazyProperty.Value;
Console.WriteLine(res);
//Never gets here
}
catch { }
i++;
try
{
var res1 = this.LazyProperty.Value;
Console.WriteLine(res1);
//Hello World
}
catch { }
}
public Lazy<string> LazyProperty { get; }
}
Note the use of LazyThreadSafetyMode.PublicationOnly.
If the initialization method throws an exception on any thread, the exception is propagated out of the Value property on that thread. The exception is not cached.
I then invoke it in the following way.
TestClass _testClass = new TestClass();
_testClass.DoSomething();
and it works exactly as you would expect, where the first result is omitted because an exception occurs, the result remains uncached and the subsequent attempt to read the value succeeds returning 'Hello World'.
Unfortunately however if I change my code to something like this:
public Lazy<Task<string>> AsyncLazyProperty { get; } = new Lazy<Task<string>>(async () =>
{
if (i == 0)
throw new Exception("My exception");
return await Task.FromResult("Hello World");
}, LazyThreadSafetyMode.PublicationOnly);
The code fails the first time it is invoked and subsequent calls to the property are cached (and can therefore never recover).
This somewhat makes sense because I suspect that the exception is never actually bubbling up beyond the task, however what I cannot determine is a way to notify the Lazy<T>
that the task/object initialisation has failed and should not be cached.
Anyone able to provide any input?
EDIT:
Thanks for your answer Ivan. I have successfully managed to get a basic example with your feedback but it turns out my problem is actually more complicated than the basic example above demonstrates and undoubtedly this problem will affect others in a similar situation.
So if I change my property signature to something like this (as per Ivans suggestion)
this.LazyProperty = new Lazy<Task<string>>(() =>
{
if (i == 0)
throw new NotImplementedException();
return DoLazyAsync();
}, LazyThreadSafetyMode.PublicationOnly);
and then invoke it like this.
await this.LazyProperty.Value;
the code works.
However if you have a method like this
this.LazyProperty = new Lazy<Task<string>>(() =>
{
return ExecuteAuthenticationAsync();
}, LazyThreadSafetyMode.PublicationOnly);
which then itself calls another Async method.
private static async Task<AccessTokenModel> ExecuteAuthenticationAsync()
{
var response = await AuthExtensions.AuthenticateAsync();
if (!response.Success)
throw new Exception($"Could not authenticate {response.Error}");
return response.Token;
}
The Lazy caching bug manifests again and the problem can be reproduced.
Here is a complete example to reproduce the problem:
this.AccessToken = new Lazy<Task<string>>(() =>
{
return OuterFunctionAsync(counter);
}, LazyThreadSafetyMode.PublicationOnly);
public Lazy<Task<string>> AccessToken { get; private set; }
private static async Task<bool> InnerFunctionAsync(int counter)
{
await Task.Delay(1000);
if (counter == 0)
throw new InvalidOperationException();
return false;
}
private static async Task<string> OuterFunctionAsync(int counter)
{
bool res = await InnerFunctionAsync(counter);
await Task.Delay(1000);
return "12345";
}
try
{
var r = await this.AccessToken.Value;
}
catch (Exception ex) { }
counter++;
try
{
//Retry is never performed, cached task returned.
var r1 = await this.AccessToken.Value;
}
catch (Exception ex) { }
The problem is how Lazy<T>
defines "failed" interfering with how Task<T>
defines "failed".
For an Lazy<T>
initialization to "fail", it has to raise an exception. This is perfectly natural and acceptable, although it is implicitly synchronous.
For Task<T>
to "fail", exceptions are captured and placed on the task. This is the normal pattern for asynchronous code.
Combining the two causes problems. The Lazy<T>
part of Lazy<Task<T>>
will only "fail" if exceptions are raised directly, and the async
pattern of Task<T>
does not propagate exceptions directly. So async
factory methods will always appear to (synchronously) "succeed" since they return a Task<T>
. At this point the Lazy<T>
part is actually done; its value is generated (even if the Task<T>
hasn't completed yet).
You can build your own AsyncLazy<T>
type without much trouble. You don't have to take a dependency on AsyncEx just for that one type:
public sealed class AsyncLazy<T>
{
private readonly object _mutex;
private readonly Func<Task<T>> _factory;
private Lazy<Task<T>> _instance;
public AsyncLazy(Func<Task<T>> factory)
{
_mutex = new object();
_factory = RetryOnFailure(factory);
_instance = new Lazy<Task<T>>(_factory);
}
private Func<Task<T>> RetryOnFailure(Func<Task<T>> factory)
{
return async () =>
{
try
{
return await factory().ConfigureAwait(false);
}
catch
{
lock (_mutex)
{
_instance = new Lazy<Task<T>>(_factory);
}
throw;
}
};
}
public Task<T> Task
{
get
{
lock (_mutex)
return _instance.Value;
}
}
public TaskAwaiter<T> GetAwaiter()
{
return Task.GetAwaiter();
}
public ConfiguredTaskAwaitable<T> ConfigureAwait(bool continueOnCapturedContext)
{
return Task.ConfigureAwait(continueOnCapturedContext);
}
}
To help you understand what's going on here is a simple program:
static void Main()
{
var numberTask = GetNumberAsync( 0 );
Console.WriteLine( numberTask.Status );
Console.ReadLine();
}
private static async Task<Int32> GetNumberAsync( Int32 number )
{
if ( number == 0 )
throw new NotSupportedException();
await Task.Delay( 1000 );
return number;
}
Try it out and you will see that the output of the program will be Faulted
. The method is always returning a result which is Task which has captured the exception.
Why the capturing occurs? It occurs because of async
modifier of the method. Under the covers the actual execution of the method uses AsyncMethodBuilder
which captures the exception and sets it as a result of the task.
How can we change this?
private static Task<Int32> GetNumberAsync( Int32 number )
{
if ( number == 0 )
throw new NotSupportedException();
return GetNumberReallyAsync();
async Task<Int32> GetNumberReallyAsync()
{
await Task.Delay( 1000 );
return number;
}
}
In this example you can see that the method doesn't have the async
modifier and thus the exception is not captured as a faulted Task.
So for your example to work as you want, you need to remove async and await:
public Lazy<Task<string>> AsyncLazyProperty { get; } = new Lazy<Task<string>>(() =>
{
if (i == 0)
throw new Exception("My exception");
return Task.FromResult("Hello World");
}, LazyThreadSafetyMode.PublicationOnly);
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With