Skip to content

Commit b6b4d8b

Browse files
committed
feat: extend support for Named-Options OptionsBinding
1 parent b6e0ad8 commit b6b4d8b

File tree

23 files changed

+879
-82
lines changed

23 files changed

+879
-82
lines changed

β€ŽCLAUDE.mdβ€Ž

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,7 @@ services.AddDependencyRegistrationsFromDomain(
298298
4. `public const string Name`
299299
5. Auto-inferred from class name
300300
- Supports validation: `ValidateDataAnnotations`, `ValidateOnStart`, Custom validators (`IValidateOptions<T>`)
301+
- **Named options support**: Multiple configurations of the same options type with different names (e.g., Primary/Secondary email servers)
301302
- Supports lifetime selection: Singleton (IOptions), Scoped (IOptionsSnapshot), Monitor (IOptionsMonitor)
302303
- Requires classes to be declared `partial`
303304
- **Smart naming** - uses short suffix if unique, full name if conflicts exist
@@ -323,6 +324,22 @@ services.AddOptions<DatabaseOptions>()
323324
.ValidateOnStart();
324325

325326
services.AddSingleton<IValidateOptions<DatabaseOptions>, DatabaseOptionsValidator>();
327+
328+
// Input with named options (multiple configurations):
329+
[OptionsBinding("Email:Primary", Name = "Primary")]
330+
[OptionsBinding("Email:Secondary", Name = "Secondary")]
331+
[OptionsBinding("Email:Fallback", Name = "Fallback")]
332+
public partial class EmailOptions { }
333+
334+
// Output with named options:
335+
services.Configure<EmailOptions>("Primary", configuration.GetSection("Email:Primary"));
336+
services.Configure<EmailOptions>("Secondary", configuration.GetSection("Email:Secondary"));
337+
services.Configure<EmailOptions>("Fallback", configuration.GetSection("Email:Fallback"));
338+
339+
// Usage: Access via IOptionsSnapshot<T>.Get(name)
340+
var emailSnapshot = serviceProvider.GetRequiredService<IOptionsSnapshot<EmailOptions>>();
341+
var primaryEmail = emailSnapshot.Get("Primary");
342+
var secondaryEmail = emailSnapshot.Get("Secondary");
326343
```
327344

328345
**Smart Naming:**

β€ŽREADME.mdβ€Ž

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,8 @@ services.AddOptionsFromApp(configuration);
318318

319319
- **🧠 Automatic Section Name Inference**: Smart resolution from explicit names, const fields (`SectionName`, `NameTitle`, `Name`), or auto-inferred from class names
320320
- **πŸ”’ Built-in Validation**: Integrated DataAnnotations validation (`ValidateDataAnnotations`) and startup validation (`ValidateOnStart`)
321+
- **🎯 Custom Validation**: Support for `IValidateOptions<T>` for complex business rules beyond DataAnnotations
322+
- **πŸ“› Named Options**: Multiple configurations of the same options type with different names (e.g., Primary/Secondary email servers)
321323
- **🎯 Explicit Section Paths**: Support for nested sections like `"App:Database"` or `"Services:Email"`
322324
- **πŸ“¦ Multiple Options Classes**: Register multiple configuration sections in a single assembly with one method call
323325
- **πŸ—οΈ Multi-Project Support**: Smart naming generates assembly-specific extension methods (e.g., `AddOptionsFromDomain()`, `AddOptionsFromDataAccess()`)

β€Ždocs/OptionsBindingGenerators-FeatureRoadmap.mdβ€Ž

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ This roadmap is based on comprehensive analysis of:
5454

5555
- **Section name resolution** - 5-level priority system (explicit β†’ const SectionName β†’ const NameTitle β†’ const Name β†’ auto-inferred)
5656
- **Validation support** - `ValidateDataAnnotations` and `ValidateOnStart` parameters
57+
- **Custom validation** - `IValidateOptions<T>` for complex business rules beyond DataAnnotations
58+
- **Named options** - Multiple configurations of the same options type with different names
5759
- **Lifetime selection** - Singleton (`IOptions`), Scoped (`IOptionsSnapshot`), Monitor (`IOptionsMonitor`)
5860
- **Multi-project support** - Assembly-specific extension methods with smart naming
5961
- **Transitive registration** - 4 overloads for automatic/selective assembly registration
@@ -68,7 +70,7 @@ This roadmap is based on comprehensive analysis of:
6870
| Status | Feature | Priority |
6971
|:------:|---------|----------|
7072
| βœ… | [Custom Validation Support (IValidateOptions)](#1-custom-validation-support-ivalidateoptions) | πŸ”΄ High |
71-
| ❌ | [Named Options Support](#2-named-options-support) | πŸ”΄ High |
73+
| βœ… | [Named Options Support](#2-named-options-support) | πŸ”΄ High |
7274
| ❌ | [Post-Configuration Support](#3-post-configuration-support) | 🟑 Medium-High |
7375
| ❌ | [Error on Missing Configuration Keys](#4-error-on-missing-configuration-keys) | πŸ”΄ High |
7476
| ❌ | [Configuration Change Callbacks](#5-configuration-change-callbacks) | 🟑 Medium |
@@ -165,7 +167,7 @@ services.AddOptions<ConnectionPoolOptions>()
165167
### 2. Named Options Support
166168

167169
**Priority**: πŸ”΄ **High**
168-
**Status**: ❌ Not Implemented
170+
**Status**: βœ… **Implemented**
169171
**Inspiration**: Microsoft.Extensions.Options named options
170172

171173
**Description**: Support multiple configuration sections binding to the same options class with different names.
@@ -224,12 +226,19 @@ public class DataService
224226
}
225227
```
226228

227-
**Implementation Notes**:
229+
**Implementation Details**:
230+
231+
- βœ… `[OptionsBinding]` attribute supports `AllowMultiple = true`
232+
- βœ… Added `Name` property to distinguish named instances
233+
- βœ… Named options use `Configure<T>(name, section)` pattern
234+
- βœ… Named options accessed via `IOptionsSnapshot<T>.Get(name)`
235+
- βœ… Can mix named and unnamed options on the same class
236+
- ⚠️ Named options do NOT support validation chain (ValidateDataAnnotations, ValidateOnStart, Validator)
228237

229-
- Allow multiple `[OptionsBinding]` attributes on same class
230-
- Add `Name` parameter to distinguish named instances
231-
- Use `Configure<T>(string name, ...)` for named options
232-
- Generate helper properties/methods for easy access
238+
**Testing**:
239+
- βœ… 8 comprehensive unit tests covering all scenarios
240+
- βœ… Sample project with EmailOptions demonstrating Primary/Secondary/Fallback servers
241+
- βœ… PetStore.Api sample with NotificationOptions (Email/SMS/Push channels)
233242

234243
---
235244

β€Ždocs/OptionsBindingGenerators-Samples.mdβ€Ž

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,145 @@ public partial class LoggingOptions { }
447447
// Inject: IOptionsMonitor<LoggingOptions>
448448
```
449449

450+
## πŸ“› Named Options Support
451+
452+
**Named Options** allow multiple configurations of the same options type with different names - perfect for fallback scenarios, multi-tenant applications, or multi-region deployments.
453+
454+
### 🎯 Example: Email Server Fallback
455+
456+
**Options Class:**
457+
458+
```csharp
459+
using Atc.SourceGenerators.Annotations;
460+
461+
namespace Atc.SourceGenerators.OptionsBinding.Options;
462+
463+
[OptionsBinding("Email:Primary", Name = "Primary")]
464+
[OptionsBinding("Email:Secondary", Name = "Secondary")]
465+
[OptionsBinding("Email:Fallback", Name = "Fallback")]
466+
public partial class EmailOptions
467+
{
468+
public string SmtpServer { get; set; } = string.Empty;
469+
public int Port { get; set; } = 587;
470+
public bool UseSsl { get; set; } = true;
471+
public string FromAddress { get; set; } = string.Empty;
472+
public int TimeoutSeconds { get; set; } = 30;
473+
}
474+
```
475+
476+
**Configuration (appsettings.json):**
477+
478+
```json
479+
{
480+
"Email": {
481+
"Primary": {
482+
"SmtpServer": "smtp.primary.example.com",
483+
"Port": 587,
484+
"UseSsl": true,
485+
"FromAddress": "noreply@primary.example.com",
486+
"TimeoutSeconds": 30
487+
},
488+
"Secondary": {
489+
"SmtpServer": "smtp.secondary.example.com",
490+
"Port": 587,
491+
"UseSsl": true,
492+
"FromAddress": "noreply@secondary.example.com",
493+
"TimeoutSeconds": 45
494+
},
495+
"Fallback": {
496+
"SmtpServer": "smtp.fallback.example.com",
497+
"Port": 25,
498+
"UseSsl": false,
499+
"FromAddress": "noreply@fallback.example.com",
500+
"TimeoutSeconds": 60
501+
}
502+
}
503+
}
504+
```
505+
506+
**Generated Code:**
507+
508+
```csharp
509+
// <auto-generated />
510+
namespace Atc.SourceGenerators.Annotations;
511+
512+
public static class ServiceCollectionExtensions
513+
{
514+
public static IServiceCollection AddOptionsFromOptionsBinding(
515+
this IServiceCollection services,
516+
IConfiguration configuration)
517+
{
518+
// Configure EmailOptions (Named: "Primary")
519+
services.Configure<EmailOptions>("Primary", configuration.GetSection("Email:Primary"));
520+
521+
// Configure EmailOptions (Named: "Secondary")
522+
services.Configure<EmailOptions>("Secondary", configuration.GetSection("Email:Secondary"));
523+
524+
// Configure EmailOptions (Named: "Fallback")
525+
services.Configure<EmailOptions>("Fallback", configuration.GetSection("Email:Fallback"));
526+
527+
return services;
528+
}
529+
}
530+
```
531+
532+
**Usage in Services:**
533+
534+
```csharp
535+
public class EmailService
536+
{
537+
private readonly IOptionsSnapshot<EmailOptions> _emailSnapshot;
538+
539+
public EmailService(IOptionsSnapshot<EmailOptions> emailSnapshot)
540+
{
541+
_emailSnapshot = emailSnapshot;
542+
}
543+
544+
public async Task SendAsync(string to, string body)
545+
{
546+
// Try primary first
547+
var primaryEmail = _emailSnapshot.Get("Primary");
548+
if (await TrySendAsync(primaryEmail, to, body))
549+
return;
550+
551+
// Fallback to secondary
552+
var secondaryEmail = _emailSnapshot.Get("Secondary");
553+
if (await TrySendAsync(secondaryEmail, to, body))
554+
return;
555+
556+
// Last resort: fallback server
557+
var fallbackEmail = _emailSnapshot.Get("Fallback");
558+
await TrySendAsync(fallbackEmail, to, body);
559+
}
560+
561+
private async Task<bool> TrySendAsync(EmailOptions options, string to, string body)
562+
{
563+
try
564+
{
565+
// Send email using options.SmtpServer, options.Port, etc.
566+
return true;
567+
}
568+
catch
569+
{
570+
return false;
571+
}
572+
}
573+
}
574+
```
575+
576+
### ⚠️ Important Notes
577+
578+
- **Use `IOptionsSnapshot<T>`**: Named options are accessed via `IOptionsSnapshot<T>.Get(name)`, not `IOptions<T>.Value`
579+
- **No Validation Chain**: Named options use the simpler `Configure<T>(name, section)` pattern without validation support
580+
- **AllowMultiple**: The `[OptionsBinding]` attribute supports multiple instances on the same class
581+
582+
### 🎯 Common Use Cases
583+
584+
- **Fallback Servers**: Primary/Secondary/Fallback email or database servers
585+
- **Multi-Region**: Different API endpoints for US, EU, Asia regions
586+
- **Multi-Tenant**: Tenant-specific configurations
587+
- **Environment Tiers**: Production, Staging, Development endpoints
588+
450589
## ✨ Key Takeaways
451590

452591
1. **Zero Boilerplate**: No manual `AddOptions().Bind()` code to write
@@ -456,6 +595,7 @@ public partial class LoggingOptions { }
456595
5. **Multi-Project**: Each project gets its own `AddOptionsFromXXX()` method
457596
6. **Flexible Lifetimes**: Choose between Singleton, Scoped, or Monitor
458597
7. **Startup Validation**: Catch configuration errors before runtime
598+
8. **Named Options**: Multiple configurations of the same type for fallback/multi-tenant scenarios
459599

460600
## πŸ”— Related Documentation
461601

β€Ždocs/OptionsBindingGenerators.mdβ€Ž

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -741,6 +741,7 @@ Console.WriteLine($"Other interval: {otherOptions.Value.RepeatIntervalInSeconds}
741741
- **🧠 Automatic section name inference** - Smart resolution from explicit names, const fields (`SectionName`, `NameTitle`, `Name`), or auto-inferred from class names
742742
- **πŸ”’ Built-in validation** - Integrated DataAnnotations validation (`ValidateDataAnnotations`) and startup validation (`ValidateOnStart`)
743743
- **🎯 Custom validation** - Support for `IValidateOptions<T>` for complex business rules beyond DataAnnotations
744+
- **πŸ“› Named options** - Multiple configurations of the same options type with different names (e.g., Primary/Secondary email servers)
744745
- **🎯 Explicit section paths** - Support for nested sections like `"App:Database"` or `"Services:Email"`
745746
- **πŸ“¦ Multiple options classes** - Register multiple configuration sections in a single assembly with one method call
746747
- **πŸ—οΈ Multi-project support** - Smart naming generates assembly-specific extension methods (e.g., `AddOptionsFromDomain()`, `AddOptionsFromDataAccess()`)
@@ -1270,6 +1271,133 @@ var configuration = new ConfigurationBuilder()
12701271
services.AddOptionsFromApp(configuration);
12711272
```
12721273

1274+
### πŸ“› Named Options (Multiple Configurations)
1275+
1276+
**Named Options** allow you to have multiple configurations of the same options type with different names. This is useful when you need different configurations for the same logical service (e.g., Primary/Secondary email servers, Production/Staging databases).
1277+
1278+
#### ✨ Use Cases
1279+
1280+
- **πŸ”„ Fallback Servers**: Primary, Secondary, and Fallback email/database servers
1281+
- **🌍 Multi-Region**: Different API endpoints for different regions (US, EU, Asia)
1282+
- **🎯 Multi-Tenant**: Tenant-specific configurations
1283+
- **πŸ”§ Environment Tiers**: Production, Staging, Development endpoints
1284+
1285+
#### 🎯 Basic Example
1286+
1287+
**Define options with multiple named instances:**
1288+
1289+
```csharp
1290+
[OptionsBinding("Email:Primary", Name = "Primary")]
1291+
[OptionsBinding("Email:Secondary", Name = "Secondary")]
1292+
[OptionsBinding("Email:Fallback", Name = "Fallback")]
1293+
public partial class EmailOptions
1294+
{
1295+
public string SmtpServer { get; set; } = string.Empty;
1296+
public int Port { get; set; } = 587;
1297+
public bool UseSsl { get; set; } = true;
1298+
public string FromAddress { get; set; } = string.Empty;
1299+
}
1300+
```
1301+
1302+
**Configure appsettings.json:**
1303+
1304+
```json
1305+
{
1306+
"Email": {
1307+
"Primary": {
1308+
"SmtpServer": "smtp.primary.example.com",
1309+
"Port": 587,
1310+
"UseSsl": true,
1311+
"FromAddress": "noreply@primary.example.com"
1312+
},
1313+
"Secondary": {
1314+
"SmtpServer": "smtp.secondary.example.com",
1315+
"Port": 587,
1316+
"UseSsl": true,
1317+
"FromAddress": "noreply@secondary.example.com"
1318+
},
1319+
"Fallback": {
1320+
"SmtpServer": "smtp.fallback.example.com",
1321+
"Port": 25,
1322+
"UseSsl": false,
1323+
"FromAddress": "noreply@fallback.example.com"
1324+
}
1325+
}
1326+
}
1327+
```
1328+
1329+
**Access named options using IOptionsSnapshot:**
1330+
1331+
```csharp
1332+
public class EmailService
1333+
{
1334+
private readonly IOptionsSnapshot<EmailOptions> _emailOptionsSnapshot;
1335+
1336+
public EmailService(IOptionsSnapshot<EmailOptions> emailOptionsSnapshot)
1337+
{
1338+
_emailOptionsSnapshot = emailOptionsSnapshot;
1339+
}
1340+
1341+
public async Task SendAsync(string to, string body)
1342+
{
1343+
// Try primary first
1344+
var primaryOptions = _emailOptionsSnapshot.Get("Primary");
1345+
if (await TrySendAsync(primaryOptions, to, body))
1346+
return;
1347+
1348+
// Fallback to secondary
1349+
var secondaryOptions = _emailOptionsSnapshot.Get("Secondary");
1350+
if (await TrySendAsync(secondaryOptions, to, body))
1351+
return;
1352+
1353+
// Last resort: fallback server
1354+
var fallbackOptions = _emailOptionsSnapshot.Get("Fallback");
1355+
await TrySendAsync(fallbackOptions, to, body);
1356+
}
1357+
}
1358+
```
1359+
1360+
#### πŸ”§ Generated Code
1361+
1362+
```csharp
1363+
// Generated registration methods
1364+
services.Configure<EmailOptions>("Primary", configuration.GetSection("Email:Primary"));
1365+
services.Configure<EmailOptions>("Secondary", configuration.GetSection("Email:Secondary"));
1366+
services.Configure<EmailOptions>("Fallback", configuration.GetSection("Email:Fallback"));
1367+
```
1368+
1369+
#### ⚠️ Important Notes
1370+
1371+
- **πŸ“ Use `IOptionsSnapshot<T>`**: Named options are accessed via `IOptionsSnapshot<T>.Get(name)`, not `IOptions<T>.Value`
1372+
- **🚫 No Validation Chain**: Named options use the simpler `Configure<T>(name, section)` pattern without validation support
1373+
- **πŸ”„ AllowMultiple**: The `[OptionsBinding]` attribute supports `AllowMultiple = true` to enable multiple configurations
1374+
1375+
#### 🎯 Mixing Named and Unnamed Options
1376+
1377+
You can have both named and unnamed options on the same class:
1378+
1379+
```csharp
1380+
// Default unnamed instance
1381+
[OptionsBinding("Email")]
1382+
1383+
// Named instances for specific use cases
1384+
[OptionsBinding("Email:Backup", Name = "Backup")]
1385+
public partial class EmailOptions
1386+
{
1387+
public string SmtpServer { get; set; } = string.Empty;
1388+
public int Port { get; set; } = 587;
1389+
}
1390+
```
1391+
1392+
```csharp
1393+
// Access default (unnamed) instance
1394+
var defaultEmail = serviceProvider.GetRequiredService<IOptions<EmailOptions>>();
1395+
1396+
// Access named instances
1397+
var emailSnapshot = serviceProvider.GetRequiredService<IOptionsSnapshot<EmailOptions>>();
1398+
var backupEmail = emailSnapshot.Get("Backup");
1399+
```
1400+
12731401
---
12741402

12751403
## πŸ›‘οΈ Diagnostics

β€Ždocs/PetStoreApi-Samples.mdβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ graph TB
106106

107107
### Project References (Clean Architecture)
108108

109-
```
109+
```text
110110
PetStore.Api
111111
β”œβ”€β”€ PetStore.Domain
112112
β”‚ β”œβ”€β”€ PetStore.DataAccess (NO Api.Contract reference)

0 commit comments

Comments
Β (0)