.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.
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 itWhy 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):
appsettings.jsonappsettings.{Environment}.json- User secrets (Development environment only)
- Environment variables
- Command-line arguments
// 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:
# 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:
# Switches between appsettings.Development.json and appsettings.Production.json
export ASPNETCORE_ENVIRONMENT=Production
# Same machinery for non-web workers
export DOTNET_ENVIRONMENT=Production// 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:
# 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:
// 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 withIOptionsMonitor<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.Secretspackage layers Key Vault intoIConfigurationso 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.