diff --git a/src/libraries/Microsoft.Extensions.Options/src/OptionsServiceCollectionExtensions.cs b/src/libraries/Microsoft.Extensions.Options/src/OptionsServiceCollectionExtensions.cs
index a84cbe19e1fa6f..3d89c86e2c90ac 100644
--- a/src/libraries/Microsoft.Extensions.Options/src/OptionsServiceCollectionExtensions.cs
+++ b/src/libraries/Microsoft.Extensions.Options/src/OptionsServiceCollectionExtensions.cs
@@ -27,7 +27,7 @@ public static IServiceCollection AddOptions(this IServiceCollection services)
}
services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptions<>), typeof(UnnamedOptionsManager<>)));
- services.TryAdd(ServiceDescriptor.Scoped(typeof(IOptionsSnapshot<>), typeof(OptionsManager<>)));
+ services.TryAdd(ServiceDescriptor.Scoped(typeof(IOptionsSnapshot<>), typeof(OptionsSnapshot<>)));
services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptionsMonitor<>), typeof(OptionsMonitor<>)));
services.TryAdd(ServiceDescriptor.Transient(typeof(IOptionsFactory<>), typeof(OptionsFactory<>)));
services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptionsMonitorCache<>), typeof(OptionsCache<>)));
diff --git a/src/libraries/Microsoft.Extensions.Options/src/OptionsSnapshot.cs b/src/libraries/Microsoft.Extensions.Options/src/OptionsSnapshot.cs
new file mode 100644
index 00000000000000..197d97c19d7e7d
--- /dev/null
+++ b/src/libraries/Microsoft.Extensions.Options/src/OptionsSnapshot.cs
@@ -0,0 +1,66 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Concurrent;
+using System.Diagnostics.CodeAnalysis;
+using System.Threading;
+
+namespace Microsoft.Extensions.Options
+{
+ ///
+ /// Implementation of .
+ ///
+ /// Options type.
+ internal sealed class OptionsSnapshot<[DynamicallyAccessedMembers(Options.DynamicallyAccessedMembers)] TOptions> :
+ IOptionsSnapshot
+ where TOptions : class
+ {
+ private readonly IOptionsMonitor _optionsMonitor;
+
+ private volatile ConcurrentDictionary _cache;
+ private volatile TOptions _unnamedOptionsValue;
+
+ ///
+ /// Initializes a new instance with the specified options configurations.
+ ///
+ /// The options monitor to use to provide options.
+ public OptionsSnapshot(IOptionsMonitor optionsMonitor)
+ {
+ _optionsMonitor = optionsMonitor;
+ }
+
+ ///
+ /// The default configured instance, equivalent to Get(Options.DefaultName).
+ ///
+ public TOptions Value => Get(Options.DefaultName);
+
+ ///
+ /// Returns a configured instance with the given .
+ ///
+ public TOptions Get(string name)
+ {
+ if (name == null || name == Options.DefaultName)
+ {
+ if (_unnamedOptionsValue is TOptions value)
+ {
+ return value;
+ }
+
+ return _unnamedOptionsValue = _optionsMonitor.Get(Options.DefaultName);
+ }
+
+ var cache = _cache ?? Interlocked.CompareExchange(ref _cache, new(concurrencyLevel: 1, capacity: 5, StringComparer.Ordinal), null) ?? _cache;
+
+#if NETSTANDARD2_1
+ TOptions options = cache.GetOrAdd(name, static (name, optionsMonitor) => optionsMonitor.Get(name), _optionsMonitor);
+#else
+ if (!cache.TryGetValue(name, out TOptions options))
+ {
+ options = cache.GetOrAdd(name, _optionsMonitor.Get(name));
+ }
+#endif
+ return options;
+ }
+ }
+}
diff --git a/src/libraries/Microsoft.Extensions.Options/tests/Microsoft.Extensions.Options.Tests/OptionsSnapshotTest.cs b/src/libraries/Microsoft.Extensions.Options/tests/Microsoft.Extensions.Options.Tests/OptionsSnapshotTest.cs
index adb63a5c6913c9..28390691c6903e 100644
--- a/src/libraries/Microsoft.Extensions.Options/tests/Microsoft.Extensions.Options.Tests/OptionsSnapshotTest.cs
+++ b/src/libraries/Microsoft.Extensions.Options/tests/Microsoft.Extensions.Options.Tests/OptionsSnapshotTest.cs
@@ -131,12 +131,14 @@ public void Configure(string name, FakeOptions options)
[Fact]
public void SnapshotOptionsAreCachedPerScope()
{
+ var config = new ConfigurationBuilder().AddInMemoryCollection().Build();
var services = new ServiceCollection()
.AddOptions()
- .AddScoped, TestConfigure>()
+ .AddSingleton, TestConfigure>()
+ .AddSingleton>(new ConfigurationChangeTokenSource(Options.DefaultName, config))
+ .AddSingleton>(new ConfigurationChangeTokenSource("1", config))
.BuildServiceProvider();
- var cache = services.GetRequiredService>();
var factory = services.GetRequiredService();
FakeOptions options = null;
FakeOptions namedOne = null;
@@ -148,18 +150,30 @@ public void SnapshotOptionsAreCachedPerScope()
namedOne = scope.ServiceProvider.GetRequiredService>().Get("1");
Assert.Equal(namedOne, scope.ServiceProvider.GetRequiredService>().Get("1"));
Assert.Equal(2, TestConfigure.ConfigureCount);
+
+ // Reload triggers Configure for the two registered change tokens.
+ config.Reload();
+ Assert.Equal(4, TestConfigure.ConfigureCount);
+
+ // Reload should not affect current scope.
+ var options2 = scope.ServiceProvider.GetRequiredService>().Value;
+ Assert.Equal(options, options2);
+ var namedOne2 = scope.ServiceProvider.GetRequiredService>().Get("1");
+ Assert.Equal(namedOne, namedOne2);
}
Assert.Equal(1, TestConfigure.CtorCount);
+
+ // Reload should be reflected in a fresh scope.
using (var scope = factory.CreateScope())
{
var options2 = scope.ServiceProvider.GetRequiredService>().Value;
Assert.NotEqual(options, options2);
- Assert.Equal(3, TestConfigure.ConfigureCount);
+ Assert.Equal(4, TestConfigure.ConfigureCount);
var namedOne2 = scope.ServiceProvider.GetRequiredService>().Get("1");
- Assert.NotEqual(namedOne2, namedOne);
+ Assert.NotEqual(namedOne, namedOne2);
Assert.Equal(4, TestConfigure.ConfigureCount);
}
- Assert.Equal(2, TestConfigure.CtorCount);
+ Assert.Equal(1, TestConfigure.CtorCount);
}
[Fact]