Мне нужен простой AsyncLazy<T>
, который ведет себя точно так же, как Lazy<T>
, но правильно поддерживает обработку исключений и избегает их кэширования.
В частности, проблема, с которой я сталкиваюсь, такова:
Я могу написать кусок кода как таковой:
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; }
}
Обратите внимание на использование LazyThreadSafetyMode.PublicationOnly .
Если метод инициализации вызывает исключение в каком-либо потоке,
исключение распространяется из свойства Value в этом потоке.
исключение не кэшируется.
Затем я вызываю его следующим образом.
TestClass _testClass = new TestClass();
_testClass.DoSomething();
и он работает точно так, как вы ожидаете, когда первый результат опущен, потому что возникает исключение, результат остается не кэшированным, а последующая попытка прочитать значение завершается успешно, возвращая «Hello World».
К сожалению, однако, если я изменю свой код на что-то вроде этого:
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);
При первом вызове кода происходит сбой, и последующие вызовы свойства кэшируются (и поэтому не могут быть восстановлены).
Это в некоторой степени имеет смысл, потому что я подозреваю, что исключение никогда не всплывает за пределы задачи, однако я не могу определить способ уведомления Lazy<T>
, что инициализация задачи / объекта завершилась неудачно и не должна кэшироваться.
Кто-нибудь может предоставить любой ввод?
EDIT:
Спасибо за ваш ответ, Иван. Мне удалось получить базовый пример с вашими отзывами, но оказалось, что моя проблема на самом деле более сложная, чем демонстрирует приведенный выше базовый пример, и, несомненно, эта проблема повлияет на других в аналогичной ситуации.
Так что, если я изменю свою подпись собственности на что-то вроде этого (согласно предложению Ивана)
this.LazyProperty = new Lazy<Task<string>>(() =>
{
if (i == 0)
throw new NotImplementedException();
return DoLazyAsync();
}, LazyThreadSafetyMode.PublicationOnly);
и затем вызывайте его следующим образом.
await this.LazyProperty.Value;
код работает.
Однако, если у вас есть такой метод
this.LazyProperty = new Lazy<Task<string>>(() =>
{
return ExecuteAuthenticationAsync();
}, LazyThreadSafetyMode.PublicationOnly);
, который затем сам вызывает другой метод Async.
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;
}
Ошибка Ленивое кеширование проявляется снова, и проблема может быть воспроизведена.
Вот полный пример для воспроизведения проблемы:
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) { }