Skip to content

Commit cd53286

Browse files
Improve early access to Options during Service-Registration (#9)
* docs: add Build-Time Requirements * docs: add reguest: Early Access to Options During Service Registration * feat: extend support for Early Access to Options During Service Registration * fix: add missing AlsoRegisterDirectType parameter after merge 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 8af73ae commit cd53286

File tree

17 files changed

+2474
-416
lines changed

17 files changed

+2474
-416
lines changed

CLAUDE.md

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,42 @@ dotnet run --project sample/PetStore.Api
8787
dotnet clean
8888
```
8989

90+
## Build Requirements
91+
92+
### SDK Version
93+
94+
This project uses **Roslyn 5.0.0 (.NET 10)** source generators and requires .NET 10 SDK for building.
95+
96+
**Consumer Projects:**
97+
- Projects that reference `Atc.SourceGenerators` must be built with .NET 10 SDK (or later)
98+
- Consumer projects can target ANY .NET version (.NET 9, .NET 8, .NET Framework, etc.)
99+
- This is a build-time requirement only - runtime target framework is independent
100+
101+
**Why .NET 10 SDK is required:**
102+
- Roslyn 5.0.0 APIs ship with .NET 10 SDK
103+
- Source generators execute during compilation, requiring the SDK's Roslyn version
104+
- Target framework and SDK version are independent concepts in .NET
105+
106+
**Example Scenario:**
107+
```xml
108+
<!-- Consumer project can target .NET 9 -->
109+
<Project Sdk="Microsoft.NET.Sdk">
110+
<PropertyGroup>
111+
<TargetFramework>net9.0</TargetFramework>
112+
</PropertyGroup>
113+
114+
<ItemGroup>
115+
<!-- Requires .NET 10 SDK to build due to Roslyn 5.0.0 -->
116+
<PackageReference Include="Atc.SourceGenerators" Version="1.0.0" />
117+
</ItemGroup>
118+
</Project>
119+
```
120+
121+
```bash
122+
# Must use .NET 10 SDK to build
123+
dotnet build # Executes source generators using Roslyn 5.0.0
124+
```
125+
90126
## Architecture
91127

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

318422
**Generated Code Pattern:**
319423
```csharp

README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,38 @@ Or in your `.csproj`:
9797

9898
**Note:** The generator emits fallback attributes automatically, so the Annotations package is optional. However, it provides better XML documentation and IntelliSense. If you include it, suppress the expected CS0436 warning: `<NoWarn>$(NoWarn);CS0436</NoWarn>`
9999

100+
## ⚙️ Requirements
101+
102+
### Build-Time Requirements
103+
104+
This package uses **Roslyn 5.0.0 (.NET 10)** for source generation. To build projects that consume this package:
105+
106+
**Required:**
107+
- **.NET 10 SDK** (or later)
108+
109+
**Important Notes:**
110+
- Projects targeting **.NET 9 (or earlier)** CAN successfully build using the .NET 10 SDK
111+
- This is a **build-time requirement only**, not a runtime requirement
112+
- Your application can still target and run on .NET 9, .NET 8, or earlier framework versions
113+
- The SDK version only affects the build process, not the target framework or runtime
114+
115+
**Example:**
116+
117+
```bash
118+
# Install .NET 10 SDK
119+
# Download from: https://dotnet.microsoft.com/download/dotnet/10.0
120+
121+
# Your project can still target .NET 9
122+
<TargetFramework>net9.0</TargetFramework>
123+
124+
# But requires .NET 10 SDK to build (due to Roslyn 5.0.0 source generator dependency)
125+
dotnet build # Must use .NET 10 SDK
126+
```
127+
128+
**Why .NET 10 SDK?**
129+
130+
The source generators in this package leverage Roslyn 5.0.0 APIs which ship with .NET 10. While your consuming applications can target any .NET version (including .NET 9, .NET 8, or .NET Framework), the build toolchain requires .NET 10 SDK for proper source generator execution.
131+
100132
---
101133

102134
### ⚡ DependencyRegistrationGenerator
@@ -322,6 +354,7 @@ services.AddOptionsFromApp(configuration);
322354
- **🔔 Configuration Change Callbacks**: Auto-generated IHostedService for OnChange notifications with Monitor lifetime - perfect for feature flags and runtime config updates
323355
- **🔧 Post-Configuration Support**: Normalize or transform values after binding with `PostConfigure` callbacks (e.g., ensure paths have trailing slashes, lowercase URLs)
324356
- **📛 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)
325358
- **🎯 Explicit Section Paths**: Support for nested sections like `"App:Database"` or `"Services:Email"`
326359
- **📂 Nested Subsection Binding**: Automatic binding of complex properties to configuration subsections (e.g., `StorageOptions.Database.Retry``"Storage:Database:Retry"`)
327360
- **📦 Multiple Options Classes**: Register multiple configuration sections in a single assembly with one method call

0 commit comments

Comments
 (0)