A service may need a retry limit, an endpoint address and a credential. These values have different validation and storage requirements. Scattering string-key lookups through business code makes missing settings hard to diagnose and encourages secrets to appear in logs. A typed options class gives one feature a visible settings contract, while configuration providers supply the values under a known precedence rule.
The examples use .NET 8 and C# 12. A console project references the ASP.NET Core shared framework for configuration, options and dependency injection. The main example uses only in-memory settings so its output is deterministic. You will learn how file and environment providers fit into the same model without assuming that an arbitrary configuration value is safe or valid.
Providers form an ordered key-value view
Configuration combines providers, and later providers generally override earlier values for the same key. A host's defaults can include JSON files, environment variables and command-line arguments, but an explicit builder contains only what you add. Read the host's documented defaults instead of assuming every application has the same provider set.
Hierarchical keys use a separator such as Retry:Attempts. Environment-variable providers commonly support double underscores to represent this hierarchy across operating systems. A value's presence does not prove its source is trustworthy. Command-line arguments can be visible in process listings, environment values can be inherited, and a configuration file may be writable by another account. Define where each setting comes from and which source is allowed to override it.
Typed options group a feature's contract
An options class gives settings descriptive properties and defaults. Binding maps a configuration section to those properties. Business services can then receive the appropriate options interface instead of knowing every raw key. Defaults should be intentionally safe, not a way to conceal a missing required endpoint or credential.
Binding and validation are separate. An integer can bind successfully while being outside the application's allowed range. Validate cross-property rules, such as a maximum delay being no shorter than the initial delay. Error messages should identify the setting without printing secret values. Avoid reading a connection string or token into a generic object dump for diagnostics. The settings contract belongs to the feature, so validation should be located where that feature's requirements are understood.
Choose the options lifetime deliberately
IOptions<T> supplies a stable options value for ordinary use. IOptionsSnapshot<T> is scoped and useful when a new scope should observe updated settings. IOptionsMonitor<T> supports current values and change notifications for singleton consumers. Reloading depends on the provider and its configuration; choosing a monitor does not make every source reload automatically.
A consumer must decide how to apply a change. Replacing a retry limit is simpler than replacing a resource already created from an endpoint. Do not assume changing configuration reconstructs every singleton service. Validate updates and define fallback behavior when a new value is invalid. ValidateOnStart supports early validation under an appropriate host startup workflow; ordinary service resolution also needs validation when no host is started.
Secrets need a separate storage policy
Committed JSON settings are appropriate for non-sensitive defaults, not production credentials. .NET's user-secrets tooling can keep development values outside the project tree. It is a development convenience and is not an encrypted production vault. Initialize the project and set values using the documented tooling; avoid placing real secrets in tutorial commands or shell histories.
A production application should use an approved secret source under its deployment environment's access controls. Configuration is an abstraction for receiving the secret, not a guarantee that it is protected. Never log the full configuration tree. Redact credentials from diagnostics, limit who can read the source, and plan rotation. A renamed key or a base64 representation does not turn a secret into harmless public data.
Test precedence and missing values
A settings test should cover a valid value, a missing required value, a boundary value and a conflicting override. Use in-memory providers to isolate the feature from the test machine. If the application supports reload, test how its consumer behaves after a validated change rather than merely testing the monitor's existence.
Keep conversion errors distinct from validation errors where the API exposes them. A nonnumeric batch string is not the same as a numeric value outside the allowed range. Avoid catch-all code that falls back to an unlimited batch size. Safe defaults, early validation and explicit source precedence make failures understandable before they cause excessive work or leak data.
Official references
C# Configuration, Options and Secrets Examples
Each example shows the code, the output it produces and what each part does.
Bind, override and validate retry settings
Use the following project file and complete program. Two in-memory providers demonstrate precedence without reading a machine-specific environment.
The later provider wins, and accessing the typed value applies validation. The example's default does not replace the explicit configured value.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
</Project>using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
IConfiguration settings = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
["Retry:Attempts"] = "2"
})
.AddInMemoryCollection(new Dictionary<string, string?>
{
["Retry:Attempts"] = "4"
})
.Build();
var services = new ServiceCollection();
services.AddOptions<RetryOptions>()
.Bind(settings.GetSection("Retry"))
.Validate(o => o.Attempts is >= 1 and <= 5,
"Retry attempts must be between 1 and 5.");
using var provider = services.BuildServiceProvider();
var options = provider.GetRequiredService<IOptions<RetryOptions>>().Value;
Console.WriteLine($"Attempts: {options.Attempts}");
public sealed class RetryOptions
{
public int Attempts { get; set; } = 1;
}Attempts: 4C# Configuration, Options and Secrets in Real Projects
Worked exercise: reject an unsafe limit
The task is to reject a configured batch size above the supported maximum before processing any records. Use the same project file as the main example. Resolve the options at the operation boundary, catch the specific validation failure, and print a safe message. A hosted application can additionally request validation during startup.
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
var settings = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
["Import:BatchSize"] = "5000"
})
.Build();
var services = new ServiceCollection();
services.AddOptions<ImportOptions>()
.Bind(settings.GetSection("Import"))
.Validate(o => o.BatchSize is >= 1 and <= 500,
"Batch size must be between 1 and 500.");
using var provider = services.BuildServiceProvider();
try
{
var options = provider.GetRequiredService<IOptions<ImportOptions>>().Value;
Console.WriteLine($"Processing {options.BatchSize}");
}
catch (OptionsValidationException)
{
Console.WriteLine("Import configuration rejected");
}
public sealed class ImportOptions
{
public int BatchSize { get; set; } = 100;
}Import configuration rejectedThe integer binds, but fails the domain range. No import begins. A real startup diagnostic can include the non-sensitive key and permitted range without echoing unrelated confidential settings.
C# Configuration, Options and Secrets FAQ
Does later provider precedence apply to an entire section?
Configuration resolves values by keys, so an override can replace one setting while leaving other settings from an earlier provider. Do not assume that adding one key erases the entire section. Inspect the effective contract and test conflicting sources. For lists or complex binding, follow the provider and binder's documented behavior rather than treating configuration as a general JSON merge operation.
Is a bound options object automatically valid?
No. Binding performs mapping and conversion, while validation checks the feature's rules. A successfully bound positive integer can still exceed a safe batch limit. Add clear validation and decide when it runs. Required endpoints and cross-property relationships also need checks. Defaults should be deliberate and must not silently hide missing values that the application requires.
Are user secrets secure enough for production?
They are intended for development and keep values outside the committed project, but they are not a production secret vault or an encryption guarantee. Production needs an approved source with access controls and rotation procedures. The application should still avoid logging retrieved secrets. Separating a secret from source control is useful, but does not resolve every storage and operational concern.
Does IOptionsMonitor recreate services when settings change?
No. It exposes current options and notifications. A service decides whether and how to apply them, and a resource built from an old value may remain unchanged. Some providers do not support automatic reload. Define the consumer's update policy, validate new settings, and test a real change path. Do not promise live reconfiguration merely because a monitor is injected.
C# Configuration, Options and Secrets Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
Which statement about development user secrets is accurate?
C# Configuration, Options and Secrets Quiz
2 questions to check what you learned.
Two providers define Retry:Attempts. Which value normally wins?
- The alphabetically first provider.
- The later provider's value for that key.
- The smallest numeric value.
- The options class default regardless of configuration.
A batch size of 5000 binds to int, but only 1–500 is supported. What is needed?
- Successful binding to int alone proves the value meets the feature's supported range.
- Moving the key into an environment variable without adding range validation.
- A feature validation rule before processing.
- Using a larger integer type while retaining no domain range check.