diff --git a/src/libraries/Microsoft.Extensions.Options/src/Microsoft.Extensions.Options.csproj b/src/libraries/Microsoft.Extensions.Options/src/Microsoft.Extensions.Options.csproj
index 39bfd89a00736f..89f467796d51ee 100644
--- a/src/libraries/Microsoft.Extensions.Options/src/Microsoft.Extensions.Options.csproj
+++ b/src/libraries/Microsoft.Extensions.Options/src/Microsoft.Extensions.Options.csproj
@@ -6,6 +6,8 @@
true
true
Provides a strongly typed way of specifying and accessing settings using dependency injection.
+ 1
+ true
diff --git a/src/libraries/Microsoft.Extensions.Options/src/OptionsCache.cs b/src/libraries/Microsoft.Extensions.Options/src/OptionsCache.cs
index 9be845ca4e8b71..51de2bbe4fc27d 100644
--- a/src/libraries/Microsoft.Extensions.Options/src/OptionsCache.cs
+++ b/src/libraries/Microsoft.Extensions.Options/src/OptionsCache.cs
@@ -65,7 +65,7 @@ internal TOptions GetOrAdd(string? name, Func crea
#if NET || NETSTANDARD2_1
return _cache.GetOrAdd(
name ?? Options.DefaultName,
- static (name, arg) => new Lazy(arg.createOptions(name, arg.factoryArgument)), (createOptions, factoryArgument)).Value;
+ static (name, arg) => new Lazy(() => arg.createOptions(name, arg.factoryArgument)), (createOptions, factoryArgument)).Value;
#endif
}
diff --git a/src/libraries/Microsoft.Extensions.Options/tests/Microsoft.Extensions.Options.Tests/OptionsMonitorTest.cs b/src/libraries/Microsoft.Extensions.Options/tests/Microsoft.Extensions.Options.Tests/OptionsMonitorTest.cs
index 06fb1d9476350b..c04f48af0e8f9d 100644
--- a/src/libraries/Microsoft.Extensions.Options/tests/Microsoft.Extensions.Options.Tests/OptionsMonitorTest.cs
+++ b/src/libraries/Microsoft.Extensions.Options/tests/Microsoft.Extensions.Options.Tests/OptionsMonitorTest.cs
@@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
+using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Primitives;
@@ -485,5 +486,56 @@ public void TestCurrentValueDoesNotAllocateOnceValueIsCached()
Assert.Equal(0, GC.GetAllocatedBytesForCurrentThread() - initialBytes);
}
#endif
+
+ ///
+ /// Replicates https://github.com/dotnet/runtime/issues/79529
+ ///
+ [Fact]
+ [SkipOnPlatform(TestPlatforms.Browser, "Synchronous wait is not supported on browser")]
+ public void InstantiatesOnlyOneOptionsInstance()
+ {
+ using AutoResetEvent @event = new(initialState: false);
+
+ OptionsMonitor monitor = new(
+ // WaitHandleConfigureOptions makes instance configuration slow enough to force a race condition
+ new OptionsFactory(new[] { new WaitHandleConfigureOptions(@event) }, Enumerable.Empty>()),
+ Enumerable.Empty>(),
+ new OptionsCache());
+
+ using Barrier barrier = new(participantCount: 2);
+ Task[] instanceTasks = Enumerable.Range(0, 2)
+ .Select(_ => Task.Factory.StartNew(
+ () =>
+ {
+ barrier.SignalAndWait();
+ return monitor.Get("someName");
+ },
+ CancellationToken.None,
+ TaskCreationOptions.LongRunning,
+ TaskScheduler.Default)
+ )
+ .ToArray();
+
+ // No tasks can finish yet; but give them a chance to run and get blocked on the WaitHandle
+ Assert.Equal(-1, Task.WaitAny(instanceTasks, TimeSpan.FromSeconds(0.01)));
+
+ // 1 release should be sufficient to complete both tasks
+ @event.Set();
+ Assert.True(Task.WaitAll(instanceTasks, TimeSpan.FromSeconds(30)));
+ Assert.Equal(1, instanceTasks.Select(t => t.Result).Distinct().Count());
+ }
+
+ private class WaitHandleConfigureOptions : IConfigureNamedOptions
+ {
+ private readonly WaitHandle _waitHandle;
+
+ public WaitHandleConfigureOptions(WaitHandle waitHandle)
+ {
+ _waitHandle = waitHandle;
+ }
+
+ void IConfigureNamedOptions.Configure(string? name, FakeOptions options) => _waitHandle.WaitOne();
+ void IConfigureOptions.Configure(FakeOptions options) => _waitHandle.WaitOne();
+ }
}
}