
Microsoft Extensions Configuration
- 431 installs
- 1.1k repo stars
- Updated July 3, 2026
- aaronontheweb/dotnet-skills
microsoft-extensions-configuration is a .NET agent skill that configures strongly typed settings with Microsoft.Extensions.Options, IValidateOptions, and ValidateOnStart for developers preventing runtime configuration fa
About
microsoft-extensions-configuration is an agent skill in aaronontheweb/dotnet-skills, part of a plugin with 30 skills and 5 specialized agents for professional .NET development. The SKILL.md documents 5 configuration patterns: basic Options binding from appsettings.json, Data Annotations validation with [Required] and [Range], IValidateOptions<T> for cross-property and conditional rules, IOptions vs IOptionsSnapshot vs IOptionsMonitor lifetime choices, and PostConfigure normalization before validation. Registration emphasizes AddOptions<T>().BindConfiguration().ValidateDataAnnotations().ValidateOnStart() so misconfiguration fails at startup rather than deep in business logic. Anti-patterns warn against injecting IConfiguration directly, validating in service constructors, and throwing inside IValidateOptions instead of returning ValidateOptionsResult.Fail. An advanced-patterns.md reference covers named options and production AkkaSettings examples. Use microsoft-extensions-configuration when wiring new .NET service configuration or eliminating silent config defaults.
- appsettings layering
- Environment variable overrides
- IOptions binding patterns
- User secrets for local dev
- Configuration validation hooks
Microsoft Extensions Configuration by the numbers
- 431 all-time installs (skills.sh)
- Ranked #44 of 153 .NET & C# skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/aaronontheweb/dotnet-skills --skill microsoft-extensions-configurationAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 431 |
|---|---|
| repo stars | ★ 1.1k |
| Last updated | July 3, 2026 |
| Repository | aaronontheweb/dotnet-skills ↗ |
How do you validate .NET configuration at startup?
Configure .NET services with appsettings, environment variables, user secrets, and options binding when standing up APIs, workers, or CLI tools.
Who is it for?
.NET developers configuring APIs, background workers, or CLI tools with appsettings.json, user secrets, and environment variables.
Skip if: Non-.NET projects or apps with trivial configuration that does not need typed options or startup validation.
When should I use this skill?
User sets up appsettings binding, IOptions patterns, IValidateOptions, or reports runtime failures from missing connection strings or invalid config values.
What you get
Strongly typed settings classes, DI registration with ValidateOnStart, IValidateOptions validators, and startup failure on misconfiguration.
- Strongly typed settings POCO classes
- Options DI registration with validators
- Startup validation error messages
By the numbers
- Parent dotnet-skills plugin includes 30 skills
- SKILL.md documents 5 configuration patterns
Files
Microsoft.Extensions Configuration Patterns
When to Use This Skill
Use this skill when:
- Binding configuration from appsettings.json to strongly-typed classes
- Validating configuration at application startup (fail fast)
- Implementing complex validation logic for settings
- Designing configuration classes that are testable and maintainable
- Understanding IOptions<T>, IOptionsSnapshot<T>, and IOptionsMonitor<T>
Reference Files
- advanced-patterns.md: Validators with dependencies, named options, complete production example (AkkaSettings), and testing validators
Why Configuration Validation Matters
The Problem: Applications often fail at runtime due to misconfiguration - missing connection strings, invalid URLs, out-of-range values. These failures happen deep in business logic, far from where configuration is loaded.
The Solution: Validate configuration at startup. If invalid, fail immediately with a clear error message.
// BAD: Fails at runtime when someone tries to use the service
public class EmailService
{
public EmailService(IOptions<SmtpSettings> options)
{
var settings = options.Value;
// Throws NullReferenceException 10 minutes into production
_client = new SmtpClient(settings.Host, settings.Port);
}
}
// GOOD: Fails at startup with clear error
// "SmtpSettings validation failed: Host is required"---
Pattern 1: Basic Options Binding
Define a Settings Class
public class SmtpSettings
{
public const string SectionName = "Smtp";
public string Host { get; set; } = string.Empty;
public int Port { get; set; } = 587;
public string? Username { get; set; }
public string? Password { get; set; }
public bool UseSsl { get; set; } = true;
}Bind from Configuration
builder.Services.AddOptions<SmtpSettings>()
.BindConfiguration(SmtpSettings.SectionName);
// appsettings.json
{
"Smtp": {
"Host": "smtp.example.com",
"Port": 587,
"Username": "user@example.com",
"Password": "secret",
"UseSsl": true
}
}Consume in Services
public class EmailService
{
private readonly SmtpSettings _settings;
// IOptions<T> - singleton, read once at startup
public EmailService(IOptions<SmtpSettings> options)
{
_settings = options.Value;
}
}---
Pattern 2: Data Annotations Validation
For simple validation rules, use Data Annotations:
using System.ComponentModel.DataAnnotations;
public class SmtpSettings
{
public const string SectionName = "Smtp";
[Required(ErrorMessage = "SMTP host is required")]
public string Host { get; set; } = string.Empty;
[Range(1, 65535, ErrorMessage = "Port must be between 1 and 65535")]
public int Port { get; set; } = 587;
[EmailAddress(ErrorMessage = "Username must be a valid email address")]
public string? Username { get; set; }
public string? Password { get; set; }
public bool UseSsl { get; set; } = true;
}Enable Data Annotations Validation
builder.Services.AddOptions<SmtpSettings>()
.BindConfiguration(SmtpSettings.SectionName)
.ValidateDataAnnotations() // Enable attribute-based validation
.ValidateOnStart(); // Validate immediately at startupKey Point: .ValidateOnStart() is critical. Without it, validation only runs when the options are first accessed.
---
Pattern 3: IValidateOptions<T> for Complex Validation
Data Annotations work for simple rules, but complex validation requires IValidateOptions<T>:
| Scenario | Data Annotations | IValidateOptions |
|---|---|---|
| Required field | Yes | Yes |
| Range check | Yes | Yes |
| Cross-property validation | No | Yes |
| Conditional validation | No | Yes |
| External service checks | No | Yes |
| Dependency injection in validator | No | Yes |
Implementing IValidateOptions
using Microsoft.Extensions.Options;
public class SmtpSettingsValidator : IValidateOptions<SmtpSettings>
{
public ValidateOptionsResult Validate(string? name, SmtpSettings options)
{
var failures = new List<string>();
if (string.IsNullOrWhiteSpace(options.Host))
failures.Add("Host is required");
if (options.Port is < 1 or > 65535)
failures.Add($"Port {options.Port} is invalid. Must be between 1 and 65535");
// Cross-property validation
if (!string.IsNullOrEmpty(options.Username) && string.IsNullOrEmpty(options.Password))
failures.Add("Password is required when Username is specified");
// Conditional validation
if (options.UseSsl && options.Port == 25)
failures.Add("Port 25 is typically not used with SSL. Consider port 465 or 587");
return failures.Count > 0
? ValidateOptionsResult.Fail(failures)
: ValidateOptionsResult.Success;
}
}Register the Validator
builder.Services.AddOptions<SmtpSettings>()
.BindConfiguration(SmtpSettings.SectionName)
.ValidateDataAnnotations()
.ValidateOnStart();
builder.Services.AddSingleton<IValidateOptions<SmtpSettings>, SmtpSettingsValidator>();Order matters: Data Annotations run first, then IValidateOptions validators. All failures are collected together.
See advanced-patterns.md for validators with dependencies, named options, and a complete production example.
---
Pattern 4: Options Lifetime
| Interface | Lifetime | Reloads on Change | Use Case |
|---|---|---|---|
IOptions<T> | Singleton | No | Static config, read once |
IOptionsSnapshot<T> | Scoped | Yes (per request) | Web apps needing fresh config |
IOptionsMonitor<T> | Singleton | Yes (with callback) | Background services, real-time updates |
IOptionsMonitor for Background Services
public class BackgroundWorker : BackgroundService
{
private readonly IOptionsMonitor<WorkerSettings> _optionsMonitor;
private WorkerSettings _currentSettings;
public BackgroundWorker(IOptionsMonitor<WorkerSettings> optionsMonitor)
{
_optionsMonitor = optionsMonitor;
_currentSettings = optionsMonitor.CurrentValue;
_optionsMonitor.OnChange(settings =>
{
_currentSettings = settings;
});
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await DoWorkAsync();
await Task.Delay(_currentSettings.PollingInterval, stoppingToken);
}
}
}---
Pattern 5: Post-Configuration
Modify options after binding but before validation:
builder.Services.AddOptions<ApiSettings>()
.BindConfiguration("Api")
.PostConfigure(options =>
{
if (!string.IsNullOrEmpty(options.BaseUrl) && !options.BaseUrl.EndsWith('/'))
options.BaseUrl += '/';
options.Timeout ??= TimeSpan.FromSeconds(30);
})
.ValidateDataAnnotations()
.ValidateOnStart();---
Anti-Patterns to Avoid
1. Manual Configuration Access
// BAD: Bypasses validation, hard to test
public class MyService
{
public MyService(IConfiguration configuration)
{
var host = configuration["Smtp:Host"]; // No validation!
}
}
// GOOD: Strongly-typed, validated
public class MyService
{
public MyService(IOptions<SmtpSettings> options)
{
var host = options.Value.Host; // Validated at startup
}
}2. Validation in Constructor
// BAD: Validation happens at runtime, not startup
public class MyService
{
public MyService(IOptions<Settings> options)
{
if (string.IsNullOrEmpty(options.Value.Required))
throw new ArgumentException("Required is missing"); // Too late!
}
}
// GOOD: Validation at startup via IValidateOptions + ValidateOnStart()3. Forgetting ValidateOnStart
// BAD: Validation only runs when first accessed
builder.Services.AddOptions<Settings>()
.ValidateDataAnnotations(); // Missing ValidateOnStart!
// GOOD: Fails immediately if invalid
builder.Services.AddOptions<Settings>()
.ValidateDataAnnotations()
.ValidateOnStart();4. Throwing in IValidateOptions
// BAD: Throws exception, breaks validation chain
public ValidateOptionsResult Validate(string? name, Settings options)
{
if (options.Value < 0)
throw new ArgumentException("Value cannot be negative"); // Wrong!
return ValidateOptionsResult.Success;
}
// GOOD: Return failure result
public ValidateOptionsResult Validate(string? name, Settings options)
{
if (options.Value < 0)
return ValidateOptionsResult.Fail("Value cannot be negative");
return ValidateOptionsResult.Success;
}---
Summary
| Principle | Implementation |
|---|---|
| Fail fast | .ValidateOnStart() |
| Strongly-typed | Bind to POCO classes |
| Simple validation | Data Annotations |
| Complex validation | IValidateOptions<T> |
| Cross-property rules | IValidateOptions<T> |
| Environment-aware | Inject IHostEnvironment |
| Testable | Validators are plain classes |
Advanced Configuration Patterns
Validators with dependencies, named options, complete production example, and testing configuration validators.
Contents
- Validators with Dependencies
- Named Options
- Complete Example - Production Settings Class
- Testing Configuration Validators
Validators with Dependencies
IValidateOptions validators are resolved from DI, so they can have dependencies:
public class DatabaseSettingsValidator : IValidateOptions<DatabaseSettings>
{
private readonly ILogger<DatabaseSettingsValidator> _logger;
private readonly IHostEnvironment _environment;
public DatabaseSettingsValidator(
ILogger<DatabaseSettingsValidator> logger,
IHostEnvironment environment)
{
_logger = logger;
_environment = environment;
}
public ValidateOptionsResult Validate(string? name, DatabaseSettings options)
{
var failures = new List<string>();
if (string.IsNullOrWhiteSpace(options.ConnectionString))
{
failures.Add("ConnectionString is required");
}
// Environment-specific validation
if (_environment.IsProduction())
{
if (options.ConnectionString?.Contains("localhost") == true)
{
failures.Add("Production cannot use localhost database");
}
if (!options.ConnectionString?.Contains("Encrypt=True") == true)
{
_logger.LogWarning("Production database connection should use encryption");
}
}
// Validate connection string format
if (!string.IsNullOrEmpty(options.ConnectionString))
{
try
{
var builder = new SqlConnectionStringBuilder(options.ConnectionString);
if (string.IsNullOrEmpty(builder.DataSource))
{
failures.Add("ConnectionString must specify a Data Source");
}
}
catch (Exception ex)
{
failures.Add($"ConnectionString is malformed: {ex.Message}");
}
}
return failures.Count > 0
? ValidateOptionsResult.Fail(failures)
: ValidateOptionsResult.Success;
}
}Named Options
When you have multiple instances of the same settings type (e.g., multiple database connections):
// appsettings.json
{
"Databases": {
"Primary": {
"ConnectionString": "Server=primary;..."
},
"Replica": {
"ConnectionString": "Server=replica;..."
}
}
}
// Registration
builder.Services.AddOptions<DatabaseSettings>("Primary")
.BindConfiguration("Databases:Primary")
.ValidateDataAnnotations()
.ValidateOnStart();
builder.Services.AddOptions<DatabaseSettings>("Replica")
.BindConfiguration("Databases:Replica")
.ValidateDataAnnotations()
.ValidateOnStart();
// Consumption
public class DataService
{
private readonly DatabaseSettings _primary;
private readonly DatabaseSettings _replica;
public DataService(IOptionsSnapshot<DatabaseSettings> options)
{
_primary = options.Get("Primary");
_replica = options.Get("Replica");
}
}Named Options Validator
public class DatabaseSettingsValidator : IValidateOptions<DatabaseSettings>
{
public ValidateOptionsResult Validate(string? name, DatabaseSettings options)
{
var failures = new List<string>();
var prefix = string.IsNullOrEmpty(name) ? "" : $"[{name}] ";
if (string.IsNullOrWhiteSpace(options.ConnectionString))
{
failures.Add($"{prefix}ConnectionString is required");
}
// Name-specific validation
if (name == "Primary" && options.ReadOnly)
{
failures.Add("Primary database cannot be read-only");
}
return failures.Count > 0
? ValidateOptionsResult.Fail(failures)
: ValidateOptionsResult.Success;
}
}Complete Example - Production Settings Class
using System.ComponentModel.DataAnnotations;
using Microsoft.Extensions.Options;
public class AkkaSettings
{
public const string SectionName = "AkkaSettings";
[Required]
public string ActorSystemName { get; set; } = "MySystem";
public AkkaExecutionMode ExecutionMode { get; set; } = AkkaExecutionMode.LocalTest;
public bool LogConfigOnStart { get; set; } = false;
public RemoteOptions RemoteOptions { get; set; } = new();
public ClusterOptions ClusterOptions { get; set; } = new();
public ClusterBootstrapOptions ClusterBootstrapOptions { get; set; } = new();
}
public enum AkkaExecutionMode
{
LocalTest, // No remoting, no clustering
Clustered // Full cluster with sharding, distributed pub/sub
}
public class AkkaSettingsValidator : IValidateOptions<AkkaSettings>
{
private readonly IHostEnvironment _environment;
public AkkaSettingsValidator(IHostEnvironment environment)
{
_environment = environment;
}
public ValidateOptionsResult Validate(string? name, AkkaSettings options)
{
var failures = new List<string>();
// Basic validation
if (string.IsNullOrWhiteSpace(options.ActorSystemName))
{
failures.Add("ActorSystemName is required");
}
// Mode-specific validation
if (options.ExecutionMode == AkkaExecutionMode.Clustered)
{
ValidateClusteredMode(options, failures);
}
// Environment-specific validation
if (_environment.IsProduction() && options.ExecutionMode == AkkaExecutionMode.LocalTest)
{
failures.Add("LocalTest execution mode is not allowed in production");
}
return failures.Count > 0
? ValidateOptionsResult.Fail(failures)
: ValidateOptionsResult.Success;
}
private void ValidateClusteredMode(AkkaSettings options, List<string> failures)
{
if (string.IsNullOrEmpty(options.RemoteOptions.PublicHostName))
{
failures.Add("RemoteOptions.PublicHostName is required in Clustered mode");
}
if (options.RemoteOptions.Port is null or < 0)
{
failures.Add("RemoteOptions.Port must be >= 0 in Clustered mode");
}
if (options.ClusterBootstrapOptions.Enabled)
{
ValidateClusterBootstrap(options.ClusterBootstrapOptions, failures);
}
else if (options.ClusterOptions.SeedNodes?.Length == 0)
{
failures.Add("Either ClusterBootstrap must be enabled or SeedNodes must be specified");
}
}
private void ValidateClusterBootstrap(ClusterBootstrapOptions options, List<string> failures)
{
if (string.IsNullOrEmpty(options.ServiceName))
{
failures.Add("ClusterBootstrapOptions.ServiceName is required");
}
if (options.RequiredContactPointsNr <= 0)
{
failures.Add("ClusterBootstrapOptions.RequiredContactPointsNr must be > 0");
}
switch (options.DiscoveryMethod)
{
case DiscoveryMethod.Config:
if (options.ConfigServiceEndpoints?.Length == 0)
{
failures.Add("ConfigServiceEndpoints required for Config discovery");
}
break;
case DiscoveryMethod.AzureTableStorage:
if (options.AzureDiscoveryOptions == null)
{
failures.Add("AzureDiscoveryOptions required for Azure discovery");
}
break;
}
}
}
// Registration
builder.Services.AddOptions<AkkaSettings>()
.BindConfiguration(AkkaSettings.SectionName)
.ValidateDataAnnotations()
.ValidateOnStart();
builder.Services.AddSingleton<IValidateOptions<AkkaSettings>, AkkaSettingsValidator>();Testing Configuration Validators
public class SmtpSettingsValidatorTests
{
private readonly SmtpSettingsValidator _validator = new();
[Fact]
public void Validate_WithValidSettings_ReturnsSuccess()
{
var settings = new SmtpSettings
{
Host = "smtp.example.com",
Port = 587,
Username = "user@example.com",
Password = "secret"
};
var result = _validator.Validate(null, settings);
result.Succeeded.Should().BeTrue();
}
[Fact]
public void Validate_WithMissingHost_ReturnsFail()
{
var settings = new SmtpSettings { Host = "" };
var result = _validator.Validate(null, settings);
result.Succeeded.Should().BeFalse();
result.FailureMessage.Should().Contain("Host is required");
}
[Fact]
public void Validate_WithUsernameButNoPassword_ReturnsFail()
{
var settings = new SmtpSettings
{
Host = "smtp.example.com",
Username = "user@example.com",
Password = null // Missing!
};
var result = _validator.Validate(null, settings);
result.Succeeded.Should().BeFalse();
result.FailureMessage.Should().Contain("Password is required");
}
}Related skills
How it compares
Use microsoft-extensions-configuration for typed options and startup validation; pair with dependency-injection-patterns in the same repo for broader DI registration design.
FAQ
Why use ValidateOnStart in microsoft-extensions-configuration?
microsoft-extensions-configuration stresses ValidateOnStart so AddOptions binding and validation run at application startup, surfacing errors like missing SMTP hosts immediately instead of during later service execution.
When should you use IValidateOptions over Data Annotations?
microsoft-extensions-configuration recommends Data Annotations for simple required and range rules, and IValidateOptions<T> for cross-property validation, conditional rules, and validators that need dependency injection such as IHostEnvironment or ILogger.