Every ASP.NET Core application targets one of three environments: Development (local developer machine), Staging (pre-production, mirrors production), or Production (live). The active environment is set via the ASPNETCORE_ENVIRONMENT environment variable. This single variable drives which configuration files are loaded, which middleware is registered, how detailed error messages are, and which feature flags are active. Getting environment management right is a critical operational discipline that prevents configuration secrets from leaking to developers and debug information from reaching users.
Environment Variables and Configuration Order
// โโ ASPNETCORE_ENVIRONMENT โ set before running the app โโโโโโโโโโโโโโโโโโโ
// Shell: export ASPNETCORE_ENVIRONMENT=Development
// Windows: $env:ASPNETCORE_ENVIRONMENT = "Development"
// Docker: ENV ASPNETCORE_ENVIRONMENT=Production
// launchSettings.json: "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }
// โโ Configuration loading order (later sources override earlier) โโโโโโโโโโ
// Host defaults:
// 1. appsettings.json (base settings โ committed to source control)
// 2. appsettings.{env}.json (env-specific โ Development version committed,
// Production version NOT committed)
// 3. User Secrets (Dev only) (sensitive dev settings โ never committed)
// 4. Environment variables (CI/CD, Docker, cloud platform settings)
// 5. Command-line arguments (highest priority โ for testing and overrides)
// โโ Checking the current environment โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
app.UseDeveloperExceptionPage(); // detailed stack traces in browser
}
if (app.Environment.IsProduction())
{
app.UseHsts(); // HTTP Strict Transport Security header
app.UseHttpsRedirection(); // redirect HTTP to HTTPS
}
// Custom environment check
if (app.Environment.IsEnvironment("Staging"))
{
app.UseStatusCodePages();
}
// IWebHostEnvironment in a service
public class FeatureService(IWebHostEnvironment env)
{
public bool IsDetailedLoggingEnabled => env.IsDevelopment();
}
appsettings.Development.json is committed to source control because it contains non-sensitive development settings (local DB connection string, mock feature flags, verbose logging). appsettings.Production.json should NOT be committed โ production secrets (real connection strings, API keys) should come from environment variables, Azure Key Vault, AWS Secrets Manager, or similar. The User Secrets mechanism (dotnet user-secrets set) stores development secrets in a per-user JSON file outside the project directory, keeping them out of source control while making them easily accessible during local development.Program.cs to verify required configuration values are present on startup. Instead of discovering a missing connection string on the first database request (which may be minutes into a request handler), fail fast at startup: var connectionString = builder.Configuration.GetConnectionString("Default") ?? throw new InvalidOperationException("Connection string 'Default' is not configured.");. This transforms a confusing runtime error into an immediate, obvious startup failure with a clear error message.ASPNETCORE_ENVIRONMENT=Development in a production environment. The Development environment enables UseDeveloperExceptionPage() which shows full stack traces, source code snippets, and environment variable values in HTTP error responses โ a significant information disclosure vulnerability. It also enables Swagger UI by default (exposing your API schema) and may enable other development-only features that are not production-safe. Always verify the environment variable is correctly set in your deployment pipeline.Environment-Specific Service Registration
// Register different implementations based on environment
if (builder.Environment.IsDevelopment())
{
// Use an in-memory email sender in development โ no real emails sent
builder.Services.AddSingleton<IEmailSender, InMemoryEmailSender>();
// Use a fake payment processor in development
builder.Services.AddScoped<IPaymentProcessor, FakePaymentProcessor>();
}
else
{
// Real implementations for staging and production
builder.Services.AddSingleton<IEmailSender, SmtpEmailSender>();
builder.Services.AddScoped<IPaymentProcessor, StripePaymentProcessor>();
}
// appsettings.json โ base settings
{
"Logging": { "LogLevel": { "Default": "Warning" } },
"AllowedHosts": "*"
}
// appsettings.Development.json โ development overrides
{
"Logging": { "LogLevel": { "Default": "Debug", "Microsoft.AspNetCore": "Warning" } },
"ConnectionStrings": { "Default": "Server=(localdb)\\mssqllocaldb;Database=BlogApp_Dev" }
}
Common Mistakes
Mistake 1 โ Committing production secrets in appsettings.Production.json
โ Wrong โ connection strings, API keys in source control are a security incident waiting to happen.
โ Correct โ production secrets via environment variables, Key Vault, or Secrets Manager only.
Mistake 2 โ Not validating required configuration at startup
โ Wrong โ application starts successfully but crashes on first database access with an opaque error.
โ Correct โ validate required configuration values in Program.cs before Build() and fail fast with clear error messages.