appsettings.json — Structure, Hierarchy and Type Binding

appsettings.json is the committed baseline configuration file for an ASP.NET Core application. It uses standard JSON with a nested structure that maps directly to the hierarchical configuration key system. Understanding how to structure complex configuration objects, bind them to C# classes, and work with arrays and connection strings is the practical foundation for all configuration work in the Web API chapters.

appsettings.json Structure and Binding

// ── appsettings.json ───────────────────────────────────────────────────────
// {
//   "ConnectionStrings": {
//     "Default": "Server=(localdb)\\mssqllocaldb;Database=BlogApp;Trusted_Connection=True",
//     "Redis":   "localhost:6379"
//   },
//   "JwtSettings": {
//     "Issuer":        "https://api.blogapp.com",
//     "Audience":      "https://blogapp.com",
//     "SecretKey":     "REPLACE_IN_PRODUCTION_WITH_SECURE_VALUE",
//     "ExpiryMinutes": 60
//   },
//   "Cors": {
//     "AllowedOrigins": [
//       "http://localhost:4200",
//       "https://blogapp.com"
//     ]
//   },
//   "Logging": {
//     "LogLevel": {
//       "Default":           "Information",
//       "Microsoft.AspNetCore": "Warning",
//       "Microsoft.EntityFrameworkCore.Database.Command": "Warning"
//     }
//   }
// }

// ── Strongly-typed binding with GetSection().Get() ────────────────────────
public class JwtSettings
{
    public string Issuer        { get; init; } = string.Empty;
    public string Audience      { get; init; } = string.Empty;
    public string SecretKey     { get; init; } = string.Empty;
    public int    ExpiryMinutes { get; init; } = 60;
}

// In Program.cs — bind and validate
builder.Services
    .AddOptions<JwtSettings>()
    .BindConfiguration("JwtSettings")
    .ValidateDataAnnotations()
    .ValidateOnStart();

// Or as a one-liner (no validation):
var jwtSettings = builder.Configuration
    .GetSection("JwtSettings")
    .Get<JwtSettings>() ?? throw new InvalidOperationException("JwtSettings not configured.");

// ── Binding arrays ─────────────────────────────────────────────────────────
string[] allowedOrigins = builder.Configuration
    .GetSection("Cors:AllowedOrigins")
    .Get<string[]>() ?? Array.Empty<string>();
Note: GetSection("JwtSettings").Get<JwtSettings>() returns null if the section does not exist in any loaded configuration source. Always use the null-coalescing operator with a descriptive exception to fail fast: ?? throw new InvalidOperationException("JwtSettings section is missing"). The IOptions pattern with ValidateOnStart() is better for required configuration because it validates at startup, but for one-off bootstrapping in Program.cs the direct binding approach is acceptable.
Tip: Structure your configuration in appsettings.json to match your options classes — one top-level section per configuration concern. Use PascalCase for section names to match C# property naming conventions. Keep the base appsettings.json complete but safe for development: use non-sensitive placeholder values for secrets ("SecretKey": "REPLACE_IN_PRODUCTION"), localhost URLs, and LocalDB connection strings. Production values arrive through environment variables or vault providers.
Warning: Never commit real production secrets to appsettings.json or appsettings.Production.json. Even if the repository is private today, it may become public, be cloned to a developer machine, or be accessed by someone who does not need production access. Rotate any secret that has been committed to source control immediately. Use a .gitignore rule to prevent appsettings.Production.json from ever being committed: appsettings.Production.json in .gitignore.

appsettings.Development.json

// appsettings.Development.json — overrides for local development
// This file IS committed to source control (non-sensitive dev settings)
// {
//   "Logging": {
//     "LogLevel": {
//       "Default":    "Debug",
//       "BlogApp":    "Debug",
//       "Microsoft.EntityFrameworkCore.Database.Command": "Information"
//     }
//   },
//   "ConnectionStrings": {
//     "Default": "Server=(localdb)\\mssqllocaldb;Database=BlogApp_Dev;Trusted_Connection=True"
//   },
//   "JwtSettings": {
//     "SecretKey":     "dev-only-secret-key-replace-in-production-12345"
//   }
// }

// When ASPNETCORE_ENVIRONMENT=Development:
// - appsettings.json loads first (base values)
// - appsettings.Development.json loads second (overrides logging level, DB, JWT key)
// - User Secrets load third (overrides anything else for the developer)
// Net result: developer-friendly settings without touching production config

Common Mistakes

Mistake 1 — Committing appsettings.Production.json with real secrets

❌ Wrong — real API keys, connection strings, JWT secrets in source control.

✅ Correct — only non-sensitive defaults in appsettings.json; secrets via environment variables or vault.

Mistake 2 — Using GetSection without checking for null

❌ Wrong — .Get<T>() returns null when section is missing; downstream NullReferenceException:

✅ Correct — always null-check or use IOptions with ValidateOnStart().

🧠 Test Yourself

You have a JwtSettings section in both appsettings.json and appsettings.Development.json. The development file only overrides SecretKey. What values does JwtSettings have in Development?