← Back to Article List         
Configuration Across Environments

Configuration Across Environments

Published on 27 Sep 2026     8 min read ASP.NET Core MVC
MVC

ASP.NET Core manages configuration using multiple sources. Environment-specific sources override common configuration values.

Configuration priority

With the default setup, later sources override earlier ones:

appsettings.json
        ↓ overridden by
appsettings.{Environment}.json
        ↓
User Secrets in Development
        ↓
Environment variables
        ↓
Command-line arguments

1. Common configuration

appsettings.json contains settings shared by all environments:

{
  "ApplicationSettings": {
    "ApplicationName": "Portfolio"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information"
    }
  }
}

2. Environment-specific configuration

appsettings.Development.json:

{
  "Logging": {
    "LogLevel": {
      "Default": "Debug"
    }
  }
}

appsettings.Production.json:

{
  "Logging": {
    "LogLevel": {
      "Default": "Warning"
    }
  }
}

ASP.NET Core automatically loads the file matching the current environment.

3. Set the environment

Common environment names are:

  • Development
  • Staging
  • Production

Windows PowerShell:

$env:ASPNETCORE_ENVIRONMENT = "Development"

Windows Command Prompt:

set ASPNETCORE_ENVIRONMENT=Development

Linux:

export ASPNETCORE_ENVIRONMENT=Production

For local development, launchSettings.json can set it:

{
  "profiles": {
    "MyMvcApp": {
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}

launchSettings.json is only for local development and is not published.

4. Read configuration

public class HomeController : Controller
{
    private readonly IConfiguration _configuration;

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

    public IActionResult Index()
    {
        string? appName =
            _configuration["ApplicationSettings:ApplicationName"];

        return View();
    }
}

5. Use the Options pattern

Create a strongly typed class:

public class ApplicationSettings
{
    public string ApplicationName { get; set; } = string.Empty;
}

Register it:

builder.Services.Configure<ApplicationSettings>(
    builder.Configuration.GetSection("ApplicationSettings"));

Inject it:

public class HomeController : Controller
{
    private readonly ApplicationSettings _settings;

    public HomeController(
        IOptions<ApplicationSettings> options)
    {
        _settings = options.Value;
    }
}

In ASP.NET Core web applications, the Options API is built in; an external library normally isn’t required.

6. Manage secrets safely

Do not store passwords, API keys or production connection strings in appsettings.json.

For local development, use User Secrets:

dotnet user-secrets init
dotnet user-secrets set "ConnectionStrings:DefaultConnection" "Server=..."

Read it normally:

var connectionString =
    builder.Configuration.GetConnectionString("DefaultConnection");

For production, use:

  • Environment variables
  • Azure Key Vault
  • AWS Secrets Manager
  • Kubernetes Secrets

Nested configuration keys use double underscores in environment variables:

ConnectionStrings__DefaultConnection

This represents:

ConnectionStrings:DefaultConnection

Environment checks

if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseExceptionHandler("/Home/Error");
    app.UseHsts();
}

Key points

  • Keep shared settings in appsettings.json.
  • Keep environment-specific non-secret settings in corresponding JSON files.
  • Use User Secrets only for local development.
  • Use a secure secret store or environment variables in production.
  • Prefer strongly typed Options over repeatedly accessing IConfiguration.
  • Environment variables override values from appsettings.json.

 

Explain the ASP.NET Core configuration-provider hierarchy.

A configuration provider reads settings from a source such as JSON files, User Secrets, environment variables, or command-line arguments.

When the same key exists in multiple providers:

The provider added later overrides the earlier provider.

Default hierarchy

WebApplication.CreateBuilder(args) loads application configuration in this order, from lowest to highest priority:

Priority Provider Typical purpose
1 appsettings.json Common settings
2 appsettings.{Environment}.json Environment-specific settings
3 User Secrets Local development secrets
4 Environment variables Deployment and production settings
5 Command-line arguments Runtime overrides
Command-line arguments       ← Highest priority
Environment variables
User Secrets (Development)
appsettings.{Environment}.json
appsettings.json              ← Lowest priority

Override example

appsettings.json:

{
  "ApiSettings": {
    "Timeout": 30
  }
}

appsettings.Production.json:

{
  "ApiSettings": {
    "Timeout": 60
  }
}

Environment variable:

ApiSettings__Timeout=90

Command line:

dotnet run --ApiSettings:Timeout=120

Final value:

int timeout =
    builder.Configuration.GetValue<int>("ApiSettings:Timeout");

// Result: 120

The command-line value wins because it has the highest priority.

Hierarchical keys

JSON uses nested objects:

{
  "ConnectionStrings": {
    "DefaultConnection": "Server=localhost"
  }
}

Access it with ::

string? connectionString =
    builder.Configuration["ConnectionStrings:DefaultConnection"];

Environment variables use __ because : is not supported consistently across operating systems:

ConnectionStrings__DefaultConnection=Server=production

ASP.NET Core automatically converts __ into :.

User Secrets

User Secrets are loaded only in the Development environment:

dotnet user-secrets set "ApiSettings:ApiKey" "secret-value"

They override JSON files but are overridden by environment variables and command-line arguments.

launchSettings.json

launchSettings.json is not directly an application configuration provider. It is a local development file that sets environment variables when the application is started through Visual Studio or dotnet run.

{
  "environmentVariables": {
    "ASPNETCORE_ENVIRONMENT": "Development"
  }
}

It is not published with the application.

Customizing the hierarchy

You can add another provider:

var builder = WebApplication.CreateBuilder(args);

builder.Configuration.AddJsonFile(
    "customsettings.json",
    optional: true,
    reloadOnChange: true);

Because it is added after the default providers, its matching values can override them.

Other available providers include:

  • Azure Key Vault
  • Azure App Configuration
  • XML and INI files
  • In-memory collections
  • Key-per-file
  • Custom providers

Some providers require their corresponding NuGet package.

Host vs application configuration

ASP.NET Core also has host configuration for startup settings such as:

  • Environment name
  • Application name
  • Content root
  • Web root

Common host environment variables include:

DOTNET_ENVIRONMENT
ASPNETCORE_ENVIRONMENT

Application configuration normally has higher priority than fallback host configuration.

Key points

  • Configuration is stored as case-insensitive key-value pairs.
  • Later providers override earlier providers for matching keys.
  • Environment-specific JSON overrides appsettings.json.
  • Environment variables override JSON files and User Secrets.
  • Command-line arguments have the highest default application priority.
  • Never store production passwords or API keys in JSON files.
  • Prefer the Options pattern for related configuration values.

Reference: Microsoft—Configuration in ASP.NET Core.

 

How do you use the Options pattern?

The Options pattern converts configuration sections into strongly typed C# classes.

It is better than repeatedly using:

configuration["EmailSettings:SmtpServer"]

1. Add configuration

appsettings.json:

{
  "EmailSettings": {
    "SmtpServer": "smtp.example.com",
    "Port": 587,
    "FromAddress": "admin@example.com"
  }
}

2. Create an Options class

public class EmailSettings
{
    public const string SectionName = "EmailSettings";

    public string SmtpServer { get; set; } = string.Empty;
    public int Port { get; set; }
    public string FromAddress { get; set; } = string.Empty;
}

Property names should match the configuration keys.

3. Register it in Program.cs

builder.Services.Configure<EmailSettings>(
    builder.Configuration.GetSection(EmailSettings.SectionName));

4. Inject and use it

using Microsoft.Extensions.Options;

public class EmailService
{
    private readonly EmailSettings _settings;

    public EmailService(IOptions<EmailSettings> options)
    {
        _settings = options.Value;
    }

    public void SendEmail()
    {
        Console.WriteLine(_settings.SmtpServer);
        Console.WriteLine(_settings.Port);
    }
}

Register the service:

builder.Services.AddScoped<EmailService>();

Options interfaces

Interface Lifetime Configuration changes
IOptions<T> Singleton Does not read changes after startup
IOptionsSnapshot<T> Scoped Reads changes once per request
IOptionsMonitor<T> Singleton Reads current values and supports notifications

IOptions<T>

Use for configuration that does not need to change while the application is running:

public EmailService(IOptions<EmailSettings> options)
{
    _settings = options.Value;
}

IOptionsSnapshot<T>

Use in controllers or Scoped services when configuration may change:

public EmailService(
    IOptionsSnapshot<EmailSettings> options)
{
    _settings = options.Value;
}

A new snapshot is created for each HTTP request. It cannot be injected into a Singleton.

IOptionsMonitor<T>

Use in Singleton services or when immediate change notifications are required:

public class EmailService
{
    private readonly IOptionsMonitor<EmailSettings> _options;

    public EmailService(
        IOptionsMonitor<EmailSettings> options)
    {
        _options = options;
    }

    public void SendEmail()
    {
        var settings = _options.CurrentValue;
    }
}

Listen for changes:

_options.OnChange(settings =>
{
    Console.WriteLine($"New server: {settings.SmtpServer}");
});

Validate configuration

Add validation attributes:

using System.ComponentModel.DataAnnotations;

public class EmailSettings
{
    public const string SectionName = "EmailSettings";

    [Required]
    public string SmtpServer { get; set; } = string.Empty;

    [Range(1, 65535)]
    public int Port { get; set; }

    [Required, EmailAddress]
    public string FromAddress { get; set; } = string.Empty;
}

Register with validation:

builder.Services
    .AddOptions<EmailSettings>()
    .Bind(builder.Configuration.GetSection(
        EmailSettings.SectionName))
    .ValidateDataAnnotations()
    .ValidateOnStart();

ValidateOnStart() stops the application during startup if the configuration is invalid.

Library information

For a normal ASP.NET Core Web application, the Options API is included. Usually, no additional package is required.

Namespaces:

using Microsoft.Extensions.Options;
using System.ComponentModel.DataAnnotations;

For a non-web project, or if ValidateDataAnnotations() is unavailable:

dotnet add package Microsoft.Extensions.Options.ConfigurationExtensions
dotnet add package Microsoft.Extensions.Options.DataAnnotations

Key points

  • Options provide strongly typed configuration.
  • Register them using Configure<T>() or AddOptions<T>().
  • Use IOptions<T> for fixed settings.
  • Use IOptionsSnapshot<T> for per-request refreshed settings.
  • Use IOptionsMonitor<T> for live settings and Singleton services.
  • Validate important configuration during application startup.

Reference: Microsoft—Options pattern in ASP.NET Core.

 

What is the difference between IOptions<T>, IOptionsSnapshot<T>, and IOptionsMonitor<T>?

All three provide strongly typed configuration, but they differ in lifetime and how they handle configuration changes.

Feature IOptions<T> IOptionsSnapshot<T> IOptionsMonitor<T>
DI lifetime Singleton Scoped Singleton
Reads changes No Once per request Immediately/current value
Access property .Value .Value .CurrentValue
Change notification No No Yes, using OnChange()
Named options No Yes Yes
Can inject into Singleton Yes No Yes
Best used for Fixed settings Updated settings per request Live settings/Singleton services

IOptions<T>

Reads the configuration and keeps the same value for the application lifetime.

public EmailService(IOptions<EmailSettings> options)
{
    EmailSettings settings = options.Value;
}

Use when:

  • Settings do not change while the application is running.
  • The application can restart to apply configuration changes.
  • You need options inside any service lifetime.

IOptionsSnapshot<T>

Creates a new configuration snapshot once per HTTP request.

public EmailService(
    IOptionsSnapshot<EmailSettings> options)
{
    EmailSettings settings = options.Value;
}

Use when:

  • Configuration changes should be reflected on the next request.
  • The consuming service is Scoped or Transient.

It cannot be injected into a Singleton because IOptionsSnapshot<T> is Scoped.

IOptionsMonitor<T>

Provides the current configuration value and supports change notifications.

public class EmailService
{
    private readonly IOptionsMonitor<EmailSettings> _options;

    public EmailService(
        IOptionsMonitor<EmailSettings> options)
    {
        _options = options;

        _options.OnChange(settings =>
        {
            Console.WriteLine(
                $"New server: {settings.SmtpServer}");
        });
    }

    public void SendEmail()
    {
        EmailSettings settings = _options.CurrentValue;
    }
}

Use when:

  • Configuration changes must be available without restarting.
  • Change notifications are required.
  • Options must be injected into a Singleton or background service.

The configuration provider must support reloads. The default JSON provider supports them with reloadOnChange.

Simple memory technique

  • IOptions<T> → Fixed for the application.
  • IOptionsSnapshot<T> → Refreshed for each request.
  • IOptionsMonitor<T> → Current value plus change notifications.

For most normal settings, start with IOptions<T>. Use Snapshot or Monitor only when runtime configuration changes are genuinely required.