Skip to content

perf: Cache constructed IOptions<T> types in OptionsProvider#1717

Merged
thomhurst merged 1 commit into
mainfrom
fix/options-provider-cache-generic-types
Jan 1, 2026
Merged

perf: Cache constructed IOptions<T> types in OptionsProvider#1717
thomhurst merged 1 commit into
mainfrom
fix/options-provider-cache-generic-types

Conversation

@thomhurst
Copy link
Copy Markdown
Owner

Summary

  • Adds a ConcurrentDictionary<Type, Type> cache for constructed IOptions<T> types
  • Avoids repeated MakeGenericType calls in GetOptions() loop
  • Uses static lambda for cache factory to avoid closure allocation

Changes

Before:

foreach (var option in _cachedOptionTypes.Select(t => _serviceProvider.GetService(typeof(IOptions<>).MakeGenericType(t))))

After:

foreach (var t in _cachedOptionTypes)
{
    var optionsType = IOptionsTypeCache.GetOrAdd(t, static innerType => typeof(IOptions<>).MakeGenericType(innerType));
    var option = _serviceProvider.GetService(optionsType);
    // ...
}

Test plan

  • Build succeeds
  • CI pipeline passes
  • Existing tests pass

Fixes #1548

🤖 Generated with Claude Code

Add a ConcurrentDictionary cache for the constructed IOptions<T> types
to avoid repeated MakeGenericType calls in GetOptions(). While the
option types list was already cached, the generic type construction
was happening on every iteration.

Fixes #1548

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings January 1, 2026 16:49
@thomhurst
Copy link
Copy Markdown
Owner Author

Summary

Adds caching for constructed IOptions<T> generic types to avoid repeated MakeGenericType() calls in the options provider hot path.

Critical Issues

None found ✅

Suggestions

Consider thread-safety pattern consistency: The new cache uses GetOrAdd with a static lambda (static innerType => typeof(IOptions<>).MakeGenericType(innerType)), which is excellent. However, I notice that:

  1. Line 64 uses GetOrAdd with an instance method reference: ValueGetterCache.GetOrAdd(option.GetType(), CreateValueGetter)
  2. The new code uses GetOrAdd with a static lambda

Both are correct, but for consistency, you might consider using a static lambda for the ValueGetterCache call as well:

var getter = ValueGetterCache.GetOrAdd(option.GetType(), static t => CreateValueGetter(t));

This is a minor style point - the current implementation is functionally correct and the optimization is sound.

Verification: The optimization makes sense given that _cachedOptionTypes is static and doesn't change after startup, so you'll be calling MakeGenericType() with the same types repeatedly. Caching these constructed types is a good micro-optimization.

Verdict

APPROVE - No critical issues

The optimization is well-implemented and consistent with the existing performance-focused approach in this class. The use of static lambda prevents unnecessary closure allocations.

@thomhurst thomhurst merged commit a61655d into main Jan 1, 2026
15 of 16 checks passed
@thomhurst thomhurst deleted the fix/options-provider-cache-generic-types branch January 1, 2026 16:58
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a performance optimization to the OptionsProvider class by caching constructed IOptions<T> types. The optimization reduces redundant MakeGenericType calls when iterating through option types.

Key Changes:

  • Added a static ConcurrentDictionary<Type, Type> cache (IOptionsTypeCache) for constructed IOptions<T> types
  • Modified the GetOptions() method to use the cache with a static lambda factory function
  • Eliminates repeated generic type construction overhead during option enumeration

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Performance: OptionsProvider.GetOptions uses MakeGenericType in loop without caching

2 participants