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]