A focused ASP.NET Core Minimal API companion demonstrating layered configuration, a project-specific environment-variable prefix, command-line precedence, strongly typed options, startup validation, User Secrets metadata, a lightweight feature flag, and an allowlisted Development-only diagnostics endpoint that never returns the configured API-key value.
.NET 8 Configuration & Secrets Management: Typed Options, User Secrets & Feature Flags
The full tutorial is written for ASP.NET Core 8.
This companion targets .NET 10 because that is the current DOTNET GUIDE sample SDK.
appsettings.json
-> appsettings.Development.json
-> User Secrets in Development
-> default environment variables
-> CFGPLAY_ prefixed environment variables
-> command-line arguments
-> validated options
The sample re-adds the command-line provider after CFGPLAY_, so explicit
command-line arguments remain the highest-priority application overrides.
TimeoutSeconds = 10
AdvancedSearch = false
TimeoutSeconds = 20
AdvancedSearch = true
App:ApiKey is intentionally empty in committed JSON.
The app will fail startup until a higher-priority configuration provider supplies a valid value.
From the application project folder:
dotnet user-secrets set `
"App:ApiKey" `
"local-demo-key-1234"Secret Manager keeps development values outside the project tree and source control, but the stored values are not encrypted and Secret Manager is not a production secret store.
Do not copy or commit secrets.json.
The application adds:
AddEnvironmentVariables("CFGPLAY_")The prefix is stripped.
For example:
CFGPLAY_App__TimeoutSeconds
maps to:
App:TimeoutSeconds
Double underscore is the portable hierarchy separator for environment-variable configuration.
Run:
$env:DOTNET_ENVIRONMENT = "Development"
$env:CFGPLAY_App__ApiKey = "local-demo-key-1234"
$env:CFGPLAY_App__TimeoutSeconds = "30"
dotnet run `
--project .\src\ConfigPrecedenceMinimal\ConfigPrecedenceMinimal.csproj `
--configuration Release `
-- `
--App:TimeoutSeconds=40 `
--Features:AdvancedSearch=falseThe final timeout is:
40
because the command-line provider is deliberately added last.
AppOptions is validated using:
- DataAnnotations;
- a custom
IValidateOptions<AppOptions>; ValidateOnStart.
The app rejects:
- a missing or too-short API key;
- a malformed service URL;
- HTTP when
RequireHttpsis true; - obvious placeholder API-key values.
The Development-only endpoint is:
GET /dev/config
It returns an explicit allowlist:
environment
serviceBaseUrl
timeoutSeconds
advancedSearch
apiKey
apiKeyConfigured
The API-key value is always:
[REDACTED]
The sample does not:
- enumerate all of
IConfiguration; - expose
GetDebugView()over HTTP; - guess sensitivity from key names;
- serialize provider values.
A generic redaction keyword list is not a security boundary.
The sample uses:
IOptions<AppOptions>
IOptionsSnapshot<FeatureFlagOptions>
IOptions<T> doesn't support reading changed configuration values after the
app has started.
IOptionsSnapshot<T> is scoped and recomputes options per scope when accessed.
It only reflects post-start configuration changes when the underlying provider
supports those changes.
The full tutorial discusses IOptionsMonitor<T> and named options.
- Azure Key Vault;
- AWS Secrets Manager;
- HashiCorp Vault;
- named options;
- live file reload;
- IOptionsMonitor callbacks;
- raw provider debug trees;
- external services;
- database configuration;
- SMTP configuration.
dotnet restore `
.\ConfigPrecedenceMinimal.slnx
dotnet build `
.\ConfigPrecedenceMinimal.slnx `
--configuration Release `
--no-restore
dotnet test `
.\ConfigPrecedenceMinimal.slnx `
--configuration Release `
--no-buildConfigPrecedenceMinimal.slnx
README.md
src/
`-- ConfigPrecedenceMinimal/
|-- ConfigPrecedenceMinimal.csproj
|-- Program.cs
|-- appsettings.json
|-- appsettings.Development.json
|-- Diagnostics/
| `-- SafeConfigSnapshot.cs
|-- Options/
| |-- AppOptions.cs
| `-- FeatureFlagOptions.cs
`-- Validation/
`-- AppOptionsValidator.cs
tests/
`-- ConfigPrecedenceMinimal.Tests/
|-- ConfigPrecedenceMinimal.Tests.csproj
`-- ConfigurationTests.cs
- Target framework: .NET 10
- Application NuGet dependencies: none
- Test count: 8
- External services: none
- Required secret committed to repo: none
- User Secrets: local Development only
- Development diagnostics: allowlisted and redacted
- Last reviewed: 2026-08-07