ASP.NET Core’s configuration system is a unified API over multiple sources — JSON files, environment variables, command-line arguments, in-memory dictionaries, Azure Key Vault, and more. All sources are merged into a single IConfiguration instance with a predictable override hierarchy: later sources win. This design means you commit non-sensitive defaults in appsettings.json, override locally with User Secrets, override in CI with environment variables, and override in production with a vault service — all without changing any application code.
Reading Configuration Values
// IConfiguration is available via DI everywhere in ASP.NET Core
// (injected into Program.cs as builder.Configuration)
// ── appsettings.json structure ────────────────────────────────────────────
// {
// "AllowedHosts": "*",
// "ConnectionStrings": {
// "Default": "Server=(localdb)\\mssqllocaldb;Database=BlogApp_Dev"
// },
// "Logging": {
// "LogLevel": {
// "Default": "Information",
// "Microsoft.AspNetCore": "Warning"
// }
// },
// "Features": {
// "EnableDarkMode": true,
// "MaxUploadSizeMb": 10
// }
// }
// ── Reading with indexer — returns string or null ─────────────────────────
string? host = config["ConnectionStrings:Default"]; // colon = hierarchy separator
string? lvl = config["Logging:LogLevel:Default"]; // deeply nested
// ── GetValue with type conversion and default ─────────────────────────────
int maxUpload = config.GetValue<int>("Features:MaxUploadSizeMb", defaultValue: 5);
bool darkMode = config.GetValue<bool>("Features:EnableDarkMode", defaultValue: false);
string connStr = config.GetValue<string>("ConnectionStrings:Default")
?? throw new InvalidOperationException("Connection string not configured.");
// ── GetConnectionString — shorthand for ConnectionStrings section ──────────
string? cs = config.GetConnectionString("Default"); // same as above
// ── GetSection — navigate to a sub-section ────────────────────────────────
IConfigurationSection featuresSection = config.GetSection("Features");
bool dark = featuresSection.GetValue<bool>("EnableDarkMode");
:) navigates the configuration hierarchy in JSON. config["Logging:LogLevel:Default"] reads the JSON path Logging → LogLevel → Default. When configuration comes from environment variables, the double underscore (__) replaces the colon because colons are not valid in environment variable names on Linux: Logging__LogLevel__Default=Warning. ASP.NET Core’s environment variable provider automatically converts __ to :, making the two representations interchangeable in the configuration system.IOptions<T> pattern (Chapter 18) over direct IConfiguration injection in application services. Inject IConfiguration directly only in Program.cs bootstrapping code and in infrastructure setup. Direct IConfiguration access in services means string keys scattered throughout the code, no compile-time safety, and no validation. IOptions<SmtpOptions> is strongly typed, validated at startup, and immediately clear about which configuration section the service depends on.null for missing keys — it does not throw. This means a typo in a configuration key silently returns null, which can cause a NullReferenceException far from the source. Always use GetValue<T>("key", defaultValue) when a default is acceptable, or check for null and throw a descriptive exception, or use IOptions<T> with ValidateOnStart() which validates all required fields at startup.Default Loading Order
| Source | When Loaded | Overrides |
|---|---|---|
| appsettings.json | Always | Nothing (base) |
| appsettings.{env}.json | When env matches | appsettings.json |
| User Secrets | Development only | Both appsettings files |
| Environment variables | Always | All file sources |
| Command-line args | When provided | Everything else |
Common Mistakes
Mistake 1 — Injecting IConfiguration into application services (use IOptions instead)
❌ Wrong — string keys, no validation, hidden dependencies:
public class EmailService(IConfiguration config)
{
private string _host = config["Smtp:Host"]!; // magic string, no validation!
✅ Correct — inject IOptions<SmtpOptions> with bound and validated configuration.
Mistake 2 — Not checking for null after indexer access
❌ Wrong — silent null propagates to a later NullReferenceException:
string host = config["Smtp:Host"]; // returns null if missing, no warning!
✅ Correct — use GetValue<string> with a default, or null-check and throw immediately.