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(); + } } }