← Open in the full interactive course (progress tracking, search & more)

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.

What Is It?

The Simple Explanation

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.

The Technical Definition

.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.

Development

Production

Why Does It Exist?

The Problem — One App, Many Very Different Homes

The Solution — Name the Environment, Let Configuration Adapt

.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.

Big Picture

THE PROVIDER STACK, WITH THE ENVIRONMENT FILE SLOTTED IN
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.

How It Works

FROM STARTUP TO ENVIRONMENT-AWARE CONFIGURATION
1. .NET READS THE ENVIRONMENT NAME BEFORE ANYTHING ELSE LOADS
export DOTNET_ENVIRONMENT=Staging
dotnet run
2. HOST.CREATEAPPLICATIONBUILDER LOADS THE MATCHING FILE AUTOMATICALLY
var builder = Host.CreateApplicationBuilder(args);
// Environment is "Staging" → also loads appsettings.Staging.json, if it exists
3. YOUR CODE CAN ASK "WHICH ENVIRONMENT AM I IN?" DIRECTLY
if (builder.Environment.IsDevelopment())
{
    // e.g. enable a verbose, human-readable console logger
}

Simple Example

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

Real-World Example

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.

Note: This "fail fast on missing required config" pattern pairs especially well with the Options Pattern's validation feature (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.

Analogy

The Same Actor, a Different Costume Per Stage

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.

Under the Hood

HOW HOST.CREATEAPPLICATIONBUILDER WIRES THIS UP
1. THE ENVIRONMENT NAME IS RESOLVED FIRST, BEFORE THE CONFIGURATION PIPELINE EVEN STARTS
2. TWO JSON PROVIDERS ARE ADDED IN A FIXED ORDER, NOT ONE
3. IHostEnvironment IS REGISTERED IN THE DI CONTAINER

Common Confusion

The environment name is just a string — .NET has no idea what "Staging" actually means

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.

Environment-based configuration is not a secrets manager

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.

Common Mistakes

Mistake 1 — Putting real secrets in appsettings.Production.json

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.

Mistake 2 — Forgetting to set the environment variable at all

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.

Mistake 3 — Branching heavily on environment name deep inside business logic

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.

When Should I Use It?

Mental Model

Environment name = one string, read once at startup, from DOTNET_ENVIRONMENT
appsettings.{Environment}.json = an extra, optional provider slotted right after the base file
IHostEnvironment = how your code asks "which environment am I in?" at runtime

Remember:
· No environment variable set → defaults to Production, the safest failure mode.
· Same override rule as always: the environment file's keys win over the base file's keys, per key, not per file.
· Environment-specific ≠ secret. Non-sensitive values can live in appsettings.{Environment}.json; real secrets need environment variables or a dedicated secret store instead.

Key Takeaway


Check Your Understanding

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?

Show answer

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?

Show answer

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?

Show answer

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?

Show answer

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.