Environment-specific configuration in .NET

An ASP.NET Core application often moves from a developer laptop to a CI agent, staging, and production without changing its code. Each environment needs different connection strings, API endpoints, feature switches and logging levels. .NET resolves those differences through ordered configuration providers, allowing deployment settings to override shared defaults at runtime.

A dependable setup goes beyond copying appsettings files. It defines a naming contract, separates settings from secrets, validates required values and tests the same keys used during deployment. Those concerns are practical for Australian teams spread across Sydney, Melbourne and Brisbane, where the same application may meet Azure, AWS and on-premises environments.

Understand the configuration pipeline

WebApplication.CreateBuilder(args) loads appsettings.json, appsettings.{Environment}.json, user secrets during development, environment variables and command-line arguments. The placeholder in the second filename is replaced with the current environment name, such as Development, Staging or Production.

Later providers win when they define the same key. An environment variable can therefore replace a value from appsettings.json, and an appsettings.Staging.json value can replace the base file. Environment selection and value overriding are separate decisions: ASPNETCORE_ENVIRONMENT chooses the overlay, while the provider order decides which value survives.

Keep shared, non-sensitive defaults in appsettings.json. Put deliberate environment differences in matching overlays, and reserve runtime overrides for deployment-specific values and secrets. This arrangement also avoids scattering environment checks throughout controllers and services. This model applies to modern .NET; classic .NET Framework applications use a different XML-based configuration system.

Choose a predictable naming scheme

Adopt hierarchical keys with a colon, such as Database:ConnectionString, ConnectionStrings:Default and Features:NewCheckout. Bind the same sections in every environment so an application can move between them without changing its contracts. These names are case-insensitive, but changing their spelling or nesting still creates a missing setting.

Environment variables replace the colon with a double underscore. Database__ConnectionString maps to Database:ConnectionString, while ConnectionStrings__Default maps to ConnectionStrings:Default. The default environment-variable provider has no prefix. If you introduce one, update the binding logic and documentation together.

Document which values may change without a deployment, such as feature flags, and which require a release or restart. Connection strings and API credentials belong in the protected group. This simple classification prevents a harmless toggle and a sensitive credential from sharing the same revision process.

Separate settings, secrets and policies

Settings can live in ordinary configuration files and be reviewed with the source code. Secrets need narrower access, rotation and auditing. A connection string is a secret even when its host name appears in a diagram; an API endpoint normally is not.

Use user secrets for local credentials and an approved secret manager for hosted environments. Azure Key Vault with a managed identity removes credentials from deployment scripts. If the application serves Australian customers, choose an Australian region such as Azure Australia East or Australia Southeast after checking data-residency and support requirements.

Vault key names and configuration keys are not always identical. Test how your chosen provider translates flat names into nested sections, and keep the deployment override convention consistent. A secret that loads under a slightly different name can leave the application running with an empty default.

Australian organisations covered by the Privacy Act 1988 and the Australian Privacy Principles should treat any personal information near configuration carefully. Keep identifiers out of general settings, restrict access to deployment values, and never include connection strings or access tokens in startup logs. An Angular client also needs the same boundary: reviewing environment configuration can help document which values are browser-safe and which remain server-only.

Configure local and hosted environments

Start with a small baseline in appsettings.json. The provider names and feature defaults can be committed because they reveal no credentials.

{
  "Database": {
    "Provider": "SqlServer"
  },
  "Features": {
    "NewCheckout": false
  }
}

Development overrides belong in appsettings.Development.json when the difference is safe to review, such as a feature flag or log level. Leave the connection string empty so validation exposes an unconfigured local setup. A launch profile can select the local environment, but credentials belong in user secrets.

{
  "Database": {
    "ConnectionString": ""
  },
  "Features": {
    "NewCheckout": true
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  }
}

Store the local connection string in user secrets, which live outside the repository in the developer’s profile:

dotnet user-secrets init
dotnet user-secrets set "Database:ConnectionString" "Server=localhost;Database=AppDb;Encrypt=True;TrustServerCertificate=True"

On CI and hosting platforms, provide Database__ConnectionString and Features__NewCheckout through protected variables or the platform’s secret store. A .env file is not read automatically by ASP.NET Core, and loading one requires deliberate code. Never commit a production .env file or paste its values into appsettings.Production.json.

Bind and validate strongly typed options

Create an options class for each meaningful configuration section. It gives the application a typed contract and a single place to document requirements.

public sealed class DatabaseOptions
{
    public const string SectionName = "Database";

    public string Provider { get; set; } = "SqlServer";
    public string ConnectionString { get; set; } = string.Empty;
}

Bind it during startup and reject missing or invalid values before the server accepts traffic:

builder.Services.AddOptions<DatabaseOptions>()
    .Bind(builder.Configuration.GetSection(DatabaseOptions.SectionName))
    .Validate(options => !string.IsNullOrWhiteSpace(options.ConnectionString),
        "Database:ConnectionString is required.")
    .ValidateOnStart();

Inject IOptions<T> when a value should remain stable for the process lifetime. Use IOptionsMonitor<T> only when the provider supports reloading and the consumer can tolerate change. A rotated connection string may also require a new connection pool, so treat that setting as restart-controlled rather than assuming a reload updates every client.

Keep configuration observable and testable

Configuration mistakes often resemble application faults: a missing section becomes a null reference, or a wrong connection string causes a timeout. Log the environment name and release identifier at startup, and expose a readiness check that verifies dependency access without returning secret values.

Do not paste IConfiguration.DebugView into production logs; it can expose connection strings and tokens. Unit-test the validation rules with in-memory values, then run an integration test under the Staging environment using the real key names. WebApplicationFactory<Program> provides a practical way to exercise startup and dependency wiring.

Australian background jobs also deserve timezone tests. Australia/Sydney observes daylight saving, while Australia/Brisbane does not. Use IANA identifiers, which .NET can map across Windows and Linux, instead of a server-specific Windows zone name. A job scheduled in Brisbane should not quietly shift when Sydney clocks change.

Create a rollout checklist for Australian teams

A release can work on a laptop and fail in a hosted environment when key names drift. Keep a configuration inventory and test it against each target. This discipline helps teams that develop in Brisbane but deploy through a Sydney AWS region or a Melbourne Azure region.

Include operational ownership in the inventory. Australian budgets are often denominated in Australian dollars while cloud invoices arrive in United States dollars, so a failover option can add cost as well as resilience. Record the person or team responsible for each value and its rotation schedule.

Start the rollout with one API this week: inventory its configuration keys, move its production credentials into the secret store, and run a staging deployment using the same names and validation rules. Make that release the template for every service that follows.