Skip to main content

.NET Configuration

The .NET configuration system provides a flexible, extensible mechanism for managing application settings from multiple sources. It uses a layered approach where configuration values from different sources are merged, with later sources overriding earlier ones.

Why Configuration?​

Before diving into .NET Configuration, let's address a more fundamental question: what is configuration and why do we need it?

In a nutshell, configuration allows you to change the way your application behaves without modifying the code itself.

An Example​

Most applications need to connect to a database. To establish that connection, we need:

  • Database server location (hostname and port)
  • Authentication credentials (username and password)
  • Database or schema name

We could hardcode these values directly in our application, and it would work. However:

  • Security risk: Credentials would be visible in the codebase. If your code lives in a repository (GitHub, GitLab, etc.), anyone with access can see this information. For private repositories this is a security risk, for public repositories this is a security disaster!
  • Environment differences: Development, staging, and production environments each need different connection strings. Hardcoding means maintaining separate code versions.
  • Inflexibility: Changing these values requires code changes, recompilation, and redeployment, a slow and potentially risky process.

Configuration solves these problems by extracting environment-specific and sensitive values from the codebase into external sources that are:

  • Secure: Sensitive values stored outside source control
  • Environment-specific: Different values for dev, staging, and production
  • Flexible: Can be changed without code modifications or redeployment
  • Manageable: Operations teams can manage values without touching code

Configuration Sources​

.NET applications can pull configuration from various sources, each serving different purposes and scenarios.

appsettings.json​

The default JSON configuration file included in every ASP.NET Core project.

{
"Logging": {
"LogLevel": {
"Default": "Information"
}
},
"ConnectionStrings": {
"DefaultConnection": "Server=localhost;Database=CommandDB;"
}
}

When to use:

  • Default application settings
  • Settings that are common across all environments
  • Non-sensitive configuration values
  • Settings that might be modified by operations teams

Characteristics:

  • Checked into source control
  • Visible to anyone with repository access
  • Easy to edit and maintain
  • Can be overridden by other sources

appsettings.{Environment}.json​

Environment-specific configuration files that override base settings.

// appsettings.Development.json
{
"Logging": {
"LogLevel": {
"Default": "Debug"
}
},
"ConnectionStrings": {
"DefaultConnection": "Server=localhost;Database=CommandDB_Dev;"
}
}

When to use:

  • Environment-specific values (Development, Staging, Production)
  • Overriding default appsettings for specific environments
  • Different logging levels per environment
  • Environment-specific connection strings

Characteristics:

  • Automatically loaded based on ASPNETCORE_ENVIRONMENT variable
  • Overrides values in base appsettings.json
  • Also checked into source control
  • Keeps environment differences visible and maintainable

User Secrets​

Stored locally on the developer's machine, outside the project directory.

dotnet user-secrets set "ApiKeys:OpenAI" "sk-proj-abc123..."
// Stored in: ~/.microsoft/usersecrets/{user-secrets-id}/secrets.json
{
"ApiKeys": {
"OpenAI": "sk-proj-abc123..."
}
}

When to use:

  • Local development secrets (API keys, passwords)
  • Connection strings with credentials during development
  • Any sensitive data needed for local testing
  • Avoiding accidental commits of secrets to source control

Characteristics:

  • Development only - not available in production
  • Stored per user, per machine
  • Not checked into source control
  • Accessed via Secret Manager tool
  • Requires UserSecretsId in .csproj file

Environment Variables​

System-level or process-level variables set on the host machine.

# Linux/macOS
export ConnectionStrings__DefaultConnection="Server=prod-db;Database=CommandDB;"

# Windows
set ConnectionStrings__DefaultConnection=Server=prod-db;Database=CommandDB;

When to use:

  • Production secrets and sensitive configuration
  • Container and cloud deployments (Docker, Kubernetes)
  • CI/CD pipeline configuration
  • Server-specific settings
  • Configuration that varies per deployment instance

Characteristics:

  • Highly secure - not stored in code or files
  • Supported across all platforms and hosting environments
  • Use double underscore __ for hierarchical keys (e.g., ConnectionStrings__DefaultConnection)
  • Essential for cloud-native applications

Command-Line Arguments​

Configuration passed when starting the application.

dotnet run --ConnectionStrings:DefaultConnection="Server=localhost;Database=TestDB;"

When to use:

  • Temporary overrides during testing
  • Script-based deployments
  • Debugging specific scenarios
  • One-off configuration changes

Characteristics:

  • Highest precedence - overrides all other sources
  • Useful for quick testing without modifying files
  • Use colon : for hierarchical keys in command line
  • Not persistent

Azure Key Vault / Cloud Secrets Managers​

Cloud-based secret management services (Azure Key Vault, AWS Secrets Manager, etc.).

When to use:

  • Production secrets requiring enterprise-grade security
  • Centralized secret management across applications
  • Secrets with rotation requirements
  • Compliance and audit requirements

Characteristics:

  • Requires additional NuGet packages
  • Network dependency for configuration loading
  • Cached after initial load
  • Can integrate with managed identities

Order of Precedence​

Configuration sources are applied in a specific order, with later sources overriding earlier ones:

1. appsettings.json (lowest priority)
↓
2. appsettings.{Environment}.json
↓
3. User Secrets (Development only)
↓
4. Environment Variables
↓
5. Command-Line Arguments (highest priority)

Example: If the same setting exists in multiple sources:

  • appsettings.json: "LogLevel": "Information"
  • appsettings.Development.json: "LogLevel": "Debug"
  • Environment Variable: Logging__LogLevel__Default=Warning
  • Command-Line: --Logging:LogLevel:Default=Error

Result: LogLevel = Error (command-line wins)

Accessing Configuration​

Configuration is accessed through the IConfiguration interface, which is registered in the dependency injection container by default.

In Program.cs​

var builder = WebApplication.CreateBuilder(args);

// Access configuration directly from builder
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
var apiKey = builder.Configuration["ApiKeys:OpenAI"];
var logLevel = builder.Configuration["Logging:LogLevel:Default"];

// Use configuration to setup services
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection"))
);

var app = builder.Build();

In Controllers or Services​

Inject IConfiguration through constructor dependency injection:

public class PlatformsController : ControllerBase
{
private readonly IConfiguration _configuration;

public PlatformsController(IConfiguration configuration)
{
_configuration = configuration;
}

[HttpGet("info")]
public ActionResult GetInfo()
{
var appName = _configuration["ApplicationName"];
var environment = _configuration["ASPNETCORE_ENVIRONMENT"];

return Ok(new { appName, environment });
}
}

Strongly-Typed Configuration​

Bind configuration sections to strongly-typed classes using the Options pattern:

// Configuration class
public class DatabaseOptions
{
public string ConnectionString { get; set; } = string.Empty;
public int MaxRetries { get; set; }
public int CommandTimeout { get; set; }
}

// In Program.cs
builder.Services.Configure<DatabaseOptions>(
builder.Configuration.GetSection("Database")
);

// In a service
public class DataService
{
private readonly DatabaseOptions _dbOptions;

public DataService(IOptions<DatabaseOptions> dbOptions)
{
_dbOptions = dbOptions.Value;
}
}

Configuration Keys​

Access nested configuration using colon : notation:

{
"Database": {
"ConnectionString": "...",
"Pool": {
"MaxSize": 100
}
}
}
var maxSize = _configuration["Database:Pool:MaxSize"];
// or
var maxSize = _configuration.GetValue<int>("Database:Pool:MaxSize");

Best Practices​

Do:

  • Store non-sensitive defaults in appsettings.json
  • Use User Secrets for local development secrets
  • Use environment variables for production secrets
  • Use strong typing with the Options pattern
  • Document required configuration keys

Don't:

  • Commit secrets to source control
  • Hardcode sensitive values in code
  • Use appsettings.json for production secrets
  • Access configuration everywhere - centralize in services

Common Pitfalls​

  1. Case sensitivity: Configuration keys are case-insensitive, but it's best to be consistent
  2. Hierarchical separators: Use : in code, __ in environment variables
  3. User Secrets in Production: They only work in Development environment
  4. Missing configuration: Always provide defaults or validate required settings at startup

Further Reading: