Skip to content

Commit f4f3849

Browse files
authored
Special case default Options in OptionsCache for better perf
1 parent d4c1aa9 commit f4f3849

File tree

2 files changed

+68
-3
lines changed

2 files changed

+68
-3
lines changed

src/libraries/Microsoft.Extensions.Options/src/OptionsCache.cs

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ public class OptionsCache<[DynamicallyAccessedMembers(Options.DynamicallyAccesse
1616
where TOptions : class
1717
{
1818
private readonly ConcurrentDictionary<string, Lazy<TOptions>> _cache = new ConcurrentDictionary<string, Lazy<TOptions>>(concurrencyLevel: 1, capacity: 31, StringComparer.Ordinal); // 31 == default capacity
19+
private Lazy<TOptions>? _defaultOptions = null;
1920

2021
/// <summary>
2122
/// Clears all options instances from the cache.
@@ -35,6 +36,21 @@ public virtual TOptions GetOrAdd(string? name, Func<TOptions> createOptions)
3536
name ??= Options.DefaultName;
3637
Lazy<TOptions> value;
3738

39+
if (name == Options.DefaultName)
40+
{
41+
if (_defaultOptions is null)
42+
{
43+
// We need a reference to the new instance to be able to return it. Usage of `return _defaultOptions.Value`
44+
// could technically save us some allocations but it would have a risk of sneaky race condition of .Clear
45+
// being called between the Interlocked.CompareExchange call assigning new value and the return, leading to NRE.
46+
var newDefaultOptions = new Lazy<TOptions>(createOptions);
47+
var result = Interlocked.CompareExchange(ref _defaultOptions, newDefaultOptions, null);
48+
49+
return result is not null ? result.Value : newDefaultOptions.Value;
50+
}
51+
return _defaultOptions.Value;
52+
}
53+
3854
#if NET || NETSTANDARD2_1
3955
value = _cache.GetOrAdd(name, static (name, createOptions) => new Lazy<TOptions>(createOptions), createOptions);
4056
#else
@@ -51,6 +67,22 @@ internal TOptions GetOrAdd<TArg>(string? name, Func<string, TArg, TOptions> crea
5167
{
5268
// For compatibility, fall back to public GetOrAdd() if we're in a derived class.
5369
// For simplicity, we do the same for older frameworks that don't support the factoryArgument overload of GetOrAdd().
70+
71+
if (name == Options.DefaultName)
72+
{
73+
if (_defaultOptions is null)
74+
{
75+
// We need a reference to the new instance to be able to return it. Usage of `return _defaultOptions.Value`
76+
// could technically save us some allocations but it would have a risk of sneaky race condition of .Clear
77+
// being called between the Interlocked.CompareExchange call assigning new value and the return, leading to NRE.
78+
var newDefaultOptions = new Lazy<TOptions>(() => createOptions(Options.DefaultName, factoryArgument));
79+
var result = Interlocked.CompareExchange(ref _defaultOptions, newDefaultOptions, null);
80+
81+
return result is not null ? result.Value : newDefaultOptions.Value;
82+
}
83+
return _defaultOptions.Value;
84+
}
85+
5486
#if NET || NETSTANDARD2_1
5587
if (GetType() != typeof(OptionsCache<TOptions>))
5688
#endif
@@ -77,6 +109,17 @@ internal TOptions GetOrAdd<TArg>(string? name, Func<string, TArg, TOptions> crea
77109
/// <returns><see langword="true"/> if the options were retrieved; otherwise, <see langword="false"/>.</returns>
78110
internal bool TryGetValue(string? name, [MaybeNullWhen(false)] out TOptions options)
79111
{
112+
if (name == Options.DefaultName)
113+
{
114+
if (_defaultOptions is { } defaultOptions)
115+
{
116+
options = defaultOptions.Value;
117+
return true;
118+
}
119+
options = default;
120+
return false;
121+
}
122+
80123
if (_cache.TryGetValue(name ?? Options.DefaultName, out Lazy<TOptions>? lazy))
81124
{
82125
options = lazy.Value;
@@ -97,6 +140,16 @@ public virtual bool TryAdd(string? name, TOptions options)
97140
{
98141
ArgumentNullException.ThrowIfNull(options);
99142

143+
if (name == Options.DefaultName)
144+
{
145+
if (_defaultOptions is not null)
146+
{
147+
return false; // Default options already exist
148+
}
149+
var result = Interlocked.CompareExchange(ref _defaultOptions, new Lazy<TOptions>(() => options), null);
150+
return result is null;
151+
}
152+
100153
return _cache.TryAdd(name ?? Options.DefaultName, new Lazy<TOptions>(
101154
#if !(NET || NETSTANDARD2_1)
102155
() =>
@@ -109,7 +162,19 @@ public virtual bool TryAdd(string? name, TOptions options)
109162
/// </summary>
110163
/// <param name="name">The name of the options instance.</param>
111164
/// <returns><see langword="true"/> if anything was removed; otherwise, <see langword="false"/>.</returns>
112-
public virtual bool TryRemove(string? name) =>
113-
_cache.TryRemove(name ?? Options.DefaultName, out _);
165+
public virtual bool TryRemove(string? name)
166+
{
167+
if (name == Options.DefaultName)
168+
{
169+
if (_defaultOptions is not null)
170+
{
171+
var result = Interlocked.Exchange(ref _defaultOptions, null);
172+
return result is not null;
173+
}
174+
return false;
175+
}
176+
177+
return _cache.TryRemove(name ?? Options.DefaultName, out _);
178+
}
114179
}
115180
}

src/libraries/Microsoft.Extensions.Options/src/OptionsMonitor.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ public void Dispose()
127127
_registrations.Clear();
128128
}
129129

130-
internal sealed class ChangeTrackerDisposable : IDisposable
130+
private sealed class ChangeTrackerDisposable : IDisposable
131131
{
132132
private readonly Action<TOptions, string> _listener;
133133
private readonly OptionsMonitor<TOptions> _monitor;

0 commit comments

Comments
 (0)