Skip to content

.NET Core · ASP.NET Core

ASP.NET Core Configuration in Depth

7 min read Updated 2026-06-23 Share:

Practice Configuration interview questions

Why configuration knowledge is interview gold

Configuration management is where many production incidents originate — wrong connection strings, missing API keys, config drift between environments, or features that mysteriously stop working because a value changed mid-flight. Interviewers test this topic to gauge whether a developer knows how to make configuration safe, validated, and environment-aware.

How the configuration system works

ASP.NET Core aggregates key-value pairs from multiple configuration providers into a unified IConfiguration interface. Later providers override earlier ones for the same key.

// Default provider stack (WebApplication.CreateBuilder):
// 1. appsettings.json
// 2. appsettings.{Environment}.json   ← overrides 1
// 3. User Secrets (Development only)  ← overrides 1+2
// 4. Environment variables            ← overrides everything below
// 5. Command-line arguments           ← highest priority

var builder = WebApplication.CreateBuilder(args);

// Read a simple value:
string? dbConn = builder.Configuration["ConnectionStrings:DefaultConnection"];

// GetConnectionString is a shortcut:
string? dbConn2 = builder.Configuration.GetConnectionString("DefaultConnection");

// GetSection returns a sub-node:
IConfigurationSection mail = builder.Configuration.GetSection("Mail");
string? host = mail["Host"];
int port     = mail.GetValue<int>("Port", defaultValue: 587);

appsettings.json:

{
  "ConnectionStrings": {
    "DefaultConnection": "Server=.;Database=MyDb;Trusted_Connection=True"
  },
  "Mail": {
    "Host": "smtp.example.com",
    "Port": 587,
    "UseSsl": true
  }
}

Key rule: never hard-code secrets or environment-specific values in source code. The configuration system exists precisely so those values can be overridden without a code change.

Configuration providers — understanding the priority stack

// Customizing the stack:
builder.Host.ConfigureAppConfiguration((ctx, config) =>
{
    config.Sources.Clear(); // start fresh if needed

    config.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
    config.AddJsonFile($"appsettings.{ctx.HostingEnvironment.EnvironmentName}.json",
        optional: true, reloadOnChange: true);

    if (ctx.HostingEnvironment.IsDevelopment())
        config.AddUserSecrets<Program>();

    // Environment variables: double underscore __ maps to :
    // MYAPP_Mail__Host → Mail:Host
    config.AddEnvironmentVariables(prefix: "MYAPP_");

    config.AddCommandLine(args);

    // Azure Key Vault (requires NuGet: Azure.Extensions.AspNetCore.Configuration.Secrets):
    // config.AddAzureKeyVault(new Uri("https://vault.azure.net/"), new DefaultAzureCredential());
});

Environment variable key separator: __ (double underscore) maps to :. MYAPP_Mail__Host → config key Mail:Host. MYAPP_ConnectionStrings__DefaultConnectionConnectionStrings:DefaultConnection.

This means you can override any nested config value with an environment variable on any platform, including Docker containers and Kubernetes.

Reading config — Bind, Get<T>, and GetValue

// GetValue<T> — single key with optional default:
string name   = config.GetValue<string>("App:Name") ?? "DefaultApp";
int maxItems  = config.GetValue<int>("App:MaxItems", 50);
bool debugMode = config.GetValue<bool>("App:DebugMode");

// GetSection + Get<T> — deserialize a section to a POCO:
AppOptions opts = config.GetSection("App").Get<AppOptions>()!;

// Bind — populate an existing object:
var opts2 = new AppOptions();
config.GetSection("App").Bind(opts2);

// Arrays / lists:
string[] tags = config.GetSection("App:Tags").Get<string[]>()!;

// Check section existence before binding:
var section = config.GetSection("OptionalFeature");
if (section.Exists())
    section.Bind(featureOptions);

Prefer the Options pattern over calling IConfiguration directly in services — it gives you DI, validation, reload support, and testability. Use IConfiguration directly only in startup code (Program.cs) or in simple console apps.

The Options pattern — IOptions, IOptionsSnapshot, IOptionsMonitor

Bind a config section to a strongly-typed class and inject it via DI:

// POCO — convention: add SectionName constant:
public class MailOptions
{
    public const string SectionName = "Mail";
    public string Host { get; set; } = default!;
    public int Port { get; set; } = 587;
    public bool UseSsl { get; set; } = true;
    public string FromAddress { get; set; } = default!;
}

// Register:
builder.Services.Configure<MailOptions>(
    builder.Configuration.GetSection(MailOptions.SectionName));

// .NET 7+ shorthand:
builder.Services.AddOptions<MailOptions>()
    .BindConfiguration(MailOptions.SectionName);

The three interfaces differ in lifetime and reload behavior:

// IOptions<T> — singleton; reads config ONCE at startup:
public class EmailService
{
    private readonly MailOptions _opts;
    public EmailService(IOptions<MailOptions> opts) => _opts = opts.Value;
    // _opts.Host never changes, even if appsettings.json is edited on disk
}

// IOptionsSnapshot<T> — scoped; fresh per HTTP request:
public class EmailService
{
    private readonly MailOptions _opts;
    public EmailService(IOptionsSnapshot<MailOptions> opts) => _opts = opts.Value;
    // New value takes effect on the next request after a reload
}

// IOptionsMonitor<T> — singleton; sees changes immediately:
public class EmailService
{
    private readonly IOptionsMonitor<MailOptions> _monitor;
    public EmailService(IOptionsMonitor<MailOptions> monitor)
    {
        _monitor = monitor;
        monitor.OnChange(newOpts =>
            Console.WriteLine($"Config changed: Host={newOpts.Host}"));
    }

    public string CurrentHost => _monitor.CurrentValue.Host; // always fresh
}
InterfaceDI LifetimeSees reload?Use when
IOptions<T>SingletonNeverStartup-only, singletons
IOptionsSnapshot<T>ScopedPer-requestWeb requests, feature flags
IOptionsMonitor<T>SingletonImmediatelyBackground workers, long-running singletons

Options validation — fail fast at startup

Without validation, bad config causes a runtime error when the feature is first used. ValidateOnStart() makes the app refuse to start if config is invalid:

public class SmtpOptions
{
    [Required]
    public string Host { get; set; } = default!;

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

    [Required, EmailAddress]
    public string FromAddress { get; set; } = default!;
}

builder.Services
    .AddOptions<SmtpOptions>()
    .BindConfiguration("Smtp")
    .ValidateDataAnnotations()
    .ValidateOnStart(); // throw on startup, not on first use

// Custom validation logic:
builder.Services
    .AddOptions<SmtpOptions>()
    .BindConfiguration("Smtp")
    .Validate(o => o.Port != 25 || !o.Host.Contains(".corp"),
        "Port 25 blocked for corp mail servers")
    .ValidateOnStart();

A ValidateOnStart failure throws OptionsValidationException during app.Run() — the app exits cleanly with a clear error message rather than silently misbehaving in production.

User Secrets — safe development credentials

User Secrets store sensitive config outside the repo directory so they are never accidentally committed:

# Initialize (adds <UserSecretsId> to .csproj):
dotnet user-secrets init

# Set values:
dotnet user-secrets set "ConnectionStrings:DefaultConnection" "Server=dev-db;..."
dotnet user-secrets set "Stripe:SecretKey" "sk_test_abc123"
dotnet user-secrets set "Mail:Password" "dev-smtp-password"

# List:
dotnet user-secrets list

# Remove:
dotnet user-secrets remove "Mail:Password"

# Clear all:
dotnet user-secrets clear

Stored at %APPDATA%\Microsoft\UserSecrets\<UserSecretsId>\secrets.json (Windows) or ~/.microsoft/usersecrets/<id>/secrets.json (Linux/macOS).

WebApplication.CreateBuilder automatically loads them in Development:

// This is what the builder does automatically in Development:
if (ctx.HostingEnvironment.IsDevelopment())
    config.AddUserSecrets<Program>();

User Secrets are a development convenience only — in staging and production, use environment variables or a secrets manager (Azure Key Vault, AWS Secrets Manager, HashiCorp Vault).

Environment-specific configuration

ASPNETCORE_ENVIRONMENT controls which appsettings.{Env}.json overlay is loaded:

// appsettings.json — base config, in source control, no secrets:
{
  "Logging": { "LogLevel": { "Default": "Warning" } },
  "FeatureFlags": { "NewCheckout": false }
}

// appsettings.Development.json — local overrides, can be committed:
{
  "Logging": { "LogLevel": { "Default": "Debug", "Microsoft": "Information" } },
  "FeatureFlags": { "NewCheckout": true }
}

// appsettings.Production.json — minimal; real secrets come from env vars:
{
  "Logging": { "LogLevel": { "Default": "Error" } }
}
// Check environment in code:
if (app.Environment.IsDevelopment())
    app.UseDeveloperExceptionPage();

// Custom environment names:
// ASPNETCORE_ENVIRONMENT=Staging → loads appsettings.Staging.json
if (app.Environment.IsEnvironment("Staging"))
    EnableBluegreenRouting();

Standard environments: Development, Staging, Production. Custom names are allowed.

Named options — multiple instances of the same type

Named options let you configure multiple instances of the same options class:

// Two SMTP configs:
builder.Services.Configure<SmtpOptions>("Transactional",
    builder.Configuration.GetSection("Smtp:Transactional"));
builder.Services.Configure<SmtpOptions>("Marketing",
    builder.Configuration.GetSection("Smtp:Marketing"));

// Inject via IOptionsMonitor — get by name:
public class EmailDispatcher
{
    private readonly IOptionsMonitor<SmtpOptions> _monitor;
    public EmailDispatcher(IOptionsMonitor<SmtpOptions> monitor) => _monitor = monitor;

    public SmtpOptions GetTransactional() => _monitor.Get("Transactional");
    public SmtpOptions GetMarketing()     => _monitor.Get("Marketing");
}

IHttpClientFactory uses named options internally — each AddHttpClient("name", ...) is a named options configuration under the hood.

PostConfigure — guaranteed last word

PostConfigure runs after all Configure calls for the same type, making it useful for enforcing global constraints regardless of what third-party libraries set:

// Third-party library:
builder.Services.Configure<CacheOptions>(o => { o.MaxSize = 1000; o.Expiry = TimeSpan.FromMinutes(5); });

// Your constraint — always runs last:
builder.Services.PostConfigure<CacheOptions>(o =>
{
    if (o.MaxSize > 500) o.MaxSize = 500;  // cap regardless of library setting
});

// PostConfigureAll — applies to ALL named instances:
builder.Services.PostConfigureAll<SmtpOptions>(o => o.UseSsl = true); // enforce SSL everywhere

Configuration in background services and console apps

The configuration system is framework-agnostic — it works equally well outside ASP.NET Core:

var host = Host.CreateDefaultBuilder(args)
    .ConfigureAppConfiguration((ctx, config) =>
    {
        config.AddJsonFile("appsettings.json");
        config.AddEnvironmentVariables();
        if (ctx.HostingEnvironment.IsDevelopment())
            config.AddUserSecrets<Program>();
    })
    .ConfigureServices((ctx, services) =>
    {
        services.AddOptions<WorkerOptions>()
            .BindConfiguration("Worker")
            .ValidateDataAnnotations()
            .ValidateOnStart();

        services.AddHostedService<MyWorker>();
    })
    .Build();

await host.RunAsync();

public class MyWorker : BackgroundService
{
    private readonly WorkerOptions _opts;
    public MyWorker(IOptions<WorkerOptions> opts) => _opts = opts.Value;

    protected override async Task ExecuteAsync(CancellationToken ct)
    {
        while (!ct.IsCancellationRequested)
        {
            DoWork();
            await Task.Delay(TimeSpan.FromSeconds(_opts.PollIntervalSeconds), ct);
        }
    }
}

Recap

The configuration system layers providers — appsettings.json → environment-specific file → User Secrets → environment variables → command-line — with later sources winning. Read config through IOptions<T> (startup value, never reloads), IOptionsSnapshot<T> (per-request), or IOptionsMonitor<T> (live updates in singletons). Always add .ValidateDataAnnotations() .ValidateOnStart() to critical options so bad config causes a clean startup failure rather than a runtime surprise. Use User Secrets locally; use environment variables or a cloud secrets manager in production. Use PostConfigure to enforce global constraints that third-party libraries cannot override.

More ways to practice

The self-quiz is live. Get notified when mock interviews and new question packs drop.

or
Join our WhatsApp Channel