env.dev

Environment Variables in C# / .NET: IConfiguration & __ Mapping

In ASP.NET Core, prefer IConfiguration over Environment.GetEnvironmentVariable. Double underscores map to colons (ConnectionStrings__Default → ConnectionStrings:Default).

Last updated:

.NET reads environment variables through Environment.GetEnvironmentVariable — but in any modern ASP.NET Core or worker-service app, you should almost never call it directly. The right abstraction is IConfiguration, the layered configuration system introduced with ASP.NET Core 1.0 (2016), which reads from appsettings.json, environment variables, command-line flags, user secrets, and Azure Key Vault with documented precedence. The convention to remember: __ (double underscore) in an environment variable name maps to : (colon) in the JSON path, so ConnectionStrings__Default becomes ConnectionStrings:Default. For a true .env loader, the community ships DotNetEnv; for local-development secrets, .NET has its own dotnet user-secrets mechanism. This guide covers the lot, plus the ASPNETCORE_ENVIRONMENT trick that swaps configuration files per stage.

How do you read an environment variable in C#?

The lowest-level API is System.Environment. It returns nullable strings — null for unset, never an empty string for "not present" — and supports three target scopes on Windows (Machine, User, Process); on Unix only Process is meaningful.

csharp
using System;

string? dbUrl = Environment.GetEnvironmentVariable("DATABASE_URL");
string  port  = Environment.GetEnvironmentVariable("PORT") ?? "3000";

// Whole environment as a non-generic IDictionary
foreach (System.Collections.DictionaryEntry de in Environment.GetEnvironmentVariables())
{
    Console.WriteLine($"{de.Key}={de.Value}");
}

// Set / unset (process scope only on Unix)
Environment.SetEnvironmentVariable("MY_VAR", "value");
Environment.SetEnvironmentVariable("MY_VAR", null);  // null removes it

Why prefer IConfiguration over GetEnvironmentVariable?

ASP.NET Core's IConfiguration layers multiple sources with a precedence order that survives refactoring better than scattered Environment.GetEnvironmentVariable calls. The default web host wires up these sources in order (later ones override earlier):

  1. appsettings.json
  2. appsettings.{Environment}.json
  3. User secrets (Development environment only)
  4. Environment variables
  5. Command-line arguments
csharp
// Program.cs (minimal hosting model, .NET 6+)
var builder = WebApplication.CreateBuilder(args);

// Bind a section to a typed POCO
builder.Services.Configure<DatabaseOptions>(
    builder.Configuration.GetSection("Database"));

var app = builder.Build();

// Inject via IOptions<T>
app.MapGet("/", (IOptions<DatabaseOptions> opts) =>
    new { connectionString = opts.Value.ConnectionString });

public class DatabaseOptions
{
    public string ConnectionString { get; set; } = "";
    public int    PoolSize         { get; set; } = 10;
}

How do double underscores map to nested keys?

Environment variable names can't contain colons (forbidden on most shells), so .NET adopted __ as the universal nesting separator. IConfiguration rewrites it to : automatically:

bash
# appsettings.json equivalent
# {
#   "ConnectionStrings": { "Default": "..." },
#   "Logging":           { "LogLevel": { "Default": "Information" } }
# }

export ConnectionStrings__Default="Server=localhost;Database=app"
export Logging__LogLevel__Default="Debug"
export Database__PoolSize="20"

This means any nested config key can be overridden via environment variables in production without touching JSON files — the standard pattern for Kubernetes ConfigMaps or AWS Parameter Store.

What does ASPNETCORE_ENVIRONMENT control?

ASPNETCORE_ENVIRONMENT picks which appsettings.{Environment}.json file is loaded and gates development-only middleware like the developer exception page and user secrets. The convention values are Development, Staging, and Production, but any string works:

bash
# Switches between appsettings.Development.json and appsettings.Production.json
export ASPNETCORE_ENVIRONMENT=Production

# Same machinery for non-web workers
export DOTNET_ENVIRONMENT=Production
csharp
// In Program.cs
if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

When should you use User Secrets vs DotNetEnv?

For local-development secrets — API keys, dev DB passwords — .NET has a built-in mechanism that stores values in your user profile, never in the project directory:

bash
# Initialize once per project
dotnet user-secrets init

# Store secrets — kept in ~/.microsoft/usersecrets/<id>/secrets.json
dotnet user-secrets set "Database:ConnectionString" "Server=localhost;..."
dotnet user-secrets set "Stripe:SecretKey" "sk_test_..."

For projects where a .env file makes more sense (multi-language teams, Docker Compose dev environments), the DotNetEnv package loads .env files into Environment.GetEnvironmentVariable at startup:

csharp
// dotnet add package DotNetEnv
DotNetEnv.Env.Load();          // .env in working directory
DotNetEnv.Env.Load(".env.local");

// IConfiguration sees the loaded values
var config = new ConfigurationBuilder()
    .AddEnvironmentVariables()
    .Build();

Practical patterns and gotchas

  • Use IConfiguration, not Environment. Reach for the latter only at the very edges of your app (e.g. very early bootstrap before a host is built).
  • Bind to a POCO with IOptions<T> rather than reading individual keys. You get strong typing, validation via [Required] and friends, and automatic reloading with IOptionsMonitor<T>.
  • Never check user secrets into git. The whole point is that they live outside the repo. .NET's project file references the secrets-id GUID, not the values.
  • Azure Key Vault for production secrets. The Azure.Extensions.AspNetCore.Configuration.Secrets package layers Key Vault into IConfiguration so the rest of your code stays unchanged.
  • Validate your .env files with the .env validator before they hit your container build.

For language-agnostic security, validation, and rotation rules, see environment variable best practices.

Was this helpful?

Read next

Environment Variables in Rust: std::env, dotenvy & envy

Read env vars in Rust with std::env::var, load .env files via dotenvy, and deserialize typed config with envy. Plus the Rust 2024 unsafe-set_var change.

Continue →

Frequently Asked Questions

Why use IConfiguration instead of Environment.GetEnvironmentVariable?

IConfiguration layers multiple sources — appsettings.json, environment variables, command-line args, user secrets, Azure Key Vault — with a documented precedence. You get strong typing via IOptions<T>, validation via data annotations, and reload support without scattering Environment calls across your codebase.

How do double underscores work in .NET env vars?

Environment variables can't contain colons (forbidden by most shells), so .NET adopted __ as the universal nesting separator. ConnectionStrings__Default sets the IConfiguration key ConnectionStrings:Default. Logging__LogLevel__Default → Logging:LogLevel:Default. Any nested config key can be overridden via env vars.

When should I use User Secrets vs DotNetEnv?

User Secrets is the built-in .NET mechanism for local-development secrets — values live in your user profile, never in the project. Use it for solo or small-team work. DotNetEnv is better when you have a multi-language team or Docker Compose dev environment that already standardizes on .env files.

What does ASPNETCORE_ENVIRONMENT control?

It picks which appsettings.{Environment}.json file is loaded and gates dev-only middleware like the developer exception page and user secrets. Conventional values are Development, Staging, Production, but any string works. DOTNET_ENVIRONMENT does the same for non-web workers.

Stay up to date

Get notified about new guides, tools, and cheatsheets.