Configuration & Options Pattern
15 questions found
How does ASP.NET Core's configuration system layer multiple sources together, and what is the default provider precedence?
Beginner
IConfiguration builds a unified key-value tree by combining multiple providers in registration order, where later-registered providers override earlier ones for the same key -- the default host builder registers appsettings.json first, then appsettings.{Environment}.json, then user secrets (Development only), then environment variables, then command-line arguments, giving environment variables and command-line args the ability to override file-based settings, essential for containerized deployments.
// Precedence (later overrides earlier):
// appsettings.json -> appsettings.Production.json -> environment variables -> command-line args
// appsettings.json: "ConnectionStrings:Db": "local-connection"
// Environment variable ConnectionStrings__Db overrides it at deploy time
Real-world example
A containerized application ships with a generic appsettings.json for local development, while the actual production connection string is injected via a Kubernetes environment variable, overriding the file-based default without needing separate builds per environment.
Common follow-ups: Why does the environment variable provider use double underscores instead of colons for nested keys?;How would you add a custom configuration source, like a database provider?
Configuration & Options Pattern;Hosting Models: Kestrel
IIS & Reverse Proxies
What is the Options pattern, and why is it preferred over injecting IConfiguration directly and reading string keys throughout an application?
Intermediate
The Options pattern binds a strongly-typed C# class to a section of configuration (via IOptions<T>/IOptionsSnapshot<T>/IOptionsMonitor<T>), giving compile-time type safety, IntelliSense, and centralized validation instead of scattering magic string-based Configuration["Key:SubKey"] lookups (unchecked at compile time and easy to typo) throughout the codebase.
public class SmtpSettings {
public string Host { get; set; } = "";
public int Port { get; set; }
}
builder.Services.Configure<SmtpSettings>(builder.Configuration.GetSection("Smtp"));
public class EmailService(IOptions<SmtpSettings> options) {
private readonly SmtpSettings _settings = options.Value;
}
Real-world example
A team migrates dozens of scattered Configuration["Smtp:Host"] string lookups (some with typos causing silent null values) to a single strongly-typed SmtpSettings class, catching configuration key mismatches at compile time instead of in production.
Common follow-ups: What's the difference between IOptions, IOptionsSnapshot, and IOptionsMonitor?;How do you validate options at startup using ValidateOnStart?
Dependency Injection;Diagnostics & Performance
What is the difference between IOptions<T>, IOptionsSnapshot<T>, and IOptionsMonitor<T>, and which is safe to inject into a singleton?
Advanced
IOptions<T> is registered as a singleton, computed once at first access and never refreshed even if configuration changes at runtime -- safe for singletons, suitable for genuinely static settings. IOptionsSnapshot<T> is scoped, recomputed once per request, so injecting it into a singleton throws a captive dependency error. IOptionsMonitor<T> is a singleton that can notify subscribers immediately when configuration changes via OnChange, making it the correct choice for singleton services (like a BackgroundService) needing to react to live configuration updates.
// Safe in a singleton: reacts to changes via OnChange
public class MyBackgroundService(IOptionsMonitor<WorkerSettings> monitor) : BackgroundService {
protected override Task ExecuteAsync(CancellationToken ct) {
monitor.OnChange(settings => _logger.LogInformation("Settings changed!"));
return Task.CompletedTask;
}
}
Real-world example
A long-running BackgroundService uses IOptionsMonitor to immediately pick up a changed polling interval from a hot-reloadable appsettings.json without needing to restart the entire application, something IOptions couldn't do at all.
Common follow-ups: Why can't a singleton service safely use IOptionsSnapshot?;What triggers the OnChange callback in IOptionsMonitor specifically?
Dependency Injection;Background Tasks & Hosted Services
How does appsettings.{Environment}.json layering work, and how does ASP.NET Core determine the current environment?
Intermediate
ASP.NET Core loads appsettings.json first as a base configuration, then layers appsettings.{EnvironmentName}.json on top, overriding matching keys, with the environment determined by the ASPNETCORE_ENVIRONMENT environment variable (Development, Staging, Production) -- letting you maintain environment-specific overrides without duplicating the entire configuration file.
// appsettings.json (base)
{ "Logging": { "LogLevel": { "Default": "Information" } } }
// appsettings.Development.json (overrides base only in Development)
{ "Logging": { "LogLevel": { "Default": "Debug" } } }
// Set via: ASPNETCORE_ENVIRONMENT=Development
Real-world example
A team keeps verbose Debug-level logging enabled only in appsettings.Development.json, while production automatically uses the quieter Information-level default from the base appsettings.json, without any code changes between environments.
Common follow-ups: How do you add support for a custom environment name beyond the standard three?;What happens if ASPNETCORE_ENVIRONMENT isn't set at all?
Logging;Hosting Models: Kestrel
IIS & Reverse Proxies
How do you validate configuration options at application startup to fail fast on misconfiguration, using ValidateDataAnnotations and ValidateOnStart?
Advanced
Chaining .ValidateDataAnnotations().ValidateOnStart() on the options builder ensures configuration is validated immediately when the application starts (rather than lazily on first use), causing the application to fail to launch with a clear error message for missing or invalid required settings, instead of a confusing failure discovered only when a specific code path first accesses the misconfigured option deep into runtime.
public class SmtpSettings {
[Required] public string Host { get; set; } = "";
[Range(1, 65535)] public int Port { get; set; }
}
builder.Services.AddOptions<SmtpSettings>()
.Bind(builder.Configuration.GetSection("Smtp"))
.ValidateDataAnnotations()
.ValidateOnStart();
Real-world example
A misconfigured production deployment missing the required Smtp:Host setting now fails immediately at startup with a clear validation error, instead of running until the first email-send attempt fails hours later with a confusing null reference exception.
Common follow-ups: How do you write custom validation logic beyond DataAnnotations using IValidateOptions?;What happens without ValidateOnStart -- when does validation actually run?
Global Exception Handling & Middleware;Diagnostics & Performance
How does named options support configuring multiple distinct instances of the same options class?
Intermediate
Named options let you register multiple configurations of the same C# options class under different string names (via Configure<T>(name, ...)), then resolve a specific named instance using IOptionsSnapshot<T>.Get(name) or IOptionsMonitor<T>.Get(name) -- useful when you need several independently-configured instances of the same settings shape.
builder.Services.Configure<SmtpSettings>("Marketing", builder.Configuration.GetSection("Smtp:Marketing"));
builder.Services.Configure<SmtpSettings>("Transactional", builder.Configuration.GetSection("Smtp:Transactional"));
public class EmailService(IOptionsSnapshot<SmtpSettings> options) {
public void SendMarketing() => Send(options.Get("Marketing"));
public void SendTransactional() => Send(options.Get("Transactional"));
}
Real-world example
An application sending both marketing newsletters (via a bulk email provider) and transactional receipts (via a more reliable SMTP relay) uses named options to maintain two independently-configured SmtpSettings instances from the same class definition.
Common follow-ups: How does the default (unnamed) options instance relate to named ones?;Can you validate each named instance independently?
Configuration & Options Pattern;Dependency Injection
How does configuration hot-reload work with reloadOnChange for appsettings.json, and what are its limitations?
Advanced
When appsettings.json is registered with reloadOnChange: true (the default), a file system watcher detects file changes and triggers IConfiguration to reload its values in-memory without an application restart -- code using IOptionsSnapshot<T> or IOptionsMonitor<T> automatically picks up new values, but IOptions<T> (computed once, cached forever as a singleton) does not.
builder.Configuration.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
// A service using IOptionsMonitor sees updated values automatically:
public MyService(IOptionsMonitor<FeatureSettings> monitor) {
monitor.OnChange(settings => _logger.LogInformation("Config reloaded"));
}
Real-world example
An ops team updates a feature toggle percentage directly in a mounted ConfigMap-backed appsettings.json file in a running Kubernetes pod, and the application picks up the change within seconds via hot-reload, without needing to restart the pod.
Common follow-ups: Why doesn't IOptions<T> reflect hot-reloaded changes?;What are the risks of enabling hot-reload for security-sensitive settings?
Secrets Management;Background Tasks & Hosted Services
How do environment variables map to nested configuration keys, given some shells don't support colons in variable names?
Intermediate
The environment variable configuration provider uses double underscores (__) as the hierarchy separator instead of colons, since some shells and operating systems don't support colons in environment variable names -- ASP.NET Core automatically translates ConnectionStrings__DefaultConnection into the equivalent "ConnectionStrings:DefaultConnection" configuration key internally.
# Setting a nested configuration value via environment variable
export ConnectionStrings__DefaultConnection="Server=prod;Database=MyApp;"
# Equivalent to this in appsettings.json:
# { "ConnectionStrings": { "DefaultConnection": "Server=prod;Database=MyApp;" } }
Real-world example
A Kubernetes deployment manifest sets Logging__LogLevel__Default=Warning as a container environment variable, correctly overriding the nested Logging:LogLevel:Default key from the base appsettings.json without any code changes.
Common follow-ups: Does this convention apply consistently across Windows, Linux, and macOS?;How do you configure array values via environment variables?
Hosting Models: Kestrel
IIS & Reverse Proxies;CI/CD
Publishing & Deployment
How does the configuration binder handle nested objects and arrays when binding a JSON section to a strongly-typed C# class?
Advanced
The binder uses reflection to match configuration keys (flattened into a colon-delimited hierarchy internally) to corresponding property names, recursively binding nested objects, and mapping JSON arrays to numerically-indexed keys (Items:0, Items:1) that bind to List<T> or array properties -- requiring the target class to have public settable properties matching the configuration structure.
// appsettings.json
{
"Settings": {
"MaxRetries": 3,
"Endpoints": ["https://a.com", "https://b.com"]
}
}
public class Settings {
public int MaxRetries { get; set; }
public List<string> Endpoints { get; set; } = new();
}
Real-world example
A team debugging why a configuration array wasn't binding correctly discovers their JSON used an object with string keys instead of a JSON array, since the binder specifically expects sequential numeric indices to map to a List<T> or array property.
Common follow-ups: How does binding handle configuration keys that don't match any property?;Can the binder populate a Dictionary<string, T> from configuration?
Configuration & Options Pattern;Diagnostics & Performance
What is user secrets (dotnet user-secrets), and why should it be used only for local development, never production?
Intermediate
User secrets stores sensitive configuration values (API keys, connection strings) outside the project directory in a per-user, per-project JSON file, keeping them out of source control accidentally while still being automatically loaded into IConfiguration during local development -- it's explicitly unencrypted, plain text on local disk, making it entirely unsuitable for production, where a proper secret manager (Azure Key Vault, AWS Secrets Manager) should be used instead.
dotnet user-secrets init
dotnet user-secrets set "ConnectionStrings:Db" "Server=local;Database=Dev;"
// Automatically loaded in Development via:
// builder.Configuration.AddUserSecrets<Program>(); (added by default host builder in dev)
Real-world example
A developer avoids accidentally committing their local database password to a shared Git repository by storing it in user secrets instead of directly in appsettings.Development.json, which would otherwise get committed alongside the rest of the code.
Common follow-ups: Where are user secrets actually stored on disk?;Why is user secrets specifically excluded from being enabled by default in Production?
Secrets Management;.NET CLI
SDK & Project Structure (csproj)