Skip to content

Commit 9e63154

Browse files
committed
feat: extend support for ConfigureAll OptionsBinding
1 parent 490a820 commit 9e63154

File tree

14 files changed

+938
-24
lines changed

14 files changed

+938
-24
lines changed

CLAUDE.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,7 @@ services.AddDependencyRegistrationsFromDomain(
300300
- Supports validation: `ValidateDataAnnotations`, `ValidateOnStart`, `ErrorOnMissingKeys` (fail-fast for missing sections), Custom validators (`IValidateOptions<T>`)
301301
- **Configuration change callbacks**: Auto-generated IHostedService for OnChange notifications with Monitor lifetime - perfect for feature flags and runtime configuration updates
302302
- **Post-configuration support**: `PostConfigure` callbacks for normalizing/transforming values after binding (e.g., path normalization, URL lowercase)
303+
- **ConfigureAll support**: Set common default values for all named options instances before individual binding with `ConfigureAll` callbacks (e.g., baseline retry/timeout settings)
303304
- **Named options support**: Multiple configurations of the same options type with different names (e.g., Primary/Secondary email servers)
304305
- **Nested subsection binding**: Automatic binding of complex properties to configuration subsections (e.g., `StorageOptions.Database.Retry``"Storage:Database:Retry"`) - supported out-of-the-box by Microsoft's `.Bind()` method
305306
- Supports lifetime selection: Singleton (IOptions), Scoped (IOptionsSnapshot), Monitor (IOptionsMonitor)
@@ -429,6 +430,31 @@ public partial class StorageOptions
429430
services.AddOptions<StorageOptions>()
430431
.Bind(configuration.GetSection("Storage"))
431432
.PostConfigure(options => StorageOptions.NormalizePaths(options));
433+
434+
// Input with ConfigureAll (set defaults for all named instances):
435+
[OptionsBinding("Email:Primary", Name = "Primary", ConfigureAll = nameof(SetDefaults))]
436+
[OptionsBinding("Email:Secondary", Name = "Secondary")]
437+
[OptionsBinding("Email:Fallback", Name = "Fallback")]
438+
public partial class EmailOptions
439+
{
440+
public string SmtpServer { get; set; } = string.Empty;
441+
public int Port { get; set; } = 587;
442+
public int MaxRetries { get; set; }
443+
public int TimeoutSeconds { get; set; } = 30;
444+
445+
internal static void SetDefaults(EmailOptions options)
446+
{
447+
options.MaxRetries = 3;
448+
options.TimeoutSeconds = 30;
449+
options.Port = 587;
450+
}
451+
}
452+
453+
// Output with ConfigureAll (runs BEFORE individual configurations):
454+
services.ConfigureAll<EmailOptions>(options => EmailOptions.SetDefaults(options));
455+
services.Configure<EmailOptions>("Primary", configuration.GetSection("Email:Primary"));
456+
services.Configure<EmailOptions>("Secondary", configuration.GetSection("Email:Secondary"));
457+
services.Configure<EmailOptions>("Fallback", configuration.GetSection("Email:Fallback"));
432458
```
433459

434460
**Smart Naming:**
@@ -466,6 +492,9 @@ services.AddOptionsFromDomain(configuration, "DataAccess", "Infrastructure");
466492
- `ATCOPT008` - PostConfigure not supported with named options (Error)
467493
- `ATCOPT009` - PostConfigure callback method not found (Error)
468494
- `ATCOPT010` - PostConfigure callback has invalid signature (Error)
495+
- `ATCOPT011` - ConfigureAll requires multiple named options (Error)
496+
- `ATCOPT012` - ConfigureAll callback method not found (Error)
497+
- `ATCOPT013` - ConfigureAll callback has invalid signature (Error)
469498

470499
### MappingGenerator
471500

docs/OptionsBindingGenerators-FeatureRoadmap.md

Lines changed: 51 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ This roadmap is based on comprehensive analysis of:
7979
|| [Error on Missing Configuration Keys](#4-error-on-missing-configuration-keys) | 🔴 High |
8080
|| [Configuration Change Callbacks](#5-configuration-change-callbacks) | 🟡 Medium |
8181
|| [Bind Configuration Subsections to Properties](#6-bind-configuration-subsections-to-properties) | 🟡 Medium |
82-
| | [ConfigureAll Support](#7-configureall-support) | 🟢 Low-Medium |
82+
| | [ConfigureAll Support](#7-configureall-support) | 🟢 Low-Medium |
8383
|| [Options Snapshots for Specific Sections](#8-options-snapshots-for-specific-sections) | 🟢 Low-Medium |
8484
|| [Compile-Time Section Name Validation](#9-compile-time-section-name-validation) | 🟡 Medium |
8585
|| [Auto-Generate Options Classes from appsettings.json](#10-auto-generate-options-classes-from-appsettingsjson) | 🟢 Low |
@@ -599,28 +599,66 @@ public class DatabaseRetryPolicy
599599
### 7. ConfigureAll Support
600600

601601
**Priority**: 🟢 **Low-Medium**
602-
**Status**: ❌ Not Implemented
602+
**Status**: **Implemented**
603603

604-
**Description**: Support configuring all named instances of an options type at once (e.g., setting defaults).
604+
**Description**: Support configuring all named instances of an options type at once, allowing you to set common defaults that apply to all named configurations before individual settings override them.
605605

606606
**Example**:
607607

608608
```csharp
609-
// Configure defaults for ALL named DatabaseOptions instances
610-
services.ConfigureAll<DatabaseOptions>(options =>
609+
[OptionsBinding("Email:Primary", Name = "Primary", ConfigureAll = nameof(SetDefaults))]
610+
[OptionsBinding("Email:Secondary", Name = "Secondary")]
611+
[OptionsBinding("Email:Fallback", Name = "Fallback")]
612+
public partial class EmailOptions
611613
{
612-
options.MaxRetries = 3; // Default for all instances
613-
options.CommandTimeout = TimeSpan.FromSeconds(30);
614-
});
614+
public string SmtpServer { get; set; } = string.Empty;
615+
public int Port { get; set; } = 587;
616+
public int MaxRetries { get; set; }
617+
public int TimeoutSeconds { get; set; } = 30;
615618

616-
// Named instances override specific values
617-
services.Configure<DatabaseOptions>("Primary", config.GetSection("Databases:Primary"));
619+
internal static void SetDefaults(EmailOptions options)
620+
{
621+
// Set common defaults for ALL email configurations
622+
options.MaxRetries = 3;
623+
options.TimeoutSeconds = 30;
624+
options.Port = 587;
625+
}
626+
}
618627
```
619628

620-
**Implementation Notes**:
629+
**Generated Code**:
630+
631+
```csharp
632+
// Configure defaults for ALL named instances FIRST
633+
services.ConfigureAll<EmailOptions>(options => EmailOptions.SetDefaults(options));
634+
635+
// Then configure individual instances (can override defaults)
636+
services.Configure<EmailOptions>("Primary", config.GetSection("Email:Primary"));
637+
services.Configure<EmailOptions>("Secondary", config.GetSection("Email:Secondary"));
638+
services.Configure<EmailOptions>("Fallback", config.GetSection("Email:Fallback"));
639+
```
640+
641+
**Implementation Details**:
642+
643+
-**Requires multiple named instances** - Cannot be used with single unnamed instance (compile-time error)
644+
-**Method signature validation** - Must be `static void MethodName(TOptions options)`
645+
-**Execution order** - ConfigureAll runs BEFORE individual Configure calls
646+
-**Flexible placement** - Can be specified on any one of the `[OptionsBinding]` attributes
647+
-**Override support** - Individual configurations can override defaults set by ConfigureAll
648+
-**Compile-time safety** - Diagnostics ATCOPT011-013 validate usage and method signature
649+
650+
**Use Cases**:
651+
652+
- **Baseline settings**: Set common retry, timeout, or connection defaults across all database connections
653+
- **Feature flags**: Enable/disable common features for all tenant configurations
654+
- **Security defaults**: Apply consistent security settings across all API client configurations
655+
- **Notification channels**: Set common rate limits and retry policies for all notification providers
656+
657+
**Testing**:
621658

622-
- Generate `ConfigureAll<T>()` call when multiple named instances exist
623-
- Useful for setting defaults across all instances
659+
- ✅ 14 comprehensive unit tests covering all scenarios
660+
- ✅ Sample project: EmailOptions demonstrates default retry/timeout settings
661+
- ✅ PetStore.Api sample: NotificationOptions demonstrates common defaults for Email/SMS/Push channels
624662

625663
---
626664

docs/OptionsBindingGenerators-Samples.md

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -697,9 +697,9 @@ The generator enforces these rules at compile time:
697697

698698
## 📛 Named Options Support
699699

700-
**Named Options** allow multiple configurations of the same options type with different names - perfect for fallback scenarios, multi-tenant applications, or multi-region deployments.
700+
**Named Options** allow multiple configurations of the same options type with different names - perfect for fallback scenarios, multi-tenant applications, or multi-region deployments. You can also use **ConfigureAll** to set common defaults for all named instances.
701701

702-
### 🎯 Example: Email Server Fallback
702+
### 🎯 Example: Email Server Fallback with ConfigureAll
703703

704704
**Options Class:**
705705

@@ -708,7 +708,7 @@ using Atc.SourceGenerators.Annotations;
708708

709709
namespace Atc.SourceGenerators.OptionsBinding.Options;
710710

711-
[OptionsBinding("Email:Primary", Name = "Primary")]
711+
[OptionsBinding("Email:Primary", Name = "Primary", ConfigureAll = nameof(SetDefaults))]
712712
[OptionsBinding("Email:Secondary", Name = "Secondary")]
713713
[OptionsBinding("Email:Fallback", Name = "Fallback")]
714714
public partial class EmailOptions
@@ -718,6 +718,16 @@ public partial class EmailOptions
718718
public bool UseSsl { get; set; } = true;
719719
public string FromAddress { get; set; } = string.Empty;
720720
public int TimeoutSeconds { get; set; } = 30;
721+
public int MaxRetries { get; set; }
722+
723+
internal static void SetDefaults(EmailOptions options)
724+
{
725+
// Set common defaults for ALL email configurations
726+
options.UseSsl = true;
727+
options.TimeoutSeconds = 30;
728+
options.MaxRetries = 3;
729+
options.Port = 587;
730+
}
721731
}
722732
```
723733

@@ -763,6 +773,9 @@ public static class ServiceCollectionExtensions
763773
this IServiceCollection services,
764774
IConfiguration configuration)
765775
{
776+
// Configure defaults for ALL named instances of EmailOptions
777+
services.ConfigureAll<EmailOptions>(options => EmailOptions.SetDefaults(options));
778+
766779
// Configure EmailOptions (Named: "Primary")
767780
services.Configure<EmailOptions>("Primary", configuration.GetSection("Email:Primary"));
768781

docs/OptionsBindingGenerators.md

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -746,6 +746,7 @@ Console.WriteLine($"Other interval: {otherOptions.Value.RepeatIntervalInSeconds}
746746
- **🚨 Error on missing keys** - Fail-fast validation when configuration sections are missing (`ErrorOnMissingKeys`) to catch deployment issues at startup
747747
- **🔔 Configuration change callbacks** - Automatically respond to configuration changes at runtime with `OnChange` callbacks (requires Monitor lifetime)
748748
- **🔧 Post-configuration support** - Normalize or transform values after binding with `PostConfigure` callbacks (e.g., ensure paths have trailing slashes, lowercase URLs)
749+
- **🎛️ ConfigureAll support** - Set common default values for all named options instances before individual binding with `ConfigureAll` callbacks (e.g., baseline retry/timeout settings)
749750
- **📛 Named options** - Multiple configurations of the same options type with different names (e.g., Primary/Secondary email servers)
750751
- **🎯 Explicit section paths** - Support for nested sections like `"App:Database"` or `"Services:Email"`
751752
- **📂 Nested subsection binding** - Automatically bind complex properties to configuration subsections (e.g., `StorageOptions.Database.Retry``"Storage:Database:Retry"`)
@@ -1478,6 +1479,141 @@ The generator performs compile-time validation of PostConfigure callbacks:
14781479

14791480
---
14801481

1482+
### 🎛️ ConfigureAll Support
1483+
1484+
Set default values for **all named options instances** before individual configuration binding. This feature is perfect for establishing common baseline settings across multiple named configurations that can then be selectively overridden.
1485+
1486+
**Requirements:**
1487+
- Requires multiple named instances (at least 2)
1488+
- Callback method must have signature: `static void MethodName(TOptions options)`
1489+
- Runs **before** individual `Configure()` calls
1490+
1491+
**Basic Example:**
1492+
1493+
```csharp
1494+
[OptionsBinding("Email:Primary", Name = "Primary", ConfigureAll = nameof(SetDefaults))]
1495+
[OptionsBinding("Email:Secondary", Name = "Secondary")]
1496+
[OptionsBinding("Email:Fallback", Name = "Fallback")]
1497+
public partial class EmailOptions
1498+
{
1499+
public string SmtpServer { get; set; } = string.Empty;
1500+
public int Port { get; set; } = 587;
1501+
public bool UseSsl { get; set; } = true;
1502+
public int TimeoutSeconds { get; set; } = 30;
1503+
public int MaxRetries { get; set; }
1504+
1505+
internal static void SetDefaults(EmailOptions options)
1506+
{
1507+
// Set common defaults for ALL email configurations
1508+
options.UseSsl = true;
1509+
options.TimeoutSeconds = 30;
1510+
options.MaxRetries = 3;
1511+
options.Port = 587;
1512+
}
1513+
}
1514+
```
1515+
1516+
**Generated Code:**
1517+
1518+
The generator automatically calls `.ConfigureAll()` **before** individual configurations:
1519+
1520+
```csharp
1521+
// Configure defaults for ALL named instances FIRST
1522+
services.ConfigureAll<EmailOptions>(options => EmailOptions.SetDefaults(options));
1523+
1524+
// Then configure individual instances (can override defaults)
1525+
services.Configure<EmailOptions>("Primary", configuration.GetSection("Email:Primary"));
1526+
services.Configure<EmailOptions>("Secondary", configuration.GetSection("Email:Secondary"));
1527+
services.Configure<EmailOptions>("Fallback", configuration.GetSection("Email:Fallback"));
1528+
```
1529+
1530+
**Usage Scenarios:**
1531+
1532+
```csharp
1533+
// Notification channels - common defaults for all channels
1534+
[OptionsBinding("Notifications:Email", Name = "Email", ConfigureAll = nameof(SetCommonDefaults))]
1535+
[OptionsBinding("Notifications:SMS", Name = "SMS")]
1536+
[OptionsBinding("Notifications:Push", Name = "Push")]
1537+
public partial class NotificationOptions
1538+
{
1539+
public bool Enabled { get; set; }
1540+
public int TimeoutSeconds { get; set; } = 30;
1541+
public int MaxRetries { get; set; } = 3;
1542+
public int RateLimitPerMinute { get; set; }
1543+
1544+
internal static void SetCommonDefaults(NotificationOptions options)
1545+
{
1546+
// All notification channels start with these defaults
1547+
options.TimeoutSeconds = 30;
1548+
options.MaxRetries = 3;
1549+
options.RateLimitPerMinute = 60;
1550+
options.Enabled = true;
1551+
}
1552+
}
1553+
1554+
// Database connections - common retry and timeout defaults
1555+
[OptionsBinding("Database:Primary", Name = "Primary", ConfigureAll = nameof(SetConnectionDefaults))]
1556+
[OptionsBinding("Database:ReadReplica", Name = "ReadReplica")]
1557+
[OptionsBinding("Database:Analytics", Name = "Analytics")]
1558+
public partial class DatabaseConnectionOptions
1559+
{
1560+
public string ConnectionString { get; set; } = string.Empty;
1561+
public int MaxRetries { get; set; }
1562+
public int CommandTimeoutSeconds { get; set; }
1563+
public bool EnableRetry { get; set; }
1564+
1565+
internal static void SetConnectionDefaults(DatabaseConnectionOptions options)
1566+
{
1567+
// All database connections start with these baseline settings
1568+
options.MaxRetries = 3;
1569+
options.CommandTimeoutSeconds = 30;
1570+
options.EnableRetry = true;
1571+
}
1572+
}
1573+
```
1574+
1575+
**Validation Errors:**
1576+
1577+
The generator performs compile-time validation of ConfigureAll callbacks:
1578+
1579+
- **ATCOPT011**: ConfigureAll requires multiple named options
1580+
```csharp
1581+
// Error: ConfigureAll needs at least 2 named instances
1582+
[OptionsBinding("Settings", Name = "Default", ConfigureAll = nameof(SetDefaults))]
1583+
public partial class Settings { }
1584+
```
1585+
1586+
- **ATCOPT012**: ConfigureAll callback method not found
1587+
```csharp
1588+
// Error: Method 'SetDefaults' does not exist
1589+
[OptionsBinding("Email", Name = "Primary", ConfigureAll = "SetDefaults")]
1590+
[OptionsBinding("Email", Name = "Secondary")]
1591+
public partial class EmailOptions { }
1592+
```
1593+
1594+
- **ATCOPT013**: ConfigureAll callback method has invalid signature
1595+
```csharp
1596+
// Error: Must be static void with (TOptions) parameter
1597+
[OptionsBinding("Email", Name = "Primary", ConfigureAll = nameof(Configure))]
1598+
[OptionsBinding("Email", Name = "Secondary")]
1599+
public partial class EmailOptions
1600+
{
1601+
private void Configure() { } // Wrong: not static, missing parameter
1602+
}
1603+
```
1604+
1605+
**Important Notes:**
1606+
1607+
- ConfigureAll runs **before** individual named instance configurations
1608+
- Individual configurations can override defaults set by ConfigureAll
1609+
- Callback method can be `internal` or `public` (not `private`)
1610+
- **Requires multiple named instances** - cannot be used with single unnamed instance
1611+
- Perfect for establishing baseline settings across multiple configurations
1612+
- Order of execution: ConfigureAll → Configure("Name1") → Configure("Name2") → ...
1613+
- Can be specified on any one of the `[OptionsBinding]` attributes (only processed once)
1614+
1615+
---
1616+
14811617
## 🔧 How It Works
14821618

14831619
### 1️⃣ Attribute Detection
@@ -2052,6 +2188,12 @@ See [Post-Configuration Support](#-post-configuration-support) section for detai
20522188

20532189
---
20542190

2191+
### ❌ ATCOPT011-013: ConfigureAll Callback Diagnostics
2192+
2193+
See [ConfigureAll Support](#️-configureall-support) section for details.
2194+
2195+
---
2196+
20552197
## 🚀 Native AOT Compatibility
20562198

20572199
The Options Binding Generator is **fully compatible with Native AOT** compilation, producing code that meets all AOT requirements:

sample/Atc.SourceGenerators.OptionsBinding/Options/EmailOptions.cs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ namespace Atc.SourceGenerators.OptionsBinding.Options;
44
/// Email server options with support for multiple named configurations.
55
/// This class demonstrates the Named Options feature which allows the same options type
66
/// to be bound to different configuration sections using different names.
7+
/// It also demonstrates the ConfigureAll feature which sets default values for ALL named instances.
78
/// </summary>
8-
[OptionsBinding("Email:Primary", Name = "Primary")]
9+
[OptionsBinding("Email:Primary", Name = "Primary", ConfigureAll = nameof(SetDefaults))]
910
[OptionsBinding("Email:Secondary", Name = "Secondary")]
1011
[OptionsBinding("Email:Fallback", Name = "Fallback")]
1112
public partial class EmailOptions
@@ -34,4 +35,23 @@ public partial class EmailOptions
3435
/// Gets or sets the timeout in seconds.
3536
/// </summary>
3637
public int TimeoutSeconds { get; set; } = 30;
38+
39+
/// <summary>
40+
/// Gets or sets the maximum number of retry attempts.
41+
/// </summary>
42+
public int MaxRetries { get; set; }
43+
44+
/// <summary>
45+
/// Configures default values for ALL email instances.
46+
/// This method runs BEFORE individual configurations, allowing defaults to be set
47+
/// that can be overridden by specific configuration sections.
48+
/// </summary>
49+
internal static void SetDefaults(EmailOptions options)
50+
{
51+
// Set defaults for all email configurations
52+
options.UseSsl = true;
53+
options.TimeoutSeconds = 30;
54+
options.MaxRetries = 3;
55+
options.Port = 587;
56+
}
3757
}

0 commit comments

Comments
 (0)