appsettings.json Explained: ASP.NET Core Configuration, Environments, and Secrets

Last updated:

appsettings.jsonis ASP.NET Core's primary configuration file — a plain JSON document sitting beside Program.cs that holds the settings your app reads at startup and runtime. The default host (IHostBuilder, exposed via WebApplication.CreateBuilder in .NET 6+) loads it automatically, then merges environment-specific overlays like appsettings.Development.jsonon top, followed by User Secrets, environment variables, and command-line arguments. Each source in that chain wins over the ones before it, which is how you swap a local connection string for a production one without changing code. One firm rule: don't put secrets in appsettings.json. The file is committed to source control. Use User Secrets in development and a managed vault (Azure Key Vault, AWS Secrets Manager, HashiCorp Vault) in production.

Need to validate an appsettings.json file before committing it? Paste it into Jsonic's JSON Validator — it pinpoints syntax errors with line and column numbers.

Validate appsettings.json

How appsettings.json is loaded: the configuration provider chain

ASP.NET Core configuration is not a single file — it is an ordered chain of providers that the host stacks on top of each other. The default web host registers them in this order, and each later provider overrides keys set by earlier ones:

#ProviderSourceLoaded when
1JSONappsettings.jsonAlways
2JSONappsettings.{Environment}.jsonIf file exists for current environment
3User Secretssecrets.json under user profileDevelopment only, if UserSecretsId set
4Environment VariablesProcess env (any name, plus DOTNET_/ASPNETCORE_ prefixed)Always
5Command-line--Key=Value args to the processAlways

Because command-line args are last, you can override any setting on the fly:

dotnet run --Logging:LogLevel:Default=Debug --ConnectionStrings:Db="Server=..."

You can add more providers explicitly (Azure Key Vault, AWS Secrets Manager, INI files, a database) by calling builder.Configuration.AddXxx(...) in Program.cs. The position in code determines precedence — providers added later win.

The Logging section: LogLevel hierarchy and providers

Every ASP.NET Core template ships with a Logging section in appsettings.json. It controls which messages reach which sinks. Levels cascade: a category falls back to its parent namespace, then to Default.

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information",
      "Microsoft.EntityFrameworkCore.Database.Command": "Warning",
      "System": "Warning"
    },
    "Console": {
      "LogLevel": {
        "Default": "Information"
      }
    },
    "Debug": {
      "LogLevel": {
        "Default": "Warning"
      }
    }
  }
}

Valid levels, from most to least verbose: Trace, Debug, Information, Warning, Error, Critical, None. The top-level Logging:LogLevel applies to every provider; nested provider-specific blocks (Logging:Console:LogLevel) override it for that sink only — useful when you want verbose console logs locally but only warnings going to a remote sink.

In appsettings.Development.json, raise the defaults to Debug for your own namespaces while keeping framework chatter at Warning:

{
  "Logging": {
    "LogLevel": {
      "Default": "Debug",
      "MyApp": "Trace",
      "Microsoft": "Warning"
    }
  }
}

ConnectionStrings: built-in section and accessing in code

ConnectionStrings is a conventional top-level section that pairs with a dedicated helper method. Anything you put under it is readable two ways:

// appsettings.json
{
  "ConnectionStrings": {
    "DefaultConnection": "Server=localhost;Database=MyApp;Trusted_Connection=True;",
    "Redis": "localhost:6379"
  }
}
// Program.cs or any class with IConfiguration injected
var db = builder.Configuration.GetConnectionString("DefaultConnection");
// Equivalent to:
var db2 = builder.Configuration["ConnectionStrings:DefaultConnection"];

builder.Services.AddDbContext<AppDbContext>(opt =>
    opt.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

For real credentials, leave the value in appsettings.json as a local-only connection string (or omit it entirely) and override in production via environment variable:

# Linux/macOS
export ConnectionStrings__DefaultConnection="Server=prod-db;User Id=...;Password=..."

# Windows PowerShell
$env:ConnectionStrings__DefaultConnection = "Server=prod-db;..."

Note the __(double underscore) — environment variables can't contain colons on most shells, so the configuration system translates __ into : when reading env vars.

Environment-specific files: appsettings.Development.json, appsettings.Production.json

The host reads ASPNETCORE_ENVIRONMENT at startup and looks for a file named appsettings.{Environment}.json. If present, it is merged on top of appsettings.json — same keys overwrite, new keys add, untouched keys remain.

FileLoaded whenTypical contentsCommit?
appsettings.jsonAlwaysSafe defaults, structure, non-secret URLsYes
appsettings.Development.jsonASPNETCORE_ENVIRONMENT=DevelopmentVerbose logs, localhost endpoints, dev feature flagsYes (no secrets)
appsettings.Staging.jsonASPNETCORE_ENVIRONMENT=StagingStaging-specific endpoints, test API keysOptional
appsettings.Production.jsonASPNETCORE_ENVIRONMENT=ProductionProduction endpoints, stricter loggingOptional (prefer env vars / vault)
appsettings.{Custom}.jsonMatches any custom valueQA, Demo, Sandbox — defined by your teamDepends

The merge is shallow per key path. If appsettings.json has { "Logging": { "LogLevel": { "Default": "Info", "Microsoft": "Warning" } } } and appsettings.Development.json has { "Logging": { "LogLevel": { "Default": "Debug" } } }, the merged result is Default=Debug and Microsoft=Warning — the development file overrides only the keys it specifies.

Binding to strongly-typed objects with IOptions<T>

Reading configuration["A:B:C"] as strings everywhere is brittle. The idiomatic approach is to define a POCO class, bind a configuration section to it once at startup, and inject a typed wrapper into the classes that need it.

// appsettings.json
{
  "EmailOptions": {
    "Smtp": {
      "Host": "smtp.example.com",
      "Port": 587,
      "EnableSsl": true
    },
    "FromAddress": "no-reply@example.com",
    "RetryCount": 3
  }
}
// EmailOptions.cs
public sealed class EmailOptions
{
    public SmtpOptions Smtp { get; set; } = new();
    [EmailAddress] public string FromAddress { get; set; } = "";
    [Range(0, 10)] public int RetryCount { get; set; } = 3;
}

public sealed class SmtpOptions
{
    [Required] public string Host { get; set; } = "";
    [Range(1, 65535)] public int Port { get; set; } = 25;
    public bool EnableSsl { get; set; } = true;
}
// Program.cs
builder.Services
    .AddOptions<EmailOptions>()
    .Bind(builder.Configuration.GetSection("EmailOptions"))
    .ValidateDataAnnotations()
    .ValidateOnStart();

// EmailService.cs
public sealed class EmailService(IOptions<EmailOptions> options)
{
    private readonly EmailOptions _opts = options.Value;
    public Task SendAsync(...) { /* use _opts.Smtp.Host etc. */ }
}

Three flavors of the injectable wrapper:

  • IOptions<T> — singleton, values captured at first resolution; does NOT reflect later file changes.
  • IOptionsSnapshot<T> — scoped (per-request in web apps); re-binds at the start of each scope. Use this in controllers.
  • IOptionsMonitor<T> — singleton, but exposes CurrentValue and an OnChange callback that fires when the underlying source changes. Use this in long-lived singletons that need to react to config edits.

Secrets: User Secrets for dev, Azure Key Vault / AWS Secrets Manager for production

The cardinal rule: nothing in appsettings.jsonthat you wouldn't paste into a public Slack channel. API keys, database passwords, signing secrets, OAuth client secrets — all of these belong in a secret store, never in a file that lives in git history.

Development — User Secrets. Run dotnet user-secrets init in your project (it writes a <UserSecretsId> into the .csproj), then add values:

dotnet user-secrets init
dotnet user-secrets set "ConnectionStrings:DefaultConnection" "Server=...;Password=..."
dotnet user-secrets set "Stripe:ApiKey" "sk_test_..."

Secrets are stored as JSON under the user profile (%APPDATA%\Microsoft\UserSecrets on Windows, ~/.microsoft/usersecrets on Linux/macOS) — never inside the project. The default host loads them automatically in the Development environment.

Production — Azure Key Vault. Add the package Azure.Extensions.AspNetCore.Configuration.Secrets and wire it in:

if (!builder.Environment.IsDevelopment())
{
    var vaultUri = new Uri(builder.Configuration["KeyVault:Uri"]!);
    builder.Configuration.AddAzureKeyVault(vaultUri, new DefaultAzureCredential());
}

Key Vault secret names with -- map to : in configuration, so a vault secret ConnectionStrings--Db is read as configuration["ConnectionStrings:Db"] — your code is unchanged.

Production — AWS Secrets Manager. Use Kralizek.Extensions.Configuration.AWSSecretsManager (community) or roll a provider on top of AWSSDK.SecretsManager. Same shape: a configuration provider added to the chain in Program.cs.

Hot reload: reloadOnChange and IOptionsMonitor

The default host registers JSON files with reloadOnChange: true:

// What the default host effectively does
config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true);
config.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true);

A FileSystemWatcher notices edits and rebuilds the configuration root in place. Whether your code sees the change depends on how it reads values:

Read patternPicks up edits?Why
configuration["Key"]YesReads from the live IConfiguration root every call
IOptions<T>.ValueNoSingleton; binds once at first access
IOptionsSnapshot<T>.ValueYes, per scopeRe-binds at the start of each request
IOptionsMonitor<T>.CurrentValueYes, immediateSingleton wrapper that re-reads on change; fires OnChange
public sealed class FeatureFlagService(IOptionsMonitor<FeatureFlags> monitor)
{
    private FeatureFlags _flags = monitor.CurrentValue;

    public FeatureFlagService(IOptionsMonitor<FeatureFlags> monitor) : this(monitor)
    {
        monitor.OnChange(updated => _flags = updated);
    }

    public bool IsEnabled(string name) => _flags.Map.GetValueOrDefault(name);
}

Reload only works for file-based providers. Environment variables are snapshotted at process start. Key Vault providers can be configured with a polling interval (AzureKeyVaultConfigurationOptions.ReloadInterval) to refresh secrets on a schedule.

Key terms

appsettings.json
The primary JSON configuration file in an ASP.NET Core project, loaded automatically by the default host from the content root. Holds non-secret settings — logging, connection strings, custom options.
Configuration provider
A source of configuration key-value pairs that contributes to the merged IConfiguration root. JSON files, environment variables, command-line args, User Secrets, and Key Vault are all providers. Order of registration determines precedence — later wins.
Hierarchical key
A flattened key like Logging:LogLevel:Default that addresses a nested JSON path. The colon is the delimiter. Environment variables substitute __ for : because most shells reject colons in variable names.
ASPNETCORE_ENVIRONMENT
The environment variable that tells the host which environment is active. Conventional values are Development, Staging, Production; the value is a free-form string so custom environments are supported. Drives which appsettings.{Environment}.json file loads and several built-in behaviors (dev exception page, User Secrets).
User Secrets
A development-only secret store under the user profile, identified by a UserSecretsId in the .csproj. Loaded by the host in the Development environment only. Managed with the dotnet user-secrets CLI. Never committed to source control.
IOptions pattern
The recommended approach to consuming configuration in .NET: bind a section to a strongly-typed class at startup, then inject IOptions<T>, IOptionsSnapshot<T>, or IOptionsMonitor<T> into consumers. Supports data-annotation validation via ValidateDataAnnotations.

Frequently asked questions

What is appsettings.json used for?

appsettings.json is the primary configuration file for an ASP.NET Core application. It holds non-secret settings the app needs at startup and runtime: logging levels, connection strings, feature flags, allowed hosts, third-party API endpoints, and any custom options your code reads. The default host (WebApplication.CreateBuilder in .NET 6+) loads it automatically from the content root, then merges environment-specific overlays (appsettings.{Environment}.json), User Secrets in Development, environment variables, and command-line arguments — in that order, with later sources overriding earlier ones. It is plain JSON, version-controlled with the rest of the project, and intentionally not the place for secrets like API keys, passwords, or connection strings that contain credentials.

What is the difference between appsettings.json and appsettings.Development.json?

appsettings.json holds the base configuration that applies in every environment. appsettings.Development.json (and appsettings.Production.json, appsettings.Staging.json) holds overrides loaded only when ASPNETCORE_ENVIRONMENT matches the file’s suffix. The host loads the base file first, then merges the environment file on top: keys present in both win from the environment file; keys only in the base file remain. A common pattern is to put verbose logging, local connection strings, and developer-friendly defaults in appsettings.Development.json, while keeping production-safe defaults in the base appsettings.json. The base file is always committed; the Development file is typically committed (no secrets); a Production file may or may not be committed depending on whether it contains environment-specific endpoints.

How do I access a config value in code?

Three ways. (1) Inject IConfiguration and read by key: configuration["Logging:LogLevel:Default"] returns a string. The colon (:) is the hierarchical separator that walks into nested JSON objects. (2) Inject IConfiguration and call GetSection("MySection").Get<MyOptions>() to deserialize a subtree into a strongly-typed object. (3) The recommended pattern — register options at startup with builder.Services.Configure<MyOptions>(builder.Configuration.GetSection("MySection")) and inject IOptions<MyOptions>, IOptionsSnapshot<MyOptions>, or IOptionsMonitor<MyOptions> into your classes. Strongly-typed binding is the idiomatic .NET approach and gives you compile-time safety, validation via data annotations, and clean separation between configuration shape and consumers.

Where should I store secrets in ASP.NET Core?

Never in appsettings.json — the file is committed to source control and visible to anyone with repo access. In Development, use User Secrets (dotnet user-secrets set "Key" "value"), which stores values in a JSON file under the user profile (%APPDATA%\Microsoft\UserSecrets on Windows, ~/.microsoft/usersecrets on Linux/macOS) and is automatically loaded by the host in the Development environment only. In Production, use a managed secret store: Azure Key Vault via the Azure.Extensions.AspNetCore.Configuration.Secrets provider, AWS Secrets Manager via AWSSDK.Extensions.NETCore.Setup, HashiCorp Vault, or your cloud’s equivalent. These integrate as configuration providers so your code still reads configuration["MyKey"] without changes — only the source differs. Environment variables are an acceptable middle ground for container deployments.

Can appsettings.json have comments?

Technically yes, in ASP.NET Core. The default JSON configuration provider uses System.Text.Json with JsonCommentHandling.Skip, so single-line and multi-line comments are tolerated without errors. However, the RFC 8259 JSON spec does not allow comments, so many tools that touch the file (formatters, schema validators, generic JSON parsers in other languages, copy-paste destinations) will reject or strip them. The safe, portable practice is to keep appsettings.json as strict JSON and document settings in a README or an external schema file instead. If you must annotate inline, prefer a sibling key with a clear prefix like "_comment" to signal intent. See our JSON Comments guide for the broader picture across ecosystems.

What is the ASPNETCORE_ENVIRONMENT variable?

ASPNETCORE_ENVIRONMENT is the environment variable that tells the ASP.NET Core host which environment the app is running in. The three conventional values are Development, Staging, and Production — but the value is just a string, so you can define custom environments like QA or Demo. The host reads this variable at startup and uses it to pick which appsettings.{Environment}.json file to merge, whether to enable User Secrets (Development only), whether to show the developer exception page (Development only), and which UseEnvironment branches in Program.cs to run. In Visual Studio and Rider, launchSettings.json sets this variable per launch profile. In Docker or Kubernetes, set it as a container environment variable. In Azure App Service, set it under Configuration → Application settings.

How do I bind appsettings.json to a class?

Define a plain C# class with public properties matching the JSON keys. In Program.cs, call builder.Services.Configure<MyOptions>(builder.Configuration.GetSection("MyOptions")). Then inject IOptions<MyOptions> (singleton snapshot, never updates), IOptionsSnapshot<MyOptions> (per-request, reflects current values), or IOptionsMonitor<MyOptions> (real-time updates with OnChange callbacks) into any class that needs the values. For validation, add data annotations to the properties and call .ValidateDataAnnotations().ValidateOnStart() on the OptionsBuilder — the app will fail to start if configuration is invalid, which is far better than runtime surprises. Binding is case-insensitive by default and walks nested objects automatically, so a JSON section with sub-objects maps cleanly to a class with nested properties.

Does ASP.NET Core reload appsettings.json without restarting?

Yes, by default. The host calls AddJsonFile("appsettings.json", optional: true, reloadOnChange: true), which uses a FileSystemWatcher to detect changes and rebuilds the IConfiguration root in place. Code that reads via IConfiguration["Key"] or IOptionsSnapshot<T> / IOptionsMonitor<T> sees the new value on the next read. Classes that depend on IOptions<T> (the non-snapshot variant) cache the values at construction and will NOT see changes — that is by design for performance. To react to a change, inject IOptionsMonitor<T> and register an OnChange callback. Note: reload only applies to file-based providers; if the value originated from an environment variable or Key Vault, the trigger is different (vault providers typically poll on a schedule).

Further reading and primary sources