The Configuration System — IConfiguration and Providers

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");
Note: The colon separator (:) 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.
Tip: Prefer the 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.
Warning: The configuration indexer returns 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.

🧠 Test Yourself

Both appsettings.json and an environment variable define ConnectionStrings:Default. Which wins, and why?