Skip to content

Commit 9382fb3

Browse files
committed
feat: extend support for Early Access to Options During Service Registration
1 parent b5a91ed commit 9382fb3

File tree

16 files changed

+1820
-43
lines changed

16 files changed

+1820
-43
lines changed

CLAUDE.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,74 @@ services.AddDependencyRegistrationsFromDomain(
349349
- Requires classes to be declared `partial`
350350
- **Smart naming** - uses short suffix if unique, full name if conflicts exist
351351
- **Transitive registration**: Generates 4 overloads for each assembly to support automatic or selective registration of referenced assemblies
352+
- **Early access to options**: Avoid BuildServiceProvider anti-pattern with GetOrAdd methods for accessing options during service registration
353+
354+
**Early Access to Options (Avoids BuildServiceProvider Anti-Pattern):**
355+
356+
Three APIs available for accessing options during service registration:
357+
358+
| Method | Reads Cache | Writes Cache | Use Case |
359+
|--------|-------------|--------------|----------|
360+
| `Get[Type]...` | ✅ Yes | ❌ No | Efficient retrieval (uses cached if available, no side effects) |
361+
| `GetOrAdd[Type]...` | ✅ Yes | ✅ Yes | Early access with caching for idempotency |
362+
| `GetOptions<T>()` | ✅ Yes | ❌ No | Smart dispatcher (calls `Get[Type]...` internally) |
363+
364+
```csharp
365+
// Problem: Need options values during service registration but don't want BuildServiceProvider()
366+
// Solution: Three APIs available for early access
367+
368+
// API 1: Get methods - Efficient retrieval (reads cache, doesn't populate)
369+
var dbOptions1 = services.GetDatabaseOptionsFromDomain(configuration);
370+
var dbOptions2 = services.GetDatabaseOptionsFromDomain(configuration);
371+
// If GetOrAdd was never called: dbOptions1 != dbOptions2 (different instances)
372+
// If GetOrAdd was called first: dbOptions1 == dbOptions2 (returns cached instance)
373+
374+
// API 2: GetOrAdd methods - With caching (idempotent, populates cache)
375+
var dbCached1 = services.GetOrAddDatabaseOptionsFromDomain(configuration);
376+
var dbCached2 = services.GetOrAddDatabaseOptionsFromDomain(configuration);
377+
// dbCached1 == dbCached2 (same instance, cached for reuse)
378+
379+
// API 3: Generic smart dispatcher (calls Get internally - reads cache, doesn't populate)
380+
var dbOptions3 = services.GetOptions<DatabaseOptions>(configuration);
381+
// Internally calls GetDatabaseOptionsFromDomain() - benefits from cache if available
382+
// Works in multi-assembly projects - no CS0121 ambiguity!
383+
384+
// Example: Call GetOrAdd first, then Get benefits from cache
385+
var dbFromAdd = services.GetOrAddDatabaseOptionsFromDomain(configuration); // Populates cache
386+
var dbFromGet = services.GetDatabaseOptionsFromDomain(configuration); // Uses cache
387+
// dbFromAdd == dbFromGet (true - Get found it in cache)
388+
389+
// Use options to make conditional registration decisions
390+
if (dbFromAdd.EnableFeatureX)
391+
{
392+
services.AddScoped<IFeatureX, FeatureXService>();
393+
}
394+
395+
// Normal AddOptionsFrom* methods register with service collection
396+
services.AddOptionsFromDomain(configuration);
397+
// Options available via IOptions<T>, IOptionsSnapshot<T>, IOptionsMonitor<T>
398+
```
399+
400+
**How the Smart Dispatcher Works:**
401+
- **Library assemblies** (no OptionsBinding references): Don't generate `GetOptions<T>()` - use assembly-specific methods
402+
- **Consuming assemblies** (with OptionsBinding references): Generate smart dispatcher that routes based on type:
403+
```csharp
404+
public static T GetOptions<T>(...)
405+
{
406+
var type = typeof(T);
407+
408+
// Current assembly options
409+
if (type == typeof(DatabaseOptions))
410+
return (T)(object)services.GetDatabaseOptionsFromOptionsBinding(configuration);
411+
412+
// Referenced assembly options
413+
if (type == typeof(CacheOptions))
414+
return (T)(object)services.GetCacheOptionsFromDomain(configuration);
415+
416+
throw new InvalidOperationException($"Type '{type.FullName}' is not registered...");
417+
}
418+
```
419+
- **Result**: No CS0121 ambiguity, convenient generic API, compile-time type safety, no caching side effects!
352420

353421
**Generated Code Pattern:**
354422
```csharp

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,7 @@ services.AddOptionsFromApp(configuration);
354354
- **🔔 Configuration Change Callbacks**: Auto-generated IHostedService for OnChange notifications with Monitor lifetime - perfect for feature flags and runtime config updates
355355
- **🔧 Post-Configuration Support**: Normalize or transform values after binding with `PostConfigure` callbacks (e.g., ensure paths have trailing slashes, lowercase URLs)
356356
- **📛 Named Options**: Multiple configurations of the same options type with different names (e.g., Primary/Secondary email servers)
357+
- **⚡ Early Access to Options**: Retrieve bound and validated options during service registration without BuildServiceProvider() anti-pattern (via `GetOrAdd*` methods)
357358
- **🎯 Explicit Section Paths**: Support for nested sections like `"App:Database"` or `"Services:Email"`
358359
- **📂 Nested Subsection Binding**: Automatic binding of complex properties to configuration subsections (e.g., `StorageOptions.Database.Retry``"Storage:Database:Retry"`)
359360
- **📦 Multiple Options Classes**: Register multiple configuration sections in a single assembly with one method call

docs/OptionsBindingGenerators-FeatureRoadmap.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ This roadmap is based on comprehensive analysis of:
8484
|| [ConfigureAll Support](#7-configureall-support) | 🟢 Low-Medium |
8585
|| [Child Sections (Simplified Named Options)](#8-child-sections-simplified-named-options) | 🟢 Low-Medium |
8686
|| [Compile-Time Section Name Validation](#9-compile-time-section-name-validation) | 🟡 Medium |
87-
| | [Early Access to Options During Service Registration](#10-early-access-to-options-during-service-registration) | 🔴 High |
87+
| | [Early Access to Options During Service Registration](#10-early-access-to-options-during-service-registration) | 🔴 High |
8888
|| [Auto-Generate Options Classes from appsettings.json](#11-auto-generate-options-classes-from-appsettingsjson) | 🟢 Low |
8989
|| [Environment-Specific Validation](#12-environment-specific-validation) | 🟢 Low |
9090
|| [Hot Reload Support with Filtering](#13-hot-reload-support-with-filtering) | 🟢 Low |
@@ -863,9 +863,16 @@ public partial class NotificationOptions
863863
### 10. Early Access to Options During Service Registration
864864

865865
**Priority**: 🔴 **High***Avoids BuildServiceProvider anti-pattern*
866-
**Status**: ❌ Not Implemented
866+
**Status**: **Implemented**
867867
**Inspiration**: [StackOverflow: Avoid BuildServiceProvider](https://stackoverflow.com/questions/66263977/how-to-avoid-using-using-buildserviceprovider-method-at-multiple-places)
868868

869+
> **📝 Implementation Note:** This feature is fully implemented with three APIs:
870+
> 1. `Get[Type]From[Assembly]()` - Reads cache, doesn't populate (efficient, no side effects)
871+
> 2. `GetOrAdd[Type]From[Assembly]()` - Reads AND populates cache (idempotent)
872+
> 3. `GetOptions<T>()` - Smart dispatcher for multi-assembly projects (calls Get internally)
873+
>
874+
> See [OptionsBindingGenerators.md](OptionsBindingGenerators.md#-early-access-to-options-avoid-buildserviceprovider-anti-pattern) for current usage.
875+
869876
**Description**: Enable access to bound and validated options instances **during** service registration without calling `BuildServiceProvider()`, which is a known anti-pattern that causes memory leaks, scope issues, and application instability.
870877

871878
**User Story**:
@@ -1636,7 +1643,7 @@ Based on priority, user demand, and implementation complexity:
16361643
| ConfigureAll | 🟢 Low-Med || Low | 1.2 ||
16371644
| Nested Object Binding | 🟡 Medium | ⭐⭐ | Low | 1.3 ||
16381645
| Child Sections | 🟢 Low-Med | ⭐⭐ | Low | 1.3 ||
1639-
| **Early Access to Options** | 🔴 **High** | ⭐⭐⭐ | **Medium-High** | **1.4** | |
1646+
| **Early Access to Options** | 🔴 **High** | ⭐⭐⭐ | **Medium-High** | **1.4** | |
16401647
| Section Path Validation | 🟡 Medium | ⭐⭐ | High | 2.0+ ||
16411648
| Environment Validation | 🟢 Low || Medium | 2.0+ ||
16421649
| Hot Reload Filtering | 🟢 Low || Medium | 2.0+ ||

docs/OptionsBindingGenerators-Samples.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ This sample demonstrates the **OptionsBindingGenerator** in a multi-project cons
1414
- **Configuration change callbacks** - Automatic OnChange notifications with Monitor lifetime
1515
- **Child sections** - Simplified syntax for multiple named configurations (Email → Primary/Secondary/Fallback)
1616
- **Nested subsection binding** - Automatic binding of complex properties to configuration subsections
17+
- **Early access to options** - Retrieve options during service registration without BuildServiceProvider() anti-pattern
1718

1819
## 📁 Sample Projects
1920

@@ -1124,11 +1125,13 @@ The **OptionsBindingGenerator** automatically handles nested configuration subse
11241125
### 🎯 How It Works
11251126

11261127
When you have properties that are complex types (not primitives like string, int, etc.), the configuration binder automatically:
1128+
11271129
1. Detects the property is a complex type
11281130
2. Looks for a subsection with the same name
11291131
3. Recursively binds that subsection to the property
11301132

11311133
This works for:
1134+
11321135
- **Nested objects** - Properties with custom class types
11331136
- **Collections** - List<T>, IEnumerable<T>, arrays
11341137
- **Dictionaries** - Dictionary<string, string>, Dictionary<string, T>

0 commit comments

Comments
 (0)