diff --git a/src/Microsoft.VisualStudio.Threading/AsyncLazy`1.cs b/src/Microsoft.VisualStudio.Threading/AsyncLazy`1.cs
index 057d4621a..630bc2b6f 100644
--- a/src/Microsoft.VisualStudio.Threading/AsyncLazy`1.cs
+++ b/src/Microsoft.VisualStudio.Threading/AsyncLazy`1.cs
@@ -261,6 +261,65 @@ public T GetValue(CancellationToken cancellationToken)
}
}
+ ///
+ /// Marks the code that follows as irrelevant to the receiving value factory.
+ ///
+ /// A value to dispose of to restore relevance into the value factory.
+ ///
+ /// In some cases asynchronous work may be spun off inside a value factory.
+ /// When the value factory does not require this work to finish before the value factory can complete,
+ /// it can be useful to use this method to mark that code as irrelevant to the value factory.
+ /// In particular, this can be necessary when the spun off task may actually include code that may itself
+ /// await the completion of the value factory itself.
+ /// Such a situation would lead to an being thrown from
+ /// if the value factory has not completed already,
+ /// which can introduce non-determinstic failures in the program.
+ /// A using block around the spun off code can help your program achieve reliable behavior, as shown below.
+ ///
+ /// numberOfApples;
+ ///
+ /// public MyClass() {
+ /// this.numberOfApples = new AsyncLazy(async delegate {
+ /// // We have some fire-and-forget code to run.
+ /// // This is *not* relevant to the value factory, which is allowed to complete without waiting for this code to finish.
+ /// using (this.numberOfApples.SuppressRelevance()) {
+ /// this.FireOffNotificationsAsync();
+ /// }
+ ///
+ /// // This code is relevant to the value factory, and must complete before the value factory can complete.
+ /// return await this.CountNumberOfApplesAsync();
+ /// });
+ /// }
+ ///
+ /// public event EventHandler? ApplesCountingHasBegun;
+ ///
+ /// public async Task GetApplesCountAsync(CancellationToken cancellationToken) {
+ /// return await this.numberOfApples.GetValueAsync(cancellationToken);
+ /// }
+ ///
+ /// private async Task CountNumberOfApplesAsync() {
+ /// await Task.Delay(1000);
+ /// return 5;
+ /// }
+ ///
+ /// private async Task FireOffNotificationsAsync() {
+ /// // This may call to 3rd party code, which may happen to call back into GetApplesCountAsync (and thus into our AsyncLazy instance),
+ /// // but such calls should *not* be interpreted as value factory reentrancy. They should just wait for the value factory to finish.
+ /// // We accomplish this by suppressing relevance of the value factory while this code runs (see the caller of this method above).
+ /// this.ApplesCountingHasBegun?.Invoke(this, EventArgs.Empty);
+ /// }
+ /// }
+ /// ]]>
+ ///
+ /// If the was created with a ,
+ /// this method also calls on the
+ /// associated with that factory.
+ ///
+ ///
+ public RevertRelevance SuppressRelevance() => new RevertRelevance(this);
+
///
/// Disposes of the lazily-initialized value if disposable, and causes all subsequent attempts to obtain the value to fail.
///
@@ -373,4 +432,40 @@ public override string ToString()
? (this.value.Status == TaskStatus.RanToCompletion ? $"{this.value.Result}" : Strings.LazyValueFaulted)
: Strings.LazyValueNotCreated;
}
+
+ ///
+ /// A structure that hides relevance of a block of code from a particular and the it was created with.
+ ///
+ public readonly struct RevertRelevance : IDisposable
+ {
+ private readonly AsyncLazy? owner;
+ private readonly object? oldCheckValue;
+ private readonly JoinableTaskContext.RevertRelevance? joinableRelevance;
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The instance that created this value.
+ internal RevertRelevance(AsyncLazy owner)
+ {
+ Requires.NotNull(owner, nameof(owner));
+ this.owner = owner;
+
+ (this.oldCheckValue, owner.recursiveFactoryCheck.Value) = (owner.recursiveFactoryCheck.Value, null);
+ this.joinableRelevance = owner.jobFactory?.Context.SuppressRelevance();
+ }
+
+ ///
+ /// Reverts the async local and thread static values to their original values.
+ ///
+ public void Dispose()
+ {
+ if (this.owner is object)
+ {
+ this.owner.recursiveFactoryCheck.Value = this.oldCheckValue;
+ }
+
+ this.joinableRelevance?.Dispose();
+ }
+ }
}
diff --git a/src/Microsoft.VisualStudio.Threading/net472/PublicAPI.Unshipped.txt b/src/Microsoft.VisualStudio.Threading/net472/PublicAPI.Unshipped.txt
index c5cb0a27a..1a9888196 100644
--- a/src/Microsoft.VisualStudio.Threading/net472/PublicAPI.Unshipped.txt
+++ b/src/Microsoft.VisualStudio.Threading/net472/PublicAPI.Unshipped.txt
@@ -10,4 +10,7 @@ Microsoft.VisualStudio.Threading.AsyncCrossProcessMutex.Name.get -> string!
Microsoft.VisualStudio.Threading.AsyncCrossProcessMutex.TryEnterAsync(System.TimeSpan timeout) -> System.Threading.Tasks.Task!
Microsoft.VisualStudio.Threading.AsyncLazy.DisposeValue() -> void
Microsoft.VisualStudio.Threading.AsyncLazy.DisposeValueAsync() -> System.Threading.Tasks.Task!
-Microsoft.VisualStudio.Threading.AsyncLazy.IsValueDisposed.get -> bool
\ No newline at end of file
+Microsoft.VisualStudio.Threading.AsyncLazy.IsValueDisposed.get -> bool
+Microsoft.VisualStudio.Threading.AsyncLazy.RevertRelevance
+Microsoft.VisualStudio.Threading.AsyncLazy.RevertRelevance.Dispose() -> void
+Microsoft.VisualStudio.Threading.AsyncLazy.SuppressRelevance() -> Microsoft.VisualStudio.Threading.AsyncLazy.RevertRelevance
\ No newline at end of file
diff --git a/src/Microsoft.VisualStudio.Threading/net6.0-windows/PublicAPI.Unshipped.txt b/src/Microsoft.VisualStudio.Threading/net6.0-windows/PublicAPI.Unshipped.txt
index c5cb0a27a..1a9888196 100644
--- a/src/Microsoft.VisualStudio.Threading/net6.0-windows/PublicAPI.Unshipped.txt
+++ b/src/Microsoft.VisualStudio.Threading/net6.0-windows/PublicAPI.Unshipped.txt
@@ -10,4 +10,7 @@ Microsoft.VisualStudio.Threading.AsyncCrossProcessMutex.Name.get -> string!
Microsoft.VisualStudio.Threading.AsyncCrossProcessMutex.TryEnterAsync(System.TimeSpan timeout) -> System.Threading.Tasks.Task!
Microsoft.VisualStudio.Threading.AsyncLazy.DisposeValue() -> void
Microsoft.VisualStudio.Threading.AsyncLazy.DisposeValueAsync() -> System.Threading.Tasks.Task!
-Microsoft.VisualStudio.Threading.AsyncLazy.IsValueDisposed.get -> bool
\ No newline at end of file
+Microsoft.VisualStudio.Threading.AsyncLazy.IsValueDisposed.get -> bool
+Microsoft.VisualStudio.Threading.AsyncLazy.RevertRelevance
+Microsoft.VisualStudio.Threading.AsyncLazy.RevertRelevance.Dispose() -> void
+Microsoft.VisualStudio.Threading.AsyncLazy.SuppressRelevance() -> Microsoft.VisualStudio.Threading.AsyncLazy.RevertRelevance
\ No newline at end of file
diff --git a/src/Microsoft.VisualStudio.Threading/net6.0/PublicAPI.Unshipped.txt b/src/Microsoft.VisualStudio.Threading/net6.0/PublicAPI.Unshipped.txt
index c5cb0a27a..1a9888196 100644
--- a/src/Microsoft.VisualStudio.Threading/net6.0/PublicAPI.Unshipped.txt
+++ b/src/Microsoft.VisualStudio.Threading/net6.0/PublicAPI.Unshipped.txt
@@ -10,4 +10,7 @@ Microsoft.VisualStudio.Threading.AsyncCrossProcessMutex.Name.get -> string!
Microsoft.VisualStudio.Threading.AsyncCrossProcessMutex.TryEnterAsync(System.TimeSpan timeout) -> System.Threading.Tasks.Task!
Microsoft.VisualStudio.Threading.AsyncLazy.DisposeValue() -> void
Microsoft.VisualStudio.Threading.AsyncLazy.DisposeValueAsync() -> System.Threading.Tasks.Task!
-Microsoft.VisualStudio.Threading.AsyncLazy.IsValueDisposed.get -> bool
\ No newline at end of file
+Microsoft.VisualStudio.Threading.AsyncLazy.IsValueDisposed.get -> bool
+Microsoft.VisualStudio.Threading.AsyncLazy.RevertRelevance
+Microsoft.VisualStudio.Threading.AsyncLazy.RevertRelevance.Dispose() -> void
+Microsoft.VisualStudio.Threading.AsyncLazy.SuppressRelevance() -> Microsoft.VisualStudio.Threading.AsyncLazy.RevertRelevance
\ No newline at end of file
diff --git a/src/Microsoft.VisualStudio.Threading/netstandard2.0/PublicAPI.Unshipped.txt b/src/Microsoft.VisualStudio.Threading/netstandard2.0/PublicAPI.Unshipped.txt
index c5cb0a27a..1a9888196 100644
--- a/src/Microsoft.VisualStudio.Threading/netstandard2.0/PublicAPI.Unshipped.txt
+++ b/src/Microsoft.VisualStudio.Threading/netstandard2.0/PublicAPI.Unshipped.txt
@@ -10,4 +10,7 @@ Microsoft.VisualStudio.Threading.AsyncCrossProcessMutex.Name.get -> string!
Microsoft.VisualStudio.Threading.AsyncCrossProcessMutex.TryEnterAsync(System.TimeSpan timeout) -> System.Threading.Tasks.Task!
Microsoft.VisualStudio.Threading.AsyncLazy.DisposeValue() -> void
Microsoft.VisualStudio.Threading.AsyncLazy.DisposeValueAsync() -> System.Threading.Tasks.Task!
-Microsoft.VisualStudio.Threading.AsyncLazy.IsValueDisposed.get -> bool
\ No newline at end of file
+Microsoft.VisualStudio.Threading.AsyncLazy.IsValueDisposed.get -> bool
+Microsoft.VisualStudio.Threading.AsyncLazy.RevertRelevance
+Microsoft.VisualStudio.Threading.AsyncLazy.RevertRelevance.Dispose() -> void
+Microsoft.VisualStudio.Threading.AsyncLazy.SuppressRelevance() -> Microsoft.VisualStudio.Threading.AsyncLazy.RevertRelevance
\ No newline at end of file
diff --git a/test/Microsoft.VisualStudio.Threading.Tests/AsyncLazyTests.cs b/test/Microsoft.VisualStudio.Threading.Tests/AsyncLazyTests.cs
index 9c3e3981e..2077d81c3 100644
--- a/test/Microsoft.VisualStudio.Threading.Tests/AsyncLazyTests.cs
+++ b/test/Microsoft.VisualStudio.Threading.Tests/AsyncLazyTests.cs
@@ -652,6 +652,102 @@ public async Task ExecutionContextFlowsFromFirstCaller_JTF()
await asyncLazy.GetValueAsync();
}
+ [Fact]
+ public async Task SuppressRelevance_WithoutJTF()
+ {
+ AsyncManualResetEvent allowValueFactoryToFinish = new();
+ Task? fireAndForgetTask = null;
+ AsyncLazy asyncLazy = null!;
+ asyncLazy = new AsyncLazy(
+ async delegate
+ {
+ using (asyncLazy.SuppressRelevance())
+ {
+ fireAndForgetTask = FireAndForgetCodeAsync();
+ }
+
+ await allowValueFactoryToFinish;
+ return 1;
+ },
+ null);
+
+ bool fireAndForgetCodeAsyncEntered = false;
+ Task lazyValue = asyncLazy.GetValueAsync();
+ Assert.True(fireAndForgetCodeAsyncEntered);
+ allowValueFactoryToFinish.Set();
+
+ // Assert that the value factory was allowed to finish.
+ Assert.Equal(1, await lazyValue.WithCancellation(this.TimeoutToken));
+
+ // Assert that the fire-and-forget task was allowed to finish and did so without throwing.
+ Assert.Equal(1, await fireAndForgetTask!.WithCancellation(this.TimeoutToken));
+
+ async Task FireAndForgetCodeAsync()
+ {
+ fireAndForgetCodeAsyncEntered = true;
+ return await asyncLazy.GetValueAsync();
+ }
+ }
+
+ [Fact]
+ public async Task SuppressRelevance_WithJTF()
+ {
+ JoinableTaskContext? context = this.InitializeJTCAndSC();
+ SingleThreadedTestSynchronizationContext.IFrame frame = SingleThreadedTestSynchronizationContext.NewFrame();
+
+ JoinableTaskFactory? jtf = context.Factory;
+ AsyncManualResetEvent allowValueFactoryToFinish = new();
+ Task? fireAndForgetTask = null;
+ AsyncLazy asyncLazy = null!;
+ asyncLazy = new AsyncLazy(
+ async delegate
+ {
+ using (asyncLazy.SuppressRelevance())
+ {
+ fireAndForgetTask = FireAndForgetCodeAsync();
+ }
+
+ await allowValueFactoryToFinish;
+ return 1;
+ },
+ jtf);
+
+ bool fireAndForgetCodeAsyncEntered = false;
+ bool fireAndForgetCodeAsyncReachedUIThread = false;
+ jtf.Run(async delegate
+ {
+ Task lazyValue = asyncLazy.GetValueAsync();
+ Assert.True(fireAndForgetCodeAsyncEntered);
+ await Task.Delay(AsyncDelay);
+ Assert.False(fireAndForgetCodeAsyncReachedUIThread);
+ allowValueFactoryToFinish.Set();
+
+ // Assert that the value factory was allowed to finish.
+ Assert.Equal(1, await lazyValue.WithCancellation(this.TimeoutToken));
+ });
+
+ // Run a main thread pump so the fire-and-forget task can finish.
+ SingleThreadedTestSynchronizationContext.PushFrame(SynchronizationContext.Current!, frame);
+
+ // Assert that the fire-and-forget task was allowed to finish and did so without throwing.
+ Assert.Equal(1, await fireAndForgetTask!.WithCancellation(this.TimeoutToken));
+
+ async Task FireAndForgetCodeAsync()
+ {
+ fireAndForgetCodeAsyncEntered = true;
+
+ // Yield the caller's thread.
+ // Resuming will require the main thread, since the caller was on the main thread.
+ await Task.Yield();
+
+ fireAndForgetCodeAsyncReachedUIThread = true;
+
+ int result = await asyncLazy.GetValueAsync();
+ frame.Continue = false;
+ return result;
+ }
+ }
+
[Fact]
public async Task Dispose_ValueType_Completed()
{