Environment Variables and the Double Underscore

Override any configuration key from a shell, Docker or Kubernetes using double-underscore names, prefixes and array indexes.

SyntaxLogging__LogLevel__Default=Warning ConnectionStrings__Shop="Host=orders-db;Database=shop" Orders__AllowedCurrencies__0=EUR builder.Configuration.AddEnvironmentVariables(prefix: "ORDERSAPI_");

Environment variables are how containers receive configuration, so every key an operator may need to change must be reachable through one. The environment variable provider reads the process environment once, when configuration is built, and turns each variable into a configuration key.

Name mapping

A double underscore becomes the section separator: Logging__LogLevel__Default=Warning sets Logging:LogLevel:Default, and ConnectionStrings__Shop is what GetConnectionString("Shop") reads. A colon is legal in Windows variable names but not in bash or in most container tooling, so use __ everywhere. Keys are case-insensitive, while Linux variable names are not: ORDERS__REGION and Orders__Region are two variables that map to the same key, and which one wins is not something to rely on.

Arrays and dictionaries

An index is just another segment: Orders__AllowedCurrencies__0=EUR. An override replaces only that index. If appsettings.json lists four currencies and the deployment sets indexes 0 and 1, the bound list still has four entries, the last two from the file. There is no way to delete a key from a lower provider, so a list that must shrink per environment should not have a default in the file. Dictionary entries work the same way: Payments__MerchantAccounts__EU=ShopEU_ECOM.

Prefixes

VariablesWhat happens
ASPNETCORE_*, DOTNET_*Read first as host settings with the prefix removed (ASPNETCORE_ENVIRONMENT becomes environment)
Everything, unprefixedRead by the default provider, after the JSON files
AddEnvironmentVariables("ORDERSAPI_")Only matching variables, prefix removed; added last, so it wins
SQLCONNSTR_, SQLAZURECONNSTR_, MYSQLCONNSTR_, CUSTOMCONNSTR_Mapped to ConnectionStrings:{name}; the first three also set {name}_ProviderName

The connection-string prefixes exist for Azure App Service, which injects connection strings that way.

Containers and Kubernetes

Values come from ENV in the Dockerfile, -e or --env-file on docker run, and env/envFrom in a pod spec, where env beats envFrom for the same name. Because the provider never reloads, editing a ConfigMap does nothing to running pods: restart them with a rollout. Kubernetes also injects service link variables such as ORDERS_PORT=tcp://10.96.14.3:8080 for every Service in the namespace. An app that reads ORDERS_-prefixed variables then finds a key Port holding a URL. Set enableServiceLinks: false on the pod, or choose a prefix no Service name produces.

Failure modes

A misspelled variable is simply an unused key: Loging__LogLevel__Default raises no error. A single underscore (Orders_Region) is a different key too. Validate options at start-up so missing values fail loudly, and print the effective keys with their source when a container behaves differently from your laptop.

Example

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

When to use it

  • Set ConnectionStrings__Shop from a Kubernetes Secret with secretKeyRef so the connection string never appears in the manifest.
  • Turn on Debug logging for one pod with Logging__LogLevel__Orders.Api=Debug while the rest stay at Information.
  • Diagnose a pod whose prefixed settings read tcp://10.96.14.3:8080 because a Service named orders injected ORDERS_PORT.

More examples

Kubernetes: env, envFrom and secret references

<code>envFrom</code> turns every key of the ConfigMap into a variable, so ConfigMap keys should already use the <code>Section__Key</code> form. Entries under <code>env</code> override <code>envFrom</code> entries with the same name. <code>secretKeyRef</code> keeps the connection string out of the manifest. <code>enableServiceLinks: false</code> stops Kubernetes from adding <code>{SERVICE}_PORT</code> style variables that can collide with prefixed configuration. None of these values change in a running pod; a <code>kubectl rollout restart</code> is needed after editing the ConfigMap.

Example · yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: orders-api
spec:
  replicas: 3
  selector:
    matchLabels:
      app: orders-api
  template:
    metadata:
      labels:
        app: orders-api
    spec:
      enableServiceLinks: false
      containers:
        - name: api
          image: registry.example.com/shop/orders-api:1.14.2
          ports:
            - containerPort: 8080
          envFrom:
            - configMapRef:
                name: orders-api-settings
          env:
            - name: ASPNETCORE_ENVIRONMENT
              value: Production
            - name: Logging__LogLevel__Default
              value: Warning
            - name: Orders__AllowedCurrencies__0
              value: USD
            - name: Orders__AllowedCurrencies__1
              value: EUR
            - name: ConnectionStrings__Shop
              valueFrom:
                secretKeyRef:
                  name: orders-api-db
                  key: connection-string

Shells and docker run

Most "my setting is ignored" reports come down to the variable never reaching the process: a typo, a single underscore, or quotes kept as part of the value. <code>printenv</code> inside the container shows the truth. Filter the output to non-secret prefixes, because a full <code>printenv</code> also prints connection strings and keys into your terminal history and CI logs.

Example · bash
# Double underscores work in every shell; a colon is not valid in a bash variable name.
export Orders__MaxItemsPerOrder=100
dotnet run --project src/Orders.Api

# Individual variables, or a file of KEY=value lines.
# docker run --env-file keeps quotes literally, so do not quote values in that file.
docker run -d --rm --name orders-api -p 8080:8080 \
  -e ASPNETCORE_ENVIRONMENT=Staging \
  -e Orders__Region=eu-west \
  --env-file ./orders-api.env \
  registry.example.com/shop/orders-api:1.14.2

# Check what the running container really received (non-secret keys only).
docker exec orders-api printenv | grep -E '^(ASPNETCORE_|Orders__|Logging__)'

Reading only prefixed variables

By default every variable in the process, including <code>PATH</code>, <code>HOSTNAME</code> and Kubernetes service links, becomes a configuration key. Swapping the source for a prefixed one keeps configuration to variables meant for this service, and the operator sets <code>ORDERSAPI_Payments__Provider</code>. The cost is that conventional names such as <code>ConnectionStrings__Shop</code> stop working, so document the prefix. Setting an entry in <code>Sources</code> makes the <code>ConfigurationManager</code> reload its providers immediately.

Example · csharp
using Microsoft.Extensions.Configuration.EnvironmentVariables;

var builder = WebApplication.CreateBuilder(args);

// Replace the unprefixed provider in place, so precedence stays the same
// but only ORDERSAPI_* variables become configuration.
var sources = builder.Configuration.Sources;
var index = sources.ToList().FindIndex(s => s is EnvironmentVariablesConfigurationSource { Prefix: null });
if (index >= 0)
{
    sources[index] = new EnvironmentVariablesConfigurationSource { Prefix = "ORDERSAPI_" };
}

var app = builder.Build();

app.MapGet("/payments/provider", (IConfiguration config) =>
    TypedResults.Ok(new { provider = config["Payments:Provider"] ?? "Stripe" }));

app.Run();

Discussion

  • Be the first to comment on this lesson.