Every real-world application needs to behave differently depending on where it runs. Your local machine uses a different database than production. API keys change between environments. Logging verbosity cranks up during development but stays quiet in production.
ASP.NET Core handles this through a layered configuration system that’s both powerful and predictable. Configuration values can come from JSON files, environment variables, command-line arguments, user secrets, and more—all merged together with clear precedence rules.
In this article, we’ll cover how configuration works in .NET 10, how to structure your appsettings.json files, how environment variables override settings, how launchSettings.json works (and why it’s misunderstood), and how to use User Secrets to keep sensitive data out of source control. We’ll also cover the most common mistakes developers make and how to avoid them.
This article is part of my .NET Web API Zero to Hero course, where I cover everything you need to know to build efficient and scalable APIs. Configuration is foundational—once you understand how it works, everything from connection strings to feature flags becomes straightforward.
Let’s get into it.
How Configuration Works in ASP.NET Core
ASP.NET Core uses a layered configuration system. Multiple configuration sources are loaded in a specific order, and later sources override earlier ones. This is the foundation of environment-based configuration.
When your application starts, the default WebApplication.CreateBuilder(args) method loads configuration from these sources, in this order:
appsettings.jsonappsettings.{Environment}.json(e.g.,appsettings.Development.json)- User Secrets (only in Development environment)
- Environment variables
- Command-line arguments
Each subsequent source overrides values from previous sources. This means:
- Values in
appsettings.Development.jsonoverrideappsettings.json - Environment variables override both JSON files
- Command-line arguments have the highest priority
This layering is intentional. You define sensible defaults in appsettings.json, override them for specific environments, and use environment variables or secrets for sensitive data that should never be committed to source control.
var builder = WebApplication.CreateBuilder(args);
// Configuration is already loaded at this point// You can access it via builder.Configurationvar connectionString = builder.Configuration.GetConnectionString("DefaultConnection");The IConfiguration interface is available throughout your application via dependency injection. You can inject it anywhere you need to read configuration values.
Understanding the ASPNETCORE_ENVIRONMENT Variable
The ASPNETCORE_ENVIRONMENT environment variable controls which environment your application runs in. ASP.NET Core recognizes three standard values:
DevelopmentStagingProduction
When this variable is set to Development, ASP.NET Core:
- Loads
appsettings.Development.jsonafterappsettings.json - Enables User Secrets
- Shows detailed error pages
- Enables developer exception pages
When set to Production (or not set at all—Production is the default):
- Loads
appsettings.Production.json - Disables User Secrets
- Shows generic error pages
- Optimizes for performance
You can also use custom environment names like QA, UAT, or PreProduction. Just create the corresponding appsettings.{EnvironmentName}.json file.
// Check the current environment in codeif (builder.Environment.IsDevelopment()){ // Development-specific setup}
if (builder.Environment.IsProduction()){ // Production-specific setup}
// Or check for custom environmentsif (builder.Environment.IsEnvironment("Staging")){ // Staging-specific setup}Working with appsettings.json
The appsettings.json file is the primary configuration source for most applications. It’s a JSON file at the root of your project that stores configuration values in a hierarchical structure.
Basic Structure
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*", "ConnectionStrings": { "DefaultConnection": "Server=localhost;Database=MyApp;Trusted_Connection=true;" }, "AppSettings": { "ApplicationName": "My Awesome API", "MaxRetryAttempts": 3, "EnableFeatureX": true }}Accessing Configuration Values
There are multiple ways to access configuration values. The simplest is using IConfiguration directly:
public class MyService(IConfiguration configuration){ public void DoSomething() { // Access a simple value var appName = configuration["AppSettings:ApplicationName"];
// Access nested values using colon separator var logLevel = configuration["Logging:LogLevel:Default"];
// Get typed values var maxRetries = configuration.GetValue<int>("AppSettings:MaxRetryAttempts"); var enableFeature = configuration.GetValue<bool>("AppSettings:EnableFeatureX");
// Get connection strings (special helper method) var connectionString = configuration.GetConnectionString("DefaultConnection"); }}The colon (:) is the separator for nested configuration keys. Logging:LogLevel:Default navigates to Logging → LogLevel → Default.
Environment-Specific Override Files
Create environment-specific files to override base settings:
appsettings.json (base defaults):
{ "Logging": { "LogLevel": { "Default": "Warning" } }, "ConnectionStrings": { "DefaultConnection": "Server=prodserver;Database=MyApp;..." }, "FeatureFlags": { "EnableDebugEndpoints": false }}appsettings.Development.json (development overrides):
{ "Logging": { "LogLevel": { "Default": "Debug", "Microsoft.EntityFrameworkCore": "Information" } }, "ConnectionStrings": { "DefaultConnection": "Server=localhost;Database=MyApp_Dev;Trusted_Connection=true;" }, "FeatureFlags": { "EnableDebugEndpoints": true }}appsettings.Staging.json (staging overrides):
{ "Logging": { "LogLevel": { "Default": "Information" } }, "ConnectionStrings": { "DefaultConnection": "Server=stagingserver;Database=MyApp_Staging;..." }}Only include the values you want to override. The configuration system merges them intelligently.
Environment Variables – The Production Standard
Environment variables are the preferred way to configure applications in production. They’re secure (not committed to source control), easy to change without redeployment, and work consistently across all hosting platforms.
The Double-Underscore Convention
Here’s something that trips up many developers: ASP.NET Core uses double underscores (__) to represent the colon (:) separator in environment variable names.
To override ConnectionStrings:DefaultConnection, set:
ConnectionStrings__DefaultConnection=Server=prodserver;Database=MyApp;...To override Logging:LogLevel:Default:
Logging__LogLevel__Default=ErrorThis convention exists because colons are not valid in environment variable names on all platforms (especially Linux).
Setting Environment Variables
Windows (PowerShell):
$env:ASPNETCORE_ENVIRONMENT = "Production"$env:ConnectionStrings__DefaultConnection = "Server=prodserver;..."Windows (Command Prompt):
set ASPNETCORE_ENVIRONMENT=Productionset ConnectionStrings__DefaultConnection=Server=prodserver;...Linux/macOS:
export ASPNETCORE_ENVIRONMENT=Productionexport ConnectionStrings__DefaultConnection="Server=prodserver;..."Docker:
ENV ASPNETCORE_ENVIRONMENT=ProductionENV ConnectionStrings__DefaultConnection="Server=prodserver;..."Or in docker-compose.yml:
services: api: environment: - ASPNETCORE_ENVIRONMENT=Production - ConnectionStrings__DefaultConnection=Server=prodserver;...Why Environment Variables Override JSON Files
Environment variables take precedence over appsettings.json files. This is intentional and powerful:
- Security: Sensitive values like connection strings and API keys should never be in source control
- Flexibility: Change configuration without rebuilding or redeploying
- Platform compatibility: Works identically across Docker, Kubernetes, Azure, AWS, and on-premises
A common pattern is:
appsettings.jsoncontains non-sensitive defaults and structureappsettings.Development.jsoncontains development-specific values- Environment variables provide all sensitive and environment-specific values in production
Launch Profiles (launchSettings.json) – Local Development Only
The launchSettings.json file in the Properties folder configures how your application launches during local development. It’s commonly misunderstood, so let’s clear things up.
Critical point: launchSettings.json is NOT deployed. It only affects local development with Visual Studio, VS Code, Rider, or dotnet run.
Understanding Launch Profiles
{ "$schema": "https://json.schemastore.org/launchsettings.json", "profiles": { "http": { "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, "launchUrl": "scalar/v1", "applicationUrl": "http://localhost:5000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, "https": { "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, "launchUrl": "scalar/v1", "applicationUrl": "https://localhost:5001;http://localhost:5000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, "Production-Local": { "commandName": "Project", "launchBrowser": false, "applicationUrl": "http://localhost:5000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Production", "ConnectionStrings__DefaultConnection": "Server=localhost;Database=MyApp_Prod;..." } } }}Key Properties Explained
| Property | Purpose |
|---|---|
commandName | How to run the app. Project runs directly, IISExpress uses IIS Express |
applicationUrl | The URLs your app listens on locally |
environmentVariables | Environment variables set for this profile |
launchBrowser | Whether to open the browser automatically |
launchUrl | The relative URL to open in the browser |
Selecting a Launch Profile
dotnet CLI:
dotnet run --launch-profile "https"dotnet run --launch-profile "Production-Local"Visual Studio: Use the dropdown next to the Start button to select a profile.
Common Misconceptions
- “I put my production connection string in launchSettings.json” — Never do this.
launchSettings.jsonis for local development only and should be in.gitignoreif it contains any sensitive data. - “My app works locally but fails in production” — If you’re relying on environment variables set in
launchSettings.json, they won’t exist in production. Ensure your production environment has all required variables. - “I need launchSettings.json in my Docker image” — No, you don’t. Set environment variables in your Dockerfile or container orchestration (Kubernetes, ECS, etc.).
User Secrets – Keeping Sensitive Data Out of Source Control
User Secrets is a development-time feature that stores sensitive configuration outside your project folder. It’s perfect for API keys, connection strings, and other secrets during local development.
How User Secrets Work
Secrets are stored in a JSON file in your user profile directory:
- Windows:
%APPDATA%\Microsoft\UserSecrets\<user_secrets_id>\secrets.json - macOS/Linux:
~/.microsoft/usersecrets/<user_secrets_id>/secrets.json
Each project has a unique UserSecretsId in its .csproj file, ensuring secrets are project-specific.
Setting Up User Secrets
Step 1: Initialize User Secrets
dotnet user-secrets initThis adds a UserSecretsId to your .csproj:
<PropertyGroup> <TargetFramework>net10.0</TargetFramework> <UserSecretsId>your-unique-guid-here</UserSecretsId></PropertyGroup>Step 2: Add Secrets
dotnet user-secrets set "ConnectionStrings:DefaultConnection" "Server=localhost;Database=MyApp;Password=SuperSecret123!"dotnet user-secrets set "ExternalApi:ApiKey" "sk-1234567890abcdef"dotnet user-secrets set "Smtp:Password" "email-password-here"Step 3: View Secrets
dotnet user-secrets listStep 4: Remove Secrets
dotnet user-secrets remove "ConnectionStrings:DefaultConnection"dotnet user-secrets clear # Remove all secretsAccessing User Secrets in Code
No code changes needed. User Secrets are automatically loaded in the Development environment:
var builder = WebApplication.CreateBuilder(args);
// User Secrets are already loaded if ASPNETCORE_ENVIRONMENT=Developmentvar apiKey = builder.Configuration["ExternalApi:ApiKey"];User Secrets Best Practices
- Development only: User Secrets only work when
ASPNETCORE_ENVIRONMENT=Development. Use environment variables or a proper secrets manager (AWS Secrets Manager, Azure Key Vault) in production. - Don’t commit the secrets file: The secrets file is outside your project by design. Never copy it into your repository.
- Team sharing: User Secrets are per-machine. Each developer needs to set up their own secrets. Document which secrets are required in your README.
- Use for local overrides: Perfect for overriding
appsettings.Development.jsonwith machine-specific values like local database credentials.
The Options Pattern – Strongly Typed Configuration
While IConfiguration works, accessing configuration via string keys is error-prone. The Options Pattern provides strongly typed access to configuration sections.
public class SmtpSettings{ public string Host { get; set; } = string.Empty; public int Port { get; set; } public string Username { get; set; } = string.Empty; public string Password { get; set; } = string.Empty; public bool EnableSsl { get; set; }}appsettings.json:
{ "SmtpSettings": { "Host": "smtp.example.com", "Port": 587, "Password": "", "EnableSsl": true }}Registration:
builder.Services.Configure<SmtpSettings>( builder.Configuration.GetSection("SmtpSettings"));Usage:
public class EmailService(IOptions<SmtpSettings> options){ private readonly SmtpSettings _settings = options.Value;
public void SendEmail() { // Use _settings.Host, _settings.Port, etc. }}Configuration Precedence – The Complete Picture
Let’s consolidate the configuration precedence order. Later sources override earlier ones:
- appsettings.json — Base defaults
appsettings.{Environment}.json— Environment-specific overrides- User Secrets — Development-only secrets (only when
ASPNETCORE_ENVIRONMENT=Development) - Environment Variables — Runtime overrides
- Command-line Arguments — Highest priority
Visual Example
Given these sources:
appsettings.json:
{ "ApiSettings": { "BaseUrl": "https://api.example.com", "Timeout": 30, "RetryCount": 3 }}appsettings.Development.json:
{ "ApiSettings": { "BaseUrl": "https://dev-api.example.com" }}Environment Variable:
ApiSettings__Timeout=60Final Merged Configuration (in Development):
{ "ApiSettings": { "BaseUrl": "https://dev-api.example.com", // From appsettings.Development.json "Timeout": 60, // From environment variable "RetryCount": 3 // From appsettings.json (not overridden) }}Common Mistakes and How to Avoid Them
Mistake 1: Hardcoding Connection Strings
Bad:
{ "ConnectionStrings": { "DefaultConnection": "Server=prodserver;Database=MyApp;User=admin;Password=P@ssw0rd!" }}Better: Use User Secrets for development and environment variables for production:
{ "ConnectionStrings": { "DefaultConnection": "" }}Set the actual value via:
# Development (User Secrets)dotnet user-secrets set "ConnectionStrings:DefaultConnection" "Server=localhost;..."
# Production (Environment Variable)ConnectionStrings__DefaultConnection=Server=prodserver;...Mistake 2: Using Single Underscores in Environment Variables
Wrong:
ConnectionStrings_DefaultConnection=... # Single underscore - won't work!Correct:
ConnectionStrings__DefaultConnection=... # Double underscoreMistake 3: Forgetting ASPNETCORE_ENVIRONMENT in Production
If ASPNETCORE_ENVIRONMENT isn’t set, ASP.NET Core defaults to Production. This is usually fine, but it means:
appsettings.Development.jsonwon’t be loaded- User Secrets won’t be available
- Developer exception pages are disabled
Always explicitly set the environment in production to make configuration predictable:
ASPNETCORE_ENVIRONMENT=ProductionMistake 4: Committing launchSettings.json with Secrets
If your launchSettings.json contains sensitive environment variables for testing, add it to .gitignore:
**/Properties/launchSettings.jsonOr better: never put secrets in launchSettings.json. Use User Secrets instead.
Mistake 5: Not Validating Configuration at Startup
A missing or malformed configuration value can crash your app at runtime. Validate required settings at startup:
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");if (string.IsNullOrEmpty(connectionString)){ throw new InvalidOperationException( "DefaultConnection connection string is required. " + "Set it via environment variable: ConnectionStrings__DefaultConnection");}Or use Options validation:
builder.Services .AddOptions<SmtpSettings>() .Bind(builder.Configuration.GetSection("SmtpSettings")) .ValidateDataAnnotations() .ValidateOnStart();Mistake 6: Logging Sensitive Configuration Values
Never do this:
_logger.LogInformation("Connection string: {ConnectionString}", connectionString);If you need to verify configuration is loaded, log non-sensitive indicators:
_logger.LogInformation("Database server: {Server}", new SqlConnectionStringBuilder(connectionString).DataSource);CI/CD and Deployment Patterns
Docker
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS baseWORKDIR /appEXPOSE 8080
# Set default environmentENV ASPNETCORE_ENVIRONMENT=ProductionENV ASPNETCORE_URLS=http://+:8080
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS buildWORKDIR /srcCOPY ["MyApp.Api/MyApp.Api.csproj", "MyApp.Api/"]RUN dotnet restore "MyApp.Api/MyApp.Api.csproj"COPY . .RUN dotnet build "MyApp.Api/MyApp.Api.csproj" -c Release -o /app/build
FROM build AS publishRUN dotnet publish "MyApp.Api/MyApp.Api.csproj" -c Release -o /app/publish
FROM base AS finalWORKDIR /appCOPY --from=publish /app/publish .ENTRYPOINT ["dotnet", "MyApp.Api.dll"]Set environment variables when running the container:
docker run -d \ -e ASPNETCORE_ENVIRONMENT=Production \ -e ConnectionStrings__DefaultConnection="Server=dbserver;..." \ -e ExternalApi__ApiKey="sk-production-key" \ -p 8080:8080 \ myapp:latestGitHub Actions
- name: Deploy to Production env: ASPNETCORE_ENVIRONMENT: Production ConnectionStrings__DefaultConnection: ${{ secrets.DB_CONNECTION_STRING }} ExternalApi__ApiKey: ${{ secrets.API_KEY }} run: | # Deployment script hereAzure App Service
Set configuration in the Azure Portal under Configuration > Application settings, or via Azure CLI:
az webapp config appsettings set \ --name myapp \ --resource-group mygroup \ --settings \ ASPNETCORE_ENVIRONMENT=Production \ ConnectionStrings__DefaultConnection="Server=tcp:myserver.database.windows.net;..."Quick Reference
| What You Want | How To Do It |
|---|---|
| Set base defaults | appsettings.json |
| Override for development | appsettings.Development.json |
| Store local secrets | dotnet user-secrets set "Key" "Value" |
| Override in production | Environment variable: Key__NestedKey=value |
| Check current environment | builder.Environment.IsDevelopment() |
| Set environment | ASPNETCORE_ENVIRONMENT=Development |
| Access configuration | builder.Configuration["Section:Key"] |
| Strongly typed config | Options Pattern with IOptions<T> |
Summary
Configuration in ASP.NET Core is designed to be layered, predictable, and secure:
- appsettings.json provides base defaults that apply everywhere
appsettings.{Environment}.jsonoverrides values for specific environments- User Secrets keeps sensitive data out of source control during development
- Environment Variables are the production standard for configuration
- launchSettings.json is local-only and never deployed
The key insight is the precedence chain: each layer can override the previous one, with environment variables and command-line arguments having the final say. This lets you commit sensible defaults while keeping secrets secure.
Master this configuration system, and you’ll never struggle with environment-specific settings again. Your apps will work identically across developer machines, CI pipelines, and production servers—each with their appropriate configuration.
This article is part of our FREE .NET Web API Zero to Hero Series → Start the course here


