Skip to content

Commit 04f2c1b

Browse files
committed
feat: extend support for Error on Missing Configuration Keys OptionsBinding
1 parent b6b4d8b commit 04f2c1b

File tree

17 files changed

+497
-39
lines changed

17 files changed

+497
-39
lines changed

CLAUDE.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -297,7 +297,7 @@ services.AddDependencyRegistrationsFromDomain(
297297
3. `public const string NameTitle`
298298
4. `public const string Name`
299299
5. Auto-inferred from class name
300-
- Supports validation: `ValidateDataAnnotations`, `ValidateOnStart`, Custom validators (`IValidateOptions<T>`)
300+
- Supports validation: `ValidateDataAnnotations`, `ValidateOnStart`, `ErrorOnMissingKeys` (fail-fast for missing sections), Custom validators (`IValidateOptions<T>`)
301301
- **Named options support**: Multiple configurations of the same options type with different names (e.g., Primary/Secondary email servers)
302302
- Supports lifetime selection: Singleton (IOptions), Scoped (IOptionsSnapshot), Monitor (IOptionsMonitor)
303303
- Requires classes to be declared `partial`
@@ -325,6 +325,27 @@ services.AddOptions<DatabaseOptions>()
325325

326326
services.AddSingleton<IValidateOptions<DatabaseOptions>, DatabaseOptionsValidator>();
327327

328+
// Input with ErrorOnMissingKeys (fail-fast for missing configuration):
329+
[OptionsBinding("Database", ErrorOnMissingKeys = true, ValidateOnStart = true)]
330+
public partial class DatabaseOptions { }
331+
332+
// Output with ErrorOnMissingKeys:
333+
services.AddOptions<DatabaseOptions>()
334+
.Bind(configuration.GetSection("Database"))
335+
.Validate(options =>
336+
{
337+
var section = configuration.GetSection("Database");
338+
if (!section.Exists())
339+
{
340+
throw new global::System.InvalidOperationException(
341+
"Configuration section 'Database' is missing. " +
342+
"Ensure the section exists in your appsettings.json or other configuration sources.");
343+
}
344+
345+
return true;
346+
})
347+
.ValidateOnStart();
348+
328349
// Input with named options (multiple configurations):
329350
[OptionsBinding("Email:Primary", Name = "Primary")]
330351
[OptionsBinding("Email:Secondary", Name = "Secondary")]

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ Get errors at compile time, not runtime:
259259

260260
### ⚙️ OptionsBindingGenerator
261261

262-
Eliminate boilerplate configuration binding code. Decorate your options classes with `[OptionsBinding]` and let the generator create type-safe configuration bindings automatically. Supports DataAnnotations validation, startup validation, and custom `IValidateOptions<T>` validators for complex business rules.
262+
Eliminate boilerplate configuration binding code. Decorate your options classes with `[OptionsBinding]` and let the generator create type-safe configuration bindings automatically. Supports DataAnnotations validation, startup validation, fail-fast validation for missing configuration sections (`ErrorOnMissingKeys`), and custom `IValidateOptions<T>` validators for complex business rules.
263263

264264
#### 📚 Documentation
265265

docs/OptionsBindingGenerators-FeatureRoadmap.md

Lines changed: 37 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ This roadmap is based on comprehensive analysis of:
5656
- **Validation support** - `ValidateDataAnnotations` and `ValidateOnStart` parameters
5757
- **Custom validation** - `IValidateOptions<T>` for complex business rules beyond DataAnnotations
5858
- **Named options** - Multiple configurations of the same options type with different names
59+
- **Error on missing keys** - `ErrorOnMissingKeys` fail-fast validation when configuration sections are missing
5960
- **Lifetime selection** - Singleton (`IOptions`), Scoped (`IOptionsSnapshot`), Monitor (`IOptionsMonitor`)
6061
- **Multi-project support** - Assembly-specific extension methods with smart naming
6162
- **Transitive registration** - 4 overloads for automatic/selective assembly registration
@@ -72,7 +73,7 @@ This roadmap is based on comprehensive analysis of:
7273
|| [Custom Validation Support (IValidateOptions)](#1-custom-validation-support-ivalidateoptions) | 🔴 High |
7374
|| [Named Options Support](#2-named-options-support) | 🔴 High |
7475
|| [Post-Configuration Support](#3-post-configuration-support) | 🟡 Medium-High |
75-
| | [Error on Missing Configuration Keys](#4-error-on-missing-configuration-keys) | 🔴 High |
76+
| | [Error on Missing Configuration Keys](#4-error-on-missing-configuration-keys) | 🔴 High |
7677
|| [Configuration Change Callbacks](#5-configuration-change-callbacks) | 🟡 Medium |
7778
|| [Bind Configuration Subsections to Properties](#6-bind-configuration-subsections-to-properties) | 🟡 Medium |
7879
|| [ConfigureAll Support](#7-configureall-support) | 🟢 Low-Medium |
@@ -296,10 +297,10 @@ services.AddOptions<StorageOptions>()
296297
### 4. Error on Missing Configuration Keys
297298

298299
**Priority**: 🔴 **High***Highly requested in GitHub issues*
299-
**Status**: ❌ Not Implemented
300+
**Status**: **Implemented**
300301
**Inspiration**: [GitHub Issue #36015](https://github.com/dotnet/runtime/issues/36015)
301302

302-
**Description**: Throw exceptions when required configuration keys are missing instead of silently setting properties to null/default.
303+
**Description**: Throw exceptions when required configuration sections are missing instead of silently binding to null/default values.
303304

304305
**User Story**:
305306
> "As a developer, I want my application to fail at startup if critical configuration like database connection strings is missing, rather than failing in production with NullReferenceException."
@@ -310,36 +311,53 @@ services.AddOptions<StorageOptions>()
310311
[OptionsBinding("Database", ErrorOnMissingKeys = true, ValidateOnStart = true)]
311312
public partial class DatabaseOptions
312313
{
313-
// If "Database:ConnectionString" is missing in appsettings.json,
314-
// throw exception at startup instead of silently setting to null
314+
[Required, MinLength(10)]
315315
public string ConnectionString { get; set; } = string.Empty;
316316

317-
public int MaxRetries { get; set; } = 5;
317+
[Range(1, 10)]
318+
public int MaxRetries { get; set; } = 3;
319+
320+
public int TimeoutSeconds { get; set; } = 30;
318321
}
319322

320-
// Generated code with error checking:
323+
// Generated code with section existence check:
321324
services.AddOptions<DatabaseOptions>()
322325
.Bind(configuration.GetSection("Database"))
323326
.Validate(options =>
324327
{
325-
if (string.IsNullOrEmpty(options.ConnectionString))
328+
var section = configuration.GetSection("Database");
329+
if (!section.Exists())
326330
{
327-
throw new OptionsValidationException(
328-
nameof(DatabaseOptions),
329-
typeof(DatabaseOptions),
330-
new[] { "ConnectionString is required but was not found in configuration" });
331+
throw new global::System.InvalidOperationException(
332+
"Configuration section 'Database' is missing. " +
333+
"Ensure the section exists in your appsettings.json or other configuration sources.");
331334
}
335+
332336
return true;
333337
})
338+
.ValidateDataAnnotations()
334339
.ValidateOnStart();
335340
```
336341

337-
**Implementation Notes**:
342+
**Implementation Details**:
343+
344+
- ✅ Added `ErrorOnMissingKeys` boolean parameter to `[OptionsBinding]` attribute
345+
- ✅ Generator checks `IConfigurationSection.Exists()` to detect missing sections
346+
- ✅ Throws `InvalidOperationException` with descriptive message including section name
347+
- ✅ Combines with `ValidateOnStart = true` for startup detection (recommended)
348+
- ✅ Works with all validation options (DataAnnotations, custom validators)
349+
- ✅ Section name included in error message for easy troubleshooting
350+
- ⚠️ Named options do NOT support ErrorOnMissingKeys (named options use simpler Configure pattern)
351+
352+
**Testing**:
353+
- ✅ 11 comprehensive unit tests covering all scenarios
354+
- ✅ Sample project updated: DatabaseOptions demonstrates ErrorOnMissingKeys
355+
- ✅ PetStore.Api sample: PetStoreOptions uses ErrorOnMissingKeys for critical configuration
338356

339-
- Add `ErrorOnMissingKeys` boolean parameter
340-
- Generate validation delegate that checks for null/default values
341-
- Combine with `ValidateOnStart = true` for startup failure
342-
- Consider making this opt-in per-property with attribute: `[Required]` from DataAnnotations
357+
**Best Practices**:
358+
- Always combine with `ValidateOnStart = true` to catch missing configuration at startup
359+
- Use for production-critical configuration (databases, external services, API keys)
360+
- Avoid for optional configuration with reasonable defaults
343361

344362
---
345363

@@ -766,7 +784,7 @@ Based on priority, user demand, and implementation complexity:
766784

767785
---
768786

769-
**Last Updated**: 2025-01-17
770-
**Version**: 1.0
787+
**Last Updated**: 2025-01-19
788+
**Version**: 1.1
771789
**Research Date**: January 2025 (.NET 8/9 Options Pattern)
772790
**Maintained By**: Atc.SourceGenerators Team

docs/OptionsBindingGenerators-Samples.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,8 @@ using System.ComponentModel.DataAnnotations;
167167
namespace Atc.SourceGenerators.OptionsBinding.Domain;
168168

169169
// Explicit section name (highest priority)
170-
[OptionsBinding("Database", ValidateDataAnnotations = true, ValidateOnStart = true)]
170+
// Demonstrates fail-fast validation when configuration section is missing
171+
[OptionsBinding("Database", ValidateDataAnnotations = true, ValidateOnStart = true, ErrorOnMissingKeys = true)]
171172
public partial class DatabaseOptions
172173
{
173174
[Required]
@@ -595,7 +596,8 @@ public class EmailService
595596
5. **Multi-Project**: Each project gets its own `AddOptionsFromXXX()` method
596597
6. **Flexible Lifetimes**: Choose between Singleton, Scoped, or Monitor
597598
7. **Startup Validation**: Catch configuration errors before runtime
598-
8. **Named Options**: Multiple configurations of the same type for fallback/multi-tenant scenarios
599+
8. **Error on Missing Keys**: Fail-fast validation when configuration sections are missing
600+
9. **Named Options**: Multiple configurations of the same type for fallback/multi-tenant scenarios
599601

600602
## 🔗 Related Documentation
601603

docs/OptionsBindingGenerators.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ services.AddOptions<DatabaseOptions>()
7474
- [🏷️ Data Annotations Validation](#️-data-annotations-validation)
7575
- [🚀 Validate on Startup](#-validate-on-startup)
7676
- [🔗 Combined Validation](#-combined-validation)
77+
- [🎯 Custom Validation (IValidateOptions)](#-custom-validation-ivalidateoptions)
78+
- [🚨 Error on Missing Configuration Keys](#-error-on-missing-configuration-keys)
7779
- [⏱️ Options Lifetimes](#️-options-lifetimes)
7880
- [🔧 How It Works](#-how-it-works)
7981
- [1️⃣ Attribute Detection](#1️⃣-attribute-detection)
@@ -741,6 +743,7 @@ Console.WriteLine($"Other interval: {otherOptions.Value.RepeatIntervalInSeconds}
741743
- **🧠 Automatic section name inference** - Smart resolution from explicit names, const fields (`SectionName`, `NameTitle`, `Name`), or auto-inferred from class names
742744
- **🔒 Built-in validation** - Integrated DataAnnotations validation (`ValidateDataAnnotations`) and startup validation (`ValidateOnStart`)
743745
- **🎯 Custom validation** - Support for `IValidateOptions<T>` for complex business rules beyond DataAnnotations
746+
- **🚨 Error on missing keys** - Fail-fast validation when configuration sections are missing (`ErrorOnMissingKeys`) to catch deployment issues at startup
744747
- **📛 Named options** - Multiple configurations of the same options type with different names (e.g., Primary/Secondary email servers)
745748
- **🎯 Explicit section paths** - Support for nested sections like `"App:Database"` or `"Services:Email"`
746749
- **📦 Multiple options classes** - Register multiple configuration sections in a single assembly with one method call
@@ -1028,6 +1031,75 @@ services.AddSingleton<global::Microsoft.Extensions.Options.IValidateOptions<glob
10281031
- Can validate cross-property dependencies
10291032
- Returns detailed failure messages
10301033

1034+
#### 🚨 Error on Missing Configuration Keys
1035+
1036+
The `ErrorOnMissingKeys` feature provides fail-fast validation when configuration sections are missing, preventing runtime errors from invalid or missing configuration.
1037+
1038+
**When to use:**
1039+
- Critical configuration that must be present (database connections, API keys, etc.)
1040+
- Detect configuration issues at application startup instead of later at runtime
1041+
- Ensure deployment validation catches missing configuration files or sections
1042+
1043+
**Example:**
1044+
1045+
```csharp
1046+
using System.ComponentModel.DataAnnotations;
1047+
1048+
[OptionsBinding("Database",
1049+
ValidateDataAnnotations = true,
1050+
ValidateOnStart = true,
1051+
ErrorOnMissingKeys = true)]
1052+
public partial class DatabaseOptions
1053+
{
1054+
[Required, MinLength(10)]
1055+
public string ConnectionString { get; set; } = string.Empty;
1056+
1057+
[Range(1, 10)]
1058+
public int MaxRetries { get; set; } = 3;
1059+
1060+
public int TimeoutSeconds { get; set; } = 30;
1061+
}
1062+
```
1063+
1064+
**Generated Code:**
1065+
1066+
```csharp
1067+
services.AddOptions<global::MyApp.Options.DatabaseOptions>()
1068+
.Bind(configuration.GetSection("Database"))
1069+
.Validate(options =>
1070+
{
1071+
var section = configuration.GetSection("Database");
1072+
if (!section.Exists())
1073+
{
1074+
throw new global::System.InvalidOperationException(
1075+
"Configuration section 'Database' is missing. " +
1076+
"Ensure the section exists in your appsettings.json or other configuration sources.");
1077+
}
1078+
1079+
return true;
1080+
})
1081+
.ValidateDataAnnotations()
1082+
.ValidateOnStart();
1083+
```
1084+
1085+
**Behavior:**
1086+
- Validates that the configuration section exists using `IConfigurationSection.Exists()`
1087+
- Throws `InvalidOperationException` with descriptive message if section is missing
1088+
- Combines with `ValidateOnStart = true` to fail at startup (recommended)
1089+
- Error message includes the section name for easy troubleshooting
1090+
1091+
**Best Practices:**
1092+
- Always combine with `ValidateOnStart = true` to catch missing configuration at startup
1093+
- Use for production-critical configuration (databases, external services, etc.)
1094+
- Avoid for optional configuration with reasonable defaults
1095+
- Ensure deployment processes validate configuration files exist
1096+
1097+
**Example Error Message:**
1098+
```
1099+
System.InvalidOperationException: Configuration section 'Database' is missing.
1100+
Ensure the section exists in your appsettings.json or other configuration sources.
1101+
```
1102+
10311103
### ⏱️ Options Lifetimes
10321104

10331105
Control which options interface consumers should inject. **All three interfaces are always available**, but the `Lifetime` property indicates the recommended interface for your use case:

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ namespace Atc.SourceGenerators.OptionsBinding.Options;
33
/// <summary>
44
/// Database configuration options with validation.
55
/// Explicitly binds to "Database" section in appsettings.json.
6+
/// Demonstrates ErrorOnMissingKeys to fail fast if configuration is missing.
67
/// </summary>
7-
[OptionsBinding("Database", ValidateDataAnnotations = true, ValidateOnStart = true, Validator = typeof(Validators.DatabaseOptionsValidator))]
8+
[OptionsBinding("Database", ValidateDataAnnotations = true, ValidateOnStart = true, ErrorOnMissingKeys = true, Validator = typeof(Validators.DatabaseOptionsValidator))]
89
public partial class DatabaseOptions
910
{
1011
[Required]

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,4 @@ public partial class EmailOptions
3434
/// Gets or sets the timeout in seconds.
3535
/// </summary>
3636
public int TimeoutSeconds { get; set; } = 30;
37-
}
37+
}

sample/Atc.SourceGenerators.OptionsBinding/Options/Validators/DatabaseOptionsValidator.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ namespace Atc.SourceGenerators.OptionsBinding.Options.Validators;
66
/// </summary>
77
public class DatabaseOptionsValidator : IValidateOptions<DatabaseOptions>
88
{
9-
public ValidateOptionsResult Validate(string? name, DatabaseOptions options)
9+
public ValidateOptionsResult Validate(
10+
string? name,
11+
DatabaseOptions options)
1012
{
1113
var failures = new List<string>();
1214

@@ -20,7 +22,8 @@ public ValidateOptionsResult Validate(string? name, DatabaseOptions options)
2022
if (!string.IsNullOrWhiteSpace(options.ConnectionString))
2123
{
2224
var connStr = options.ConnectionString.ToLowerInvariant();
23-
if (!connStr.Contains("server=") && !connStr.Contains("data source="))
25+
if (!connStr.Contains("server=", StringComparison.Ordinal) &&
26+
!connStr.Contains("data source=", StringComparison.Ordinal))
2427
{
2528
failures.Add("ConnectionString must contain 'Server=' or 'Data Source=' parameter");
2629
}
@@ -30,4 +33,4 @@ public ValidateOptionsResult Validate(string? name, DatabaseOptions options)
3033
? ValidateOptionsResult.Fail(failures)
3134
: ValidateOptionsResult.Success;
3235
}
33-
}
36+
}

sample/Atc.SourceGenerators.OptionsBinding/Program.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
Console.WriteLine("1. Testing DatabaseOptions (with validation):");
2121
Console.WriteLine(" - Section: \"Database\"");
22-
Console.WriteLine(" - Validation: DataAnnotations + ValidateOnStart");
22+
Console.WriteLine(" - Validation: DataAnnotations + ValidateOnStart + ErrorOnMissingKeys");
2323

2424
try
2525
{

sample/PetStore.Domain/Options/PetStoreOptions.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ namespace PetStore.Domain.Options;
22

33
/// <summary>
44
/// Configuration options for the pet store.
5+
/// Demonstrates ErrorOnMissingKeys for critical configuration that must be present.
56
/// </summary>
6-
[OptionsBinding("PetStore", ValidateDataAnnotations = true, ValidateOnStart = true, Validator = typeof(Validators.PetStoreOptionsValidator))]
7+
[OptionsBinding("PetStore", ValidateDataAnnotations = true, ValidateOnStart = true, ErrorOnMissingKeys = true, Validator = typeof(Validators.PetStoreOptionsValidator))]
78
public partial class PetStoreOptions
89
{
910
/// <summary>

0 commit comments

Comments
 (0)