The exact same compiled binary should behave differently on your laptop than it does in production — on purpose.
You just learned that configuration can be layered from multiple providers, with later sources overriding earlier ones. But that raises an obvious question: how does the app know it's running in Development versus Production in the first place? Nobody wants their local debugging session accidentally hitting the production database, and nobody wants to redeploy a slightly-edited copy of the source code every time they move from a developer's machine to a staging server to production.
.NET solves this with a single, simple idea: the app reads its current environment name from one environment variable, and then automatically layers in an extra, environment-specific settings file on top of the base configuration you already know. Same code, same compiled binary — different values, chosen automatically based on where it's running.
In this lesson, you'll learn how .NET detects which environment it's running in, how appsettings.{Environment}.json files layer on top of the base appsettings.json, how to branch behavior in code with IHostEnvironment, and how secrets fit into this picture.
An environment is just a name — usually Development, Staging, or Production — that tells your app "here is the kind of place you're currently running." Environment-based configuration is the practice of loading different settings automatically depending on that name, without changing a single line of code.
.NET's generic host reads the environment name from the DOTNET_ENVIRONMENT environment variable (ASP.NET Core apps also honor ASPNETCORE_ENVIRONMENT, which takes priority when both are set) at startup, exposes it through the IHostEnvironment interface, and uses it to automatically add an extra JSON configuration provider — appsettings.{EnvironmentName}.json — layered on top of the base appsettings.json in the provider stack you already learned about.
appsettings.json before every deploy — all fragile, all error-prone, all one mistake away from pointing production traffic at a test database (or worse, the other way around)..NET builds environment awareness directly into the generic host. You set one environment variable per machine (once, at the OS or deployment-platform level), and the exact same compiled DLL automatically loads the matching appsettings.{Environment}.json file layered on top of the base file. The binary you tested in staging is bit-for-bit the same binary you deploy to production — only the environment name, and therefore the configuration values it loads, differ.
appsettings.json ← base defaults, all environments
↓ overridden by
appsettings.{Environment}.json ← NEW: only loaded for the current environment
↓ overridden by
Environment Variables ← deploy-time / secret overrides
↓ overridden by
Command-Line Arguments ← highest priority, wins over everything
Nothing about the override mechanism from the previous lesson has changed — this is the exact same "later provider wins, per key" rule. The only new piece is that which environment-specific file gets added to the stack is decided automatically, based on one environment variable read the instant your app starts.
export DOTNET_ENVIRONMENT=Staging
dotnet run
Production — a deliberate "fail safe," not "fail loud," choice: an app that forgets to set an environment behaves as if it's in the most locked-down, least-verbose mode.var builder = Host.CreateApplicationBuilder(args);
// Environment is "Staging" → also loads appsettings.Staging.json, if it exists
appsettings.Staging.json doesn't exist on disk, this is not an error — it's simply skipped, and only the base appsettings.json applies.if (builder.Environment.IsDevelopment())
{
// e.g. enable a verbose, human-readable console logger
}
builder.Environment implements IHostEnvironment, giving you EnvironmentName, plus convenience checks like IsDevelopment(), IsStaging(), and IsProduction().appsettings.json (base defaults, checked into source control):
{
"Api": {
"BaseUrl": "https://api-sandbox.example.com",
"TimeoutSeconds": 30
},
"Logging": {
"LogLevel": { "Default": "Information" }
}
}
appsettings.Development.json (only loaded when DOTNET_ENVIRONMENT=Development):
{
"Logging": {
"LogLevel": { "Default": "Debug" }
}
}
appsettings.Production.json (only loaded when DOTNET_ENVIRONMENT=Production):
{
"Api": {
"BaseUrl": "https://api.example.com"
},
"Logging": {
"LogLevel": { "Default": "Warning" }
}
}
The same compiled app, run with two different environment variables, ends up reading two entirely different Api:BaseUrl and log-level values — with zero code changes:
// On a developer's machine
// $ export DOTNET_ENVIRONMENT=Development && dotnet run
// → Api:BaseUrl = "https://api-sandbox.example.com" (from base file — Development.json doesn't override it)
// → Logging level = Debug
// On the production server
// $ export DOTNET_ENVIRONMENT=Production && dotnet run
// → Api:BaseUrl = "https://api.example.com" (overridden by Production.json)
// → Logging level = Warning
A background worker that syncs inventory from a supplier's API needs to hit a sandbox endpoint locally and the real one in production — and it should refuse to start if it somehow finds itself in Production without a real API key configured:
var builder = Host.CreateApplicationBuilder(args);
Console.WriteLine($"Starting InventorySync in {builder.Environment.EnvironmentName} mode");
var supplierSettings = new SupplierApiSettings();
builder.Configuration.GetSection("SupplierApi").Bind(supplierSettings);
if (builder.Environment.IsProduction() && string.IsNullOrWhiteSpace(supplierSettings.ApiKey))
{
// Fail fast and loud — better to crash on startup than silently sync nothing in production
throw new InvalidOperationException("SupplierApi:ApiKey is required in Production but was not configured.");
}
builder.Services.AddSingleton(supplierSettings);
builder.Services.AddHostedService<InventorySyncWorker>();
using var host = builder.Build();
await host.RunAsync();
public class SupplierApiSettings
{
public string BaseUrl { get; set; } = string.Empty;
public string ApiKey { get; set; } = string.Empty;
}
Locally, a developer's appsettings.Development.json points SupplierApi:BaseUrl at a sandbox and leaves ApiKey blank (the sandbox doesn't need one) — nothing crashes, because that startup check only applies in Production. On the real server, the ops team sets SupplierApi__ApiKey as a real environment variable, and the same check now passes.
ValidateOnStart(), covered in lesson 127) — instead of a hand-rolled if check like above, you can declare the requirement declaratively and let the framework enforce it at startup for every setting at once.
Think of your compiled app as a single actor who performs the exact same script every night. The theater doesn't rewrite the script for each city — but before stepping on stage, the actor checks a small card backstage that says which city they're in tonight, and puts on the costume that matches. The lines never change; only the costume, chosen by a quick lookup, does.
The environment variable is that backstage card. The environment-specific appsettings.{Environment}.json file is the costume. The script — your compiled code — never changes at all.
Host.CreateApplicationBuilder checks DOTNET_ENVIRONMENT (and, for ASP.NET Core-flavored hosts, ASPNETCORE_ENVIRONMENT, which takes precedence if both are present) immediately at startup, since the file name of the second JSON provider literally depends on this value."Production".appsettings.json (added first, required to load without error), then one for appsettings.{EnvironmentName}.json (added second, and marked optional — a missing file is not a startup failure).IHostEnvironment instance and registered as a singleton, so any class in your app can inject IHostEnvironment and ask EnvironmentName or call IsDevelopment() — the same information builder.Environment exposes during startup is available anywhere after the app is built.Development, Staging, and Production are conventional names, not magic keywords the framework specially understands beyond providing a matching Is...() helper method for each. You can invent any environment name you like — "QA", "LoadTest", "Demo" — set DOTNET_ENVIRONMENT to it, and .NET will happily look for an appsettings.QA.json file. There's no hardcoded list of "valid" environments to choose from.
An appsettings.Production.json file sitting on disk (or worse, checked into source control) is still just a text file — it's a mechanism for values that legitimately differ per environment, not a vault for values that must stay confidential. A staging URL is environment-specific but not secret; a database password is both. Genuinely sensitive values belong in environment variables set directly on the deployment platform, or a dedicated secret manager — never inside any appsettings*.json file, environment-specific or not.
Assuming that because the file is named "Production," it's somehow safer to write a real production database password into it. It's committed to source control exactly like every other appsettings*.json file, and every developer with repository access can read it.
Keep environment-specific non-secret values (URLs, timeouts, feature flags) in appsettings.{Environment}.json. Supply genuine secrets through environment variables set on the production server itself, or a proper secret manager — deep secrets-management tooling is its own topic, covered later, but the rule at this level is simple: if leaking it would be a security incident, it doesn't belong in any file that gets checked in.
Deploying to a staging server and forgetting to set DOTNET_ENVIRONMENT=Staging, so the app silently defaults to Production — which might mean pointing at production-grade external services from a server nobody intended to be production.
Set the environment variable explicitly as part of every deployment's configuration (container definition, systemd unit, hosting platform settings) — never rely on an environment simply "happening" to be correct by default. Since the safe default is Production, forgetting to set it fails toward the strict configuration rather than the loose one — but "safe by default" is not the same as "correct," so always set it explicitly.
Sprinkling if (env.IsDevelopment()) checks throughout core business logic, so the actual behavior tested locally and the behavior that runs in production quietly diverge in ways that are hard to track.
Prefer letting environment-specific configuration values (a different URL, a different log level, a different feature flag) drive behavior differences, rather than branching on the environment name directly in business logic. Reserve direct IsDevelopment()/IsProduction() checks for genuinely environment-specific infrastructure concerns — like which logging providers to register, or whether to enable a startup diagnostic — not for changing what your application actually does.
appsettings.json).DOTNET_ENVIRONMENTappsettings.{Environment}.json = an extra, optional provider slotted right after the base fileIHostEnvironment = how your code asks "which environment am I in?" at runtimeProduction, the safest failure mode.appsettings.{Environment}.json; real secrets need environment variables or a dedicated secret store instead.
DOTNET_ENVIRONMENT (or ASPNETCORE_ENVIRONMENT, which wins if both are set) at startup, defaulting to Production if neither is set.Host.CreateApplicationBuilder automatically layers an optional appsettings.{Environment}.json file on top of the base appsettings.json, using the exact same "later provider wins, per key" override rule from the previous lesson.IHostEnvironment (accessed via builder.Environment, or injected anywhere via DI) lets your code check EnvironmentName, IsDevelopment(), IsStaging(), and IsProduction()."QA", "LoadTest") and .NET will look for the matching JSON file automatically.You've seen how one environment variable can reshape an app's entire configuration. Let's check your grasp of how it fits into the provider stack.
1. No DOTNET_ENVIRONMENT or ASPNETCORE_ENVIRONMENT variable is set anywhere. What environment does the app run in?
Correct: B
Why B is correct: .NET defaults to Production when the environment variable isn't set — a deliberate choice that fails toward the most locked-down, least-verbose behavior rather than accidentally exposing development-level detail.
Why A is incorrect: It's the opposite — Development is never assumed; it must be explicitly set.
Why C is incorrect: A missing environment variable is not a startup error; the app runs normally, just under the Production default.
Why D is incorrect: The behavior is fully deterministic, not random — always Production when unset.
Reinforcement: Never rely on the default — always set the environment variable explicitly as part of your deployment or local run configuration.
2. appsettings.json sets Api:TimeoutSeconds to 30. There is no Api:TimeoutSeconds key anywhere in appsettings.Production.json. Running in Production, what value does the app read?
Correct: A
Why A is correct: Overriding happens per-key, exactly as with any other provider layering — an environment-specific file only replaces the keys it actually defines. A key it doesn't mention simply falls through to whatever the earlier provider (the base appsettings.json) supplied.
Why B is incorrect: Omitting a key is not the same as setting it to zero or clearing it — the base value remains untouched.
Why C is incorrect: There's no requirement that every environment file redefine every key; partial override files are the entire point of this mechanism.
Why D is incorrect: A provider can only override a key it actually contains — absence of a key means that provider has nothing to say about it, not that it wins with a null value.
Reinforcement: This is the same "override per key, not per file" rule from the base configuration lesson, applied to environment-specific files.
3. A team wants a "QA" testing environment that isn't Development, Staging, or Production. Which statement is true?
Correct: B
Why B is correct: The environment name is just a plain string read from an environment variable — .NET has no fixed enum of valid values. Setting it to "QA" and adding a matching appsettings.QA.json works exactly the same way as the three conventional names.
Why A is incorrect: Development, Staging, and Production are just conventional names with dedicated Is...() helper methods — not an exhaustive, enforced list.
Why C is incorrect: No framework source changes are needed; this works with the standard host builder out of the box.
Why D is incorrect: This mechanism lives in the generic host shared by console/worker apps and ASP.NET Core apps alike — it isn't ASP.NET Core-specific.
Reinforcement: Environment names are just strings the configuration system uses to pick a matching file name — invent as many as your workflow needs.
4. A developer commits a real production database password inside appsettings.Production.json, reasoning "it's fine, this file only applies in production anyway." What's wrong with this reasoning?
Correct: B
Why B is correct: "Environment-specific" describes when a file's values get loaded, not who can read the file's contents. Like any other appsettings*.json file, it's plain text checked into source control — the environment-specific naming provides zero confidentiality protection.
Why A is incorrect: There is no built-in encryption for any appsettings.json variant — they're all plain text.
Why C is incorrect: The file is loaded exactly when the app runs with DOTNET_ENVIRONMENT=Production — that's the whole mechanism working as designed, which is precisely why the secret would actually be used, and exposed.
Why D is incorrect: .NET tooling has no automatic gitignore rules for environment-specific settings files; developers must manage source control exclusions themselves, and even that wouldn't protect a file already committed.
Reinforcement: "Environment-specific" and "secret" are different concerns — real secrets need environment variables or a dedicated secret manager, never any checked-in JSON file.
You now understand how .NET detects its running environment and layers configuration files accordingly — the same binary, adapting itself to wherever it's deployed.
dotnetmadeeasy.com — Learn C# and .NET, the right way.