Environments

Set the hosting environment correctly, layer appsettings.{Environment}.json, and keep environment checks out of business logic.

SyntaxASPNETCORE_ENVIRONMENT=Staging dotnet Api.dll // appsettings.json, then appsettings.Staging.json (exact case on Linux) if (app.Environment.IsDevelopment()) app.MapOpenApi();

The host has one environment name, exposed as IHostEnvironment.EnvironmentName (builder.Environment, app.Environment). It decides which extra settings file loads and switches a few developer conveniences on. When nothing sets it, the name is Production, which is the safe default.

Where the name comes from

  • ASPNETCORE_ENVIRONMENT: read by web apps only.
  • DOTNET_ENVIRONMENT: read by every host, including workers.
  • --environment Staging on the command line, which beats both variables.
  • Code: WebApplicationOptions.EnvironmentName or HostApplicationBuilderSettings.EnvironmentName.

With WebApplication.CreateBuilder, if both variables are set, DOTNET_ENVIRONMENT wins, because the ASPNETCORE_ source is added first. The older Host.CreateDefaultBuilder().ConfigureWebHostDefaults() style does the opposite. A worker built with Host.CreateApplicationBuilder ignores ASPNETCORE_ENVIRONMENT completely. Set exactly one variable per process: ASPNETCORE_ENVIRONMENT for APIs, DOTNET_ENVIRONMENT for workers.

Locally, Properties/launchSettings.json sets ASPNETCORE_ENVIRONMENT=Development for dotnet run and the IDE. It is not part of the publish output, so it never affects a deployed app.

What the environment changes

BehaviourDevelopmentOther environments
appsettings.{Environment}.jsonLoaded after appsettings.jsonSame
User secretsLoadedNot loaded
Developer exception pageOnOff
DI ValidateScopes and ValidateOnBuildOnOff

IsDevelopment(), IsStaging(), IsProduction() and IsEnvironment("QA") compare case-insensitively. The settings file lookup does not: on Linux, appsettings.production.json is not loaded for Production. It works on a Windows or macOS laptop and silently fails in the container.

Never branch business logic on the environment

if (env.IsProduction()) await gateway.CaptureAsync(payment) means Staging can never exercise the real code path, and a QA environment added later behaves like neither. Put the difference in configuration: Payments:Mode = Sandbox in Staging, Live in Production, bound to an options class and validated at start-up. Reserve environment checks for developer tooling: the exception page, the OpenAPI UI, verbose diagnostics.

What goes wrong in production

  • A Dockerfile with ENV ASPNETCORE_ENVIRONMENT=Development left in, which turns on detailed error pages that leak stack traces.
  • A base image or Helm chart setting DOTNET_ENVIRONMENT that quietly overrides the ASPNETCORE_ENVIRONMENT you set.
  • A lower-case settings file name that only some machines load.
  • Staging configured with production endpoints because its file failed to load and the base file held production values. Keep appsettings.json free of real endpoints.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Run the same orders-api image in staging and production, with ASPNETCORE_ENVIRONMENT selecting appsettings.Staging.json or appsettings.Production.json.
  • Diagnose a container that ignores appsettings.production.json because Linux matched the file name case-sensitively against Production.
  • Replace an if (env.IsProduction()) around card capture with a validated Payments:Mode setting that Staging can switch on.

More examples

launchSettings.json for local runs only

<code>dotnet run</code> and IDEs apply the first profile (or the one named with <code>--launch-profile</code>), which is why a fresh project runs as Development on your machine. The file is not published, so a deployed app falls back to Production unless the platform sets the variable. The second profile lets you try Staging settings locally with <code>dotnet run --launch-profile staging-local</code>.

Example · json
{
  "$schema": "https://json.schemastore.org/launchsettings.json",
  "profiles": {
    "http": {
      "commandName": "Project",
      "launchBrowser": false,
      "applicationUrl": "http://localhost:5180",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },
    "staging-local": {
      "commandName": "Project",
      "applicationUrl": "http://localhost:5181",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Staging"
      }
    }
  }
}

Setting the environment per cluster

The same image runs in every cluster; only the environment variables differ. The environment name selects <code>appsettings.Staging.json</code>, while the behaviour that matters to the business, the payment mode, is an explicit setting (<code>Payments__Mode</code> maps to <code>Payments:Mode</code>). Do not also set <code>DOTNET_ENVIRONMENT</code> here: in a web app it would win.

Example · yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: orders-api
  namespace: shop-staging
spec:
  replicas: 2
  selector:
    matchLabels:
      app: orders-api
  template:
    metadata:
      labels:
        app: orders-api
    spec:
      containers:
        - name: api
          image: registry.example.com/shop/orders-api:1.14.2
          ports:
            - containerPort: 8080
          env:
            - name: ASPNETCORE_ENVIRONMENT
              value: Staging
            - name: Payments__Mode
              value: Sandbox

Configuration instead of environment checks

The broken version would be <code>if (app.Environment.IsProduction())</code> around the capture call: Staging could never test it and a new <code>QA</code> environment would silently take the sandbox path. Here the mode is a required, validated setting, so a missing value stops the app at start-up, and only the debug endpoint looks at the environment.

Example · csharp
using System.ComponentModel.DataAnnotations;
using Microsoft.Extensions.Options;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddOptions<PaymentOptions>()
    .BindConfiguration("Payments")
    .ValidateDataAnnotations()
    .ValidateOnStart();

var app = builder.Build();

// Fine: developer tooling may depend on the environment.
if (app.Environment.IsDevelopment())
{
    app.MapGet("/debug/payments", (IOptions<PaymentOptions> o) => o.Value);
}

// Business behaviour depends on configuration, so Staging can run the Live path if needed.
app.MapPost("/orders/{id:int}/capture", (int id, IOptions<PaymentOptions> options) =>
    options.Value.Mode == PaymentMode.Live
        ? Results.Accepted($"/payments/{id}", new { id, mode = "live" })
        : Results.Ok(new { id, mode = "sandbox", captured = true }));

app.Run();

enum PaymentMode { Sandbox, Live }

sealed class PaymentOptions
{
    [Required] public PaymentMode? Mode { get; set; }
    [Range(0, 1440)] public int CaptureDelayMinutes { get; set; }
}

Discussion

  • Be the first to comment on this lesson.