Articles › ASP.NET Core

Configuration in ASP.NET Core: appsettings, Environments, Secrets and the Options Pattern

Master ASP.NET Core configuration: providers and precedence, environment-specific settings, user secrets, environment variables, IOptions vs IOptionsSnapshot vs IOptionsMonitor and validation.

Every application needs settings: connection strings, API keys, feature flags, limits. Hard-coding them makes deployment painful and leaks secrets into source control. ASP.NET Core has a flexible configuration system that reads from many sources and binds values to strongly typed classes. This guide shows how it works and how to use it safely.

Configuration providers and precedence

WebApplication.CreateBuilder loads configuration from several sources, in order — later sources override earlier ones:

  1. appsettings.json
  2. appsettings.{Environment}.json (e.g. appsettings.Production.json)
  3. User secrets (Development only)
  4. Environment variables
  5. Command-line arguments

So a value in appsettings.json can be overridden on the server by an environment variable without changing any file — exactly what you want for deployment.

Reading values

{
  "ConnectionStrings": {
    "Default": "Server=.;Database=Portfolio;Trusted_Connection=True;TrustServerCertificate=True"
  },
  "Jwt": {
    "Issuer": "PortfolioApi",
    "Audience": "PortfolioClient",
    "AccessTokenMinutes": 30
  },
  "Features": {
    "EnableComments": true
  }
}
var connection = builder.Configuration.GetConnectionString("Default");
var issuer = builder.Configuration["Jwt:Issuer"];                 // ":" separates sections
var minutes = builder.Configuration.GetValue<int>("Jwt:AccessTokenMinutes");

This is fine in Program.cs, but sprinkling IConfiguration["Some:Key"] strings through services is fragile: typos fail silently at runtime and nothing documents which settings exist. That's what the Options pattern fixes.

The Options pattern

Define a class that mirrors a section:

public class JwtOptions
{
    public const string SectionName = "Jwt";

    [Required] public string Issuer { get; init; } = "";
    [Required] public string Audience { get; init; } = "";
    [Required, MinLength(32)] public string Key { get; init; } = "";
    [Range(1, 1440)] public int AccessTokenMinutes { get; init; } = 30;
}

Register and validate it:

builder.Services.AddOptions<JwtOptions>()
    .BindConfiguration(JwtOptions.SectionName)
    .ValidateDataAnnotations()
    .Validate(o => o.Issuer != o.Audience, "Issuer and Audience must differ.")
    .ValidateOnStart();

ValidateOnStart is important: a missing key makes the app fail at startup with a clear message, instead of crashing on the first login hours later.

Inject it where needed:

public class TokenService(IOptions<JwtOptions> options)
{
    private readonly JwtOptions _jwt = options.Value;
    // _jwt.Issuer, _jwt.AccessTokenMinutes …
}

IOptions vs IOptionsSnapshot vs IOptionsMonitor

Interface Lifetime Reloads on change Use when
IOptions<T> singleton ❌ settings fixed for the app's life (most cases)
IOptionsSnapshot<T> scoped ✅ per request settings may change and you're in a scoped service
IOptionsMonitor<T> singleton ✅ live, with OnChange singletons/background services that must react to changes
public class FeatureGate(IOptionsMonitor<FeatureOptions> monitor)
{
    public bool CommentsEnabled => monitor.CurrentValue.EnableComments;
}

JSON files are reloaded automatically when they change (reloadOnChange: true by default), so IOptionsMonitor lets you toggle features without restarting.

Environments

ASP.NET Core reads the environment from ASPNETCORE_ENVIRONMENT (Development, Staging, Production). Use it to vary behavior:

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

And put environment-specific overrides in appsettings.Development.json or appsettings.Production.json. Keep the base file as the shared default.

Secrets: never in source control

Development: User Secrets

dotnet user-secrets init
dotnet user-secrets set "Jwt:Key" "a-long-random-development-key-of-32+-chars"

Secrets are stored in your user profile, outside the project folder, and loaded automatically in Development.

Production: environment variables or a vault

Environment variables use double underscores for nesting:

Jwt__Key=super-secret-production-key
ConnectionStrings__Default="Server=db;Database=Portfolio;User Id=app;Password=..."

On IIS you can set them in web.config (<environmentVariables>) or the application pool; in Docker, with -e or a secrets mechanism; in the cloud, use Azure Key Vault, AWS Secrets Manager or similar providers.

Warning: If a secret was ever committed, consider it compromised and rotate it. Removing it from the latest commit doesn't remove it from Git history.

Custom configuration sources

Configuration is extensible. You can add other JSON files, in-memory values for tests, or even a database-backed provider:

builder.Configuration
    .AddJsonFile("features.json", optional: true, reloadOnChange: true)
    .AddInMemoryCollection(new Dictionary<string, string?> { ["Features:Beta"] = "false" });

Testing with configuration

Options classes make unit tests trivial — no configuration system needed:

var options = Options.Create(new JwtOptions
{
    Issuer = "test", Audience = "tests", Key = new string('k', 32), AccessTokenMinutes = 5
});
var service = new TokenService(options);

In integration tests, WebApplicationFactory lets you override settings with ConfigureAppConfiguration.

Best practices

  1. Group related settings into sections and bind them to options classes.
  2. Validate on startup; fail fast with clear messages.
  3. Keep secrets out of appsettings.json committed to Git.
  4. Use environment variables for deployment-specific values.
  5. Prefer IOptions<T> unless you truly need reloading.
  6. Don't inject IConfiguration into business services — inject options.

Summary

ASP.NET Core layers configuration from JSON files, environment-specific files, secrets, environment variables and command-line arguments, with later sources winning. Bind sections to strongly typed options classes, validate them at startup, choose IOptions, IOptionsSnapshot or IOptionsMonitor based on whether values can change, and keep secrets in user secrets during development and environment variables or a vault in production.

FAQ

Why isn't my environment-specific file loaded?

Check ASPNETCORE_ENVIRONMENT. The file name must match exactly, e.g. appsettings.Production.json for Production.

How do I set nested values with environment variables?

Use double underscores: Jwt__Key maps to Jwt:Key. On Linux, : isn't allowed in variable names.

Can I change settings without restarting?

For JSON-file settings consumed via IOptionsMonitor or IOptionsSnapshot, yes. Environment variables require a restart.