diff --git a/src/libraries/Microsoft.Extensions.Options/src/OptionsCache.cs b/src/libraries/Microsoft.Extensions.Options/src/OptionsCache.cs index ac6bab4c221656..a531e11641d15b 100644 --- a/src/libraries/Microsoft.Extensions.Options/src/OptionsCache.cs +++ b/src/libraries/Microsoft.Extensions.Options/src/OptionsCache.cs @@ -16,11 +16,16 @@ public class OptionsCache<[DynamicallyAccessedMembers(Options.DynamicallyAccesse where TOptions : class { private readonly ConcurrentDictionary> _cache = new ConcurrentDictionary>(concurrencyLevel: 1, capacity: 31, StringComparer.Ordinal); // 31 == default capacity + private Lazy? _defaultOptions = null; /// /// Clears all options instances from the cache. /// - public void Clear() => _cache.Clear(); + public void Clear() + { + _defaultOptions = null; + _cache.Clear(); + } /// /// Gets a named options instance, or adds a new instance created with . @@ -35,6 +40,21 @@ public virtual TOptions GetOrAdd(string? name, Func createOptions) name ??= Options.DefaultName; Lazy value; + if (name == Options.DefaultName) + { + if (_defaultOptions is null) + { + // We need a reference to the new instance to be able to return it. Usage of `return _defaultOptions.Value` + // could technically save us some allocations but it would have a risk of sneaky race condition of .Clear + // being called between the Interlocked.CompareExchange call assigning new value and the return, leading to NRE. + var newDefaultOptions = new Lazy(createOptions); + var result = Interlocked.CompareExchange(ref _defaultOptions, newDefaultOptions, null); + + return result is not null ? result.Value : newDefaultOptions.Value; + } + return _defaultOptions.Value; + } + #if NET || NETSTANDARD2_1 value = _cache.GetOrAdd(name, static (name, createOptions) => new Lazy(createOptions), createOptions); #else @@ -51,6 +71,22 @@ internal TOptions GetOrAdd(string? name, Func crea { // For compatibility, fall back to public GetOrAdd() if we're in a derived class. // For simplicity, we do the same for older frameworks that don't support the factoryArgument overload of GetOrAdd(). + name ??= Options.DefaultName; + if (name == Options.DefaultName) + { + if (_defaultOptions is null) + { + // We need a reference to the new instance to be able to return it. Usage of `return _defaultOptions.Value` + // could technically save us some allocations but it would have a risk of sneaky race condition of .Clear + // being called between the Interlocked.CompareExchange call assigning new value and the return, leading to NRE. + var newDefaultOptions = new Lazy(() => createOptions(Options.DefaultName, factoryArgument)); + var result = Interlocked.CompareExchange(ref _defaultOptions, newDefaultOptions, null); + + return result is not null ? result.Value : newDefaultOptions.Value; + } + return _defaultOptions.Value; + } + #if NET || NETSTANDARD2_1 if (GetType() != typeof(OptionsCache)) #endif @@ -59,7 +95,7 @@ internal TOptions GetOrAdd(string? name, Func crea string? localName = name; Func localCreateOptions = createOptions; TArg localFactoryArgument = factoryArgument; - return GetOrAdd(name, () => localCreateOptions(localName ?? Options.DefaultName, localFactoryArgument)); + return GetOrAdd(name, () => localCreateOptions(localName, localFactoryArgument)); } #if NET || NETSTANDARD2_1 @@ -77,7 +113,19 @@ internal TOptions GetOrAdd(string? name, Func crea /// if the options were retrieved; otherwise, . internal bool TryGetValue(string? name, [MaybeNullWhen(false)] out TOptions options) { - if (_cache.TryGetValue(name ?? Options.DefaultName, out Lazy? lazy)) + name ??= Options.DefaultName; + if (name == Options.DefaultName) + { + if (_defaultOptions is { } defaultOptions) + { + options = defaultOptions.Value; + return true; + } + options = default; + return false; + } + + if (_cache.TryGetValue(name, out Lazy? lazy)) { options = lazy.Value; return true; @@ -97,7 +145,18 @@ public virtual bool TryAdd(string? name, TOptions options) { ArgumentNullException.ThrowIfNull(options); - return _cache.TryAdd(name ?? Options.DefaultName, new Lazy( + name ??= Options.DefaultName; + if (name == Options.DefaultName) + { + if (_defaultOptions is not null) + { + return false; // Default options already exist + } + var result = Interlocked.CompareExchange(ref _defaultOptions, new Lazy(() => options), null); + return result is null; + } + + return _cache.TryAdd(name, new Lazy( #if !(NET || NETSTANDARD2_1) () => #endif @@ -109,7 +168,20 @@ public virtual bool TryAdd(string? name, TOptions options) /// /// The name of the options instance. /// if anything was removed; otherwise, . - public virtual bool TryRemove(string? name) => - _cache.TryRemove(name ?? Options.DefaultName, out _); + public virtual bool TryRemove(string? name) + { + name ??= Options.DefaultName; + if (name == Options.DefaultName) + { + if (_defaultOptions is not null) + { + var result = Interlocked.Exchange(ref _defaultOptions, null); + return result is not null; + } + return false; + } + + return _cache.TryRemove(name, out _); + } } } diff --git a/src/libraries/Microsoft.Extensions.Options/src/OptionsMonitor.cs b/src/libraries/Microsoft.Extensions.Options/src/OptionsMonitor.cs index 2c8180d0205674..037c68214b9d96 100644 --- a/src/libraries/Microsoft.Extensions.Options/src/OptionsMonitor.cs +++ b/src/libraries/Microsoft.Extensions.Options/src/OptionsMonitor.cs @@ -127,7 +127,7 @@ public void Dispose() _registrations.Clear(); } - internal sealed class ChangeTrackerDisposable : IDisposable + private sealed class ChangeTrackerDisposable : IDisposable { private readonly Action _listener; private readonly OptionsMonitor _monitor;