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.
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:
| # | Provider | Source | Loaded when |
|---|---|---|---|
| 1 | JSON | appsettings.json | Always |
| 2 | JSON | appsettings.{Environment}.json | If file exists for current environment |
| 3 | User Secrets | secrets.json under user profile | Development only, if UserSecretsId set |
| 4 | Environment Variables | Process env (any name, plus DOTNET_/ASPNETCORE_ prefixed) | Always |
| 5 | Command-line | --Key=Value args to the process | Always |
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.
| File | Loaded when | Typical contents | Commit? |
|---|---|---|---|
appsettings.json | Always | Safe defaults, structure, non-secret URLs | Yes |
appsettings.Development.json | ASPNETCORE_ENVIRONMENT=Development | Verbose logs, localhost endpoints, dev feature flags | Yes (no secrets) |
appsettings.Staging.json | ASPNETCORE_ENVIRONMENT=Staging | Staging-specific endpoints, test API keys | Optional |
appsettings.Production.json | ASPNETCORE_ENVIRONMENT=Production | Production endpoints, stricter logging | Optional (prefer env vars / vault) |
appsettings.{Custom}.json | Matches any custom value | QA, Demo, Sandbox — defined by your team | Depends |
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 exposesCurrentValueand anOnChangecallback 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 pattern | Picks up edits? | Why |
|---|---|---|
configuration["Key"] | Yes | Reads from the live IConfiguration root every call |
IOptions<T>.Value | No | Singleton; binds once at first access |
IOptionsSnapshot<T>.Value | Yes, per scope | Re-binds at the start of each request |
IOptionsMonitor<T>.CurrentValue | Yes, immediate | Singleton 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
IConfigurationroot. 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:Defaultthat 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 whichappsettings.{Environment}.jsonfile 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
UserSecretsIdin the .csproj. Loaded by the host in the Development environment only. Managed with thedotnet user-secretsCLI. 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>, orIOptionsMonitor<T>into consumers. Supports data-annotation validation viaValidateDataAnnotations.
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
- Microsoft Docs — Configuration in ASP.NET Core — Authoritative reference for providers, precedence, and binding
- Microsoft Docs — Use multiple environments in ASP.NET Core — How ASPNETCORE_ENVIRONMENT drives environment-specific behavior
- Microsoft Docs — Options pattern in ASP.NET Core — IOptions, IOptionsSnapshot, IOptionsMonitor, and validation
- Microsoft Docs — Safe storage of app secrets in development — Setting up and using the User Secrets manager
- Microsoft Docs — Azure Key Vault configuration provider — Wiring Key Vault into the configuration chain for production