Skip to content

Configuration Interview Questions & Answers

15 questions Updated 2026-06-23 Share:

ASP.NET Core configuration interview questions — IConfiguration, IOptions, IOptionsMonitor, environment variables, user secrets, and startup validation.

Read the in-depth guideASP.NET Core Configuration in Depth(opens in new tab)
15 of 15

The ASP.NET Core configuration system aggregates key-value pairs from multiple configuration providers into a unified IConfiguration interface. Providers are layered — later providers override earlier ones for the same key.

// Default provider order (WebApplication.CreateBuilder):
// 1. appsettings.json
// 2. appsettings.{Environment}.json (e.g., appsettings.Production.json)
// 3. User Secrets (Development only)
// 4. Environment variables
// 5. Command-line arguments

var builder = WebApplication.CreateBuilder(args);
// builder.Configuration is already populated from the sources above

// Read a value:
string? conn = builder.Configuration["ConnectionStrings:DefaultConnection"];
// or via GetConnectionString shortcut:
string? conn2 = builder.Configuration.GetConnectionString("DefaultConnection");

// Read a section:
IConfigurationSection mailSection = builder.Configuration.GetSection("Mail");
string? host = mailSection["Host"];
int port = mailSection.GetValue<int>("Port", defaultValue: 587);

appsettings.json:

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

Rule of thumb: Never hard-code secrets or environment-specific values in code. Use the configuration system so values can be overridden per environment through appsettings files, environment variables, or secrets management tools.

ASP.NET Core supports many built-in providers. WebApplication.CreateBuilder sets up the default stack automatically; you can customize with ConfigureAppConfiguration.

// Default stack — later entries WIN (override earlier):
// appsettings.json → appsettings.{Env}.json → User Secrets → Env vars → CLI args

// Customizing the stack:
builder.Configuration
    // 1. Clear existing providers:
    .Sources.Clear();

builder.Host.ConfigureAppConfiguration((ctx, config) =>
{
    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>(); // safe dev-only secrets

    config.AddEnvironmentVariables(prefix: "MYAPP_"); // MYAPP_Mail__Host → Mail:Host

    config.AddCommandLine(args); // --Mail:Host=smtp.example.com

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

    // Custom: AWS Secrets Manager, HashiCorp Vault, etc.
});

Environment variable key mapping:

  • Double underscore __ maps to : (section separator).
  • MYAPP_Mail__HostMail:Host
  • MYAPP_ConnectionStrings__DefaultConnectionConnectionStrings:DefaultConnection

Rule of thumb: For secrets (API keys, connection strings), use User Secrets in development and environment variables / a secrets manager (Azure Key Vault, AWS Secrets Manager) in production — never commit secrets to appsettings.json.

The Options pattern binds a configuration section to a strongly-typed class and injects it via DI. Three interfaces serve different lifetime / reload needs:

  • IOptions<T> — singleton; reads config once at startup; does not see changes.
  • IOptionsSnapshot<T> — scoped; re-reads config once per request; sees reloaded values from the next request onward.
  • IOptionsMonitor<T> — singleton; sees config changes immediately via a change-notification callback.
// POCO:
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;
}

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

// Inject and use:
public class EmailService
{
    private readonly MailOptions _opts;

    // IOptions<T> — fixed at startup (singleton):
    public EmailService(IOptions<MailOptions> opts) =>
        _opts = opts.Value;

    // IOptionsSnapshot<T> — re-read per request (scoped service):
    public EmailService(IOptionsSnapshot<MailOptions> opts) =>
        _opts = opts.Value;

    // IOptionsMonitor<T> — live updates (singleton service):
    public EmailService(IOptionsMonitor<MailOptions> monitor)
    {
        _opts = monitor.CurrentValue;
        monitor.OnChange(newValue =>
        {
            // Called whenever config changes on disk
            Console.WriteLine($"Config changed: Host={newValue.Host}");
        });
    }
}
Interface Lifetime Sees reloads? Best for
IOptions<T> Singleton No Startup-only config, singletons
IOptionsSnapshot<T> Scoped Per-request Web requests, feature flags
IOptionsMonitor<T> Singleton Immediately Long-running services, background workers

Rule of thumb: Use IOptions<T> for configuration that never changes at runtime. Use IOptionsSnapshot<T> in request-scoped services when you want per-request config refresh. Use IOptionsMonitor<T> in singletons or background services that need live config updates without restart.

ASP.NET Core lets you validate Options at startup using Data Annotations, IValidateOptions<T>, or the fluent Validate overload.

// Data Annotations on the POCO:
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!;
}

// Register with annotation validation + eager validation at startup:
builder.Services
    .AddOptions<SmtpOptions>()
    .BindConfiguration("Smtp")
    .ValidateDataAnnotations()
    .ValidateOnStart(); // throws on startup instead of first use!

// Custom validation:
builder.Services
    .AddOptions<SmtpOptions>()
    .BindConfiguration("Smtp")
    .Validate(opts =>
    {
        if (opts.Port == 25 && opts.Host.EndsWith(".example.com"))
            return false; // invalid combination
        return true;
    }, "Port 25 is not allowed for example.com SMTP servers")
    .ValidateOnStart();

Without ValidateOnStart(), validation runs on first access of .Value. With it, bad config causes the app to refuse to start — a much better failure mode.

Rule of thumb: Always add .ValidateDataAnnotations().ValidateOnStart() to any critical options binding. Failing fast at startup is far better than a production runtime error when a feature is first exercised.

User Secrets store sensitive config values (API keys, connection strings) outside the project directory during development, so they are never committed to source control. They are stored in %APPDATA%\Microsoft\UserSecrets\<id>\secrets.json (Windows) or ~/.microsoft/usersecrets/<id>/secrets.json (Linux/macOS).

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

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

# List all secrets:
dotnet user-secrets list

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

They are automatically loaded in the Development environment by WebApplication.CreateBuilder:

// Automatically added in Development:
if (app.Environment.IsDevelopment())
    builder.Configuration.AddUserSecrets<Program>();

// WebApplication.CreateBuilder does this by default — you usually don't need to call it manually.

// Access normally via IConfiguration:
var key = builder.Configuration["Stripe:SecretKey"];

In production, use environment variables or a secrets manager — User Secrets are a development-only mechanism.

Rule of thumb: Use User Secrets for any credential a dev needs locally but must not check in. Never add secrets.json to .gitignore — it's outside the repo directory by design.

The hosting environment (ASPNETCORE_ENVIRONMENT env var) controls which appsettings.{Environment}.json file is loaded. The environment-specific file merges with and overrides values from appsettings.json.

// appsettings.json (base — checked into source control):
{
  "Logging": { "LogLevel": { "Default": "Warning" } },
  "FeatureFlags": { "NewCheckout": false },
  "ConnectionStrings": { "DefaultConnection": "" }
}

// appsettings.Development.json (local overrides — can be committed for shared dev):
{
  "Logging": { "LogLevel": { "Default": "Debug" } },
  "FeatureFlags": { "NewCheckout": true }
}

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

// Custom environments:
// ASPNETCORE_ENVIRONMENT=Staging → loads appsettings.Staging.json
if (app.Environment.IsEnvironment("Staging"))
    EnableStagingFeatures();

IWebHostEnvironment (injected via DI) exposes:

  • EnvironmentName — the string value
  • IsDevelopment(), IsStaging(), IsProduction() — convenience methods
  • ContentRootPath, WebRootPath

Rule of thumb: Keep appsettings.json free of secrets and environment-specific values. Use appsettings.{Env}.json for non-secret environment differences. Put secrets in environment variables or a secrets manager, not in any config file that gets committed.

IConfiguration provides several methods to read values: GetValue<T>, GetSection, Bind, and Get<T>.

// appsettings.json:
// {
// "App": { "Name": "MyApp", "MaxItems": 100, "Tags": ["api","web"] }
// }

// GetValue<T> — read a single value with optional default:
string name     = config.GetValue<string>("App:Name") ?? "Default";
int maxItems    = config.GetValue<int>("App:MaxItems", defaultValue: 50);

// GetSection — get a sub-section (does NOT throw if missing):
IConfigurationSection appSection = config.GetSection("App");
string? sectionName = appSection["Name"]; // "MyApp"

// Bind — populate an existing object from config:
var opts = new AppOptions();
config.GetSection("App").Bind(opts);
// opts.Name == "MyApp", opts.MaxItems == 100

// Get<T> — returns a new instance of T bound from the section:
AppOptions opts2 = config.GetSection("App").Get<AppOptions>()!;

// Bind arrays:
string[] tags = config.GetSection("App:Tags").Get<string[]>()!;
// tags == ["api", "web"]

public class AppOptions
{
    public string Name { get; set; } = default!;
    public int MaxItems { get; set; }
    public List<string> Tags { get; set; } = new();
}

GetSection("missing") returns an empty IConfigurationSection (not null) — check section.Exists() before using it:

var section = config.GetSection("FeatureFlags");
if (section.Exists())
    section.Bind(featureFlags);

Rule of thumb: Prefer the Options pattern (IOptions<T>) over directly calling IConfiguration in application services — it gives you DI, validation, and reload support. Use IConfiguration directly only in startup code or simple console apps.

Yes — by default, AddJsonFile is registered with reloadOnChange: true, so appsettings.json and appsettings.{Env}.json are re-read when the file changes on disk (via FileSystemWatcher). IConfiguration and IOptionsMonitor<T> reflect the new values immediately; IOptionsSnapshot<T> reflects them on the next request; IOptions<T> never reloads.

// Reload is on by default — this is what WebApplication.CreateBuilder does:
config.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
config.AddJsonFile($"appsettings.{env}.json", optional: true, reloadOnChange: true);

// IOptionsMonitor: subscribes to live changes
public class FeatureFlagService
{
    private readonly IOptionsMonitor<FeatureOptions> _monitor;
    public FeatureFlagService(IOptionsMonitor<FeatureOptions> monitor)
        => _monitor = monitor;

    public bool IsEnabled(string flag)
        => _monitor.CurrentValue.EnabledFlags.Contains(flag); // always fresh
}

// IOptionsSnapshot: fresh per-request (scoped services)
public class RequestHandler
{
    private readonly FeatureOptions _opts;
    public RequestHandler(IOptionsSnapshot<FeatureOptions> opts)
        => _opts = opts.Value; // bound once per request scope
}

// IOptions: frozen at startup (no reload)
public class CachedService
{
    private readonly FeatureOptions _opts;
    public CachedService(IOptions<FeatureOptions> opts)
        => _opts = opts.Value; // static — never changes
}

Rule of thumb: Use IOptionsMonitor<T> in singletons or background workers that need live config without a restart. In production, use reload carefully for critical values — an invalid config file mid-flight can crash the app or produce unexpected behavior.

Named options allow multiple instances of the same options class, each configured differently. They are useful when you have multiple instances of a service with different configuration (e.g., two SMTP servers, multiple HTTP clients).

// Two SMTP configurations in appsettings.json:
// {
// "Smtp": {
// "Transactional": { "Host": "smtp1.example.com", "Port": 587 },
// "Marketing":     { "Host": "smtp2.example.com", "Port": 465 }
// }
// }

// Register named options:
builder.Services.Configure<SmtpOptions>("Transactional",
    builder.Configuration.GetSection("Smtp:Transactional"));
builder.Services.Configure<SmtpOptions>("Marketing",
    builder.Configuration.GetSection("Smtp:Marketing"));

// Inject and resolve by name:
public class EmailDispatcher
{
    private readonly SmtpOptions _transactional;
    private readonly SmtpOptions _marketing;

    public EmailDispatcher(IOptionsMonitor<SmtpOptions> monitor)
    {
        _transactional = monitor.Get("Transactional");
        _marketing     = monitor.Get("Marketing");
    }

    public void SendTransactional(string to, string body)
        => Send(_transactional, to, body);

    public void SendMarketing(string to, string body)
        => Send(_marketing, to, body);
}

IHttpClientFactory uses named options internally:

builder.Services.AddHttpClient("github", client =>
{
    client.BaseAddress = new Uri("https://api.github.com/");
    client.DefaultRequestHeaders.Add("User-Agent", "MyApp");
});

// Inject named client:
var client = httpClientFactory.CreateClient("github");

Rule of thumb: Use named options when you need multiple configurations for the same options type — commonly for HTTP clients, SMTP servers, external service endpoints, or feature flag namespaces.

Configure<T> sets up options values. PostConfigure<T> runs after all Configure registrations for the same type — it is the last step and can override or validate values set by earlier Configure calls or by third-party libraries.

// Scenario: override a value set by an external library's Configure call

// External library registers its defaults:
builder.Services.Configure<CacheOptions>(opts =>
{
    opts.MaxSize = 100;
    opts.Expiry  = TimeSpan.FromMinutes(10);
});

// Your code runs AFTER — PostConfigure:
builder.Services.PostConfigure<CacheOptions>(opts =>
{
    // Runs after ALL Configure calls for CacheOptions
    if (opts.MaxSize > 500)
        opts.MaxSize = 500; // enforce a cap regardless of what lib set
});

// PostConfigureAll — applies to ALL named instances:
builder.Services.PostConfigureAll<SmtpOptions>(opts =>
    opts.UseSsl = true); // enforce SSL on every named SMTP config

Execution order for options pipeline:

  1. All Configure<T> calls (in registration order)
  2. All PostConfigure<T> calls (in registration order)
  3. IValidateOptions<T> validators
// Confirm ordering with a simple example:
builder.Services.Configure<MyOpts>(o => o.Value = "first");
builder.Services.Configure<MyOpts>(o => o.Value = "second");
builder.Services.PostConfigure<MyOpts>(o => o.Value = "post"); // wins
// Result: opts.Value == "post"

Rule of thumb: Use PostConfigure when you need to enforce global constraints on options that third-party libraries configure, or when you need a guaranteed "last word" on a value regardless of other configuration sources.

The configuration system is part of Microsoft.Extensions.Configuration and works in any .NET app — not just ASP.NET Core. Use Host.CreateDefaultBuilder or the newer Host.CreateApplicationBuilder to get the same provider stack.

// Worker service / console app:
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

var host = Host.CreateDefaultBuilder(args)
    .ConfigureAppConfiguration((ctx, config) =>
    {
        // Same providers as WebApplication.CreateBuilder:
        config.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
        config.AddJsonFile($"appsettings.{ctx.HostingEnvironment.EnvironmentName}.json",
            optional: true);
        config.AddEnvironmentVariables();
        if (ctx.HostingEnvironment.IsDevelopment())
            config.AddUserSecrets<Program>();
    })
    .ConfigureServices((ctx, services) =>
    {
        services.Configure<WorkerOptions>(ctx.Configuration.GetSection("Worker"));
        services.AddHostedService<MyWorker>();
    })
    .Build();

await host.RunAsync();

// Background service receives options via DI:
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(_opts.IntervalSeconds);
            await Task.Delay(TimeSpan.FromSeconds(_opts.IntervalSeconds), ct);
        }
    }
}

Rule of thumb: Use Host.CreateDefaultBuilder or Host.CreateApplicationBuilder in console apps and background services to get the same configuration, logging, and DI infrastructure as ASP.NET Core — avoid reinventing it with manual provider setup.

IConfigureOptions<T> is an interface that lets you encapsulate options configuration logic in a dedicated class, which is itself resolved from DI. This is useful when the configuration logic requires services (e.g., reading from a database or calling an API) that are not available during the Configure delegate.

// Plain Configure<T> — closure-based, no DI services available:
builder.Services.Configure<JwtOptions>(opts =>
{
    opts.Secret = builder.Configuration["Jwt:Secret"]!;
    // Cannot inject ISomeService here — DI is not yet built
});

// IConfigureOptions<T> — runs after DI is built; can inject services:
public class JwtOptionsConfigurator : IConfigureOptions<JwtOptions>
{
    private readonly IConfiguration _config;
    private readonly ISecretVaultService _vault;

    // Dependencies resolved from the built DI container:
    public JwtOptionsConfigurator(IConfiguration config, ISecretVaultService vault)
    {
        _config = config;
        _vault  = vault;
    }

    public void Configure(JwtOptions options)
    {
        options.Issuer   = _config["Jwt:Issuer"]!;
        options.Audience = _config["Jwt:Audience"]!;
        // Fetch secret from vault — possible because this runs post-DI-build:
        options.Secret   = _vault.GetSecret("jwt-signing-key");
    }
}

// Register the configurator as a DI service:
builder.Services.AddSingleton<IConfigureOptions<JwtOptions>, JwtOptionsConfigurator>();

// IPostConfigureOptions<T> — same pattern but runs after all Configure calls:
public class JwtPostConfigurator : IPostConfigureOptions<JwtOptions>
{
    public void PostConfigure(string? name, JwtOptions options)
    {
        if (string.IsNullOrEmpty(options.Secret))
            throw new InvalidOperationException("JWT secret must be configured");
    }
}
builder.Services.AddSingleton<IPostConfigureOptions<JwtOptions>, JwtPostConfigurator>();

Rule of thumb: Use services.Configure<T> for simple config binding from IConfiguration. Use IConfigureOptions<T> when configuration requires injected services (vault clients, DB lookups, computed values) — it runs after the container is fully built and gives you access to the complete DI graph.

Azure Key Vault is added as a configuration provider using Azure.Extensions.AspNetCore.Configuration.Secrets. Secrets are loaded alongside appsettings.json and accessible through the standard IConfiguration interface.

// Package: Azure.Extensions.AspNetCore.Configuration.Secrets
//          Azure.Identity

using Azure.Identity;

var builder = WebApplication.CreateBuilder(args);

// Add Key Vault as a configuration source (production pattern):
if (!builder.Environment.IsDevelopment())
{
    var keyVaultUri = new Uri(builder.Configuration["KeyVault:Uri"]!);

    // DefaultAzureCredential: tries Managed Identity, then az login, etc.
    builder.Configuration.AddAzureKeyVault(
        keyVaultUri,
        new DefaultAzureCredential());
}

// Secrets are available via IConfiguration using -- as the key separator:
// Key Vault secret "ConnectionStrings--DefaultConnection"
//   → accessible as Configuration["ConnectionStrings:DefaultConnection"]

// Access via Options pattern (preferred):
builder.Services.Configure<DbOptions>(builder.Configuration.GetSection("ConnectionStrings"));

// Key Vault secret naming conventions:
// "Mail--Host"     → Mail:Host
// "Jwt--Secret"    → Jwt:Secret
// Named version:   "Mail--Smtp1--Host" → Mail:Smtp1:Host

// Prefix filtering — only load secrets starting with "MyApp-":
builder.Configuration.AddAzureKeyVault(
    keyVaultUri,
    new DefaultAzureCredential(),
    new AzureKeyVaultConfigurationOptions
    {
        Manager = new KeyVaultSecretManager() // custom: override to filter/rename keys
    });

Key Vault secrets are fetched once at startup (no reload by default). Enable polling:

new AzureKeyVaultConfigurationOptions
{
    ReloadInterval = TimeSpan.FromMinutes(30) // re-fetch secrets every 30 min
}

Rule of thumb: In production, replace User Secrets and env-var secrets with Azure Key Vault. Use DefaultAzureCredential so the same code works with Managed Identity in Azure and with az login locally. Never store Key Vault URIs or credentials in appsettings.json.

When using WebApplicationFactory<T> for integration tests, you can override configuration via ConfigureAppConfiguration in a custom factory, or by setting environment variables before the test host starts.

using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.Configuration;

// Custom factory that overrides configuration for tests:
public class TestWebApplicationFactory : WebApplicationFactory<Program>
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureAppConfiguration((ctx, config) =>
        {
            // Add in-memory config — overrides appsettings.json (added later = wins):
            config.AddInMemoryCollection(new Dictionary<string, string?>
            {
                ["ConnectionStrings:DefaultConnection"] = "DataSource=:memory:",
                ["Mail:Host"]     = "localhost",
                ["Mail:Port"]     = "1025",
                ["FeatureFlags:NewCheckout"] = "true",
                ["Jwt:Secret"]    = "test-only-signing-key-minimum-32-chars!!"
            });
        });

        // Also swap out services for test doubles:
        builder.ConfigureServices(services =>
        {
            // Replace real DB with in-memory:
            services.RemoveAll<DbContextOptions<AppDb>>();
            services.AddDbContext<AppDb>(o => o.UseInMemoryDatabase("TestDb"));
        });
    }
}

// Test class:
public class OrdersApiTests : IClassFixture<TestWebApplicationFactory>
{
    private readonly HttpClient _client;

    public OrdersApiTests(TestWebApplicationFactory factory)
        => _client = factory.CreateClient();

    [Fact]
    public async Task GetOrders_ReturnsOk()
    {
        var response = await _client.GetAsync("/api/orders");
        response.EnsureSuccessStatusCode();
    }
}

Rule of thumb: Use AddInMemoryCollection inside ConfigureAppConfiguration to inject test-specific values — it is the last provider added and therefore wins. Keep test config minimal: only override values that differ from defaults or would cause the test to contact real external services.

IChangeToken is the low-level primitive that the configuration system uses to signal when a source has changed. IConfiguration.GetReloadToken() returns a change token that fires once when any provider triggers a reload. You can use it to react to config changes in code that does not have access to IOptionsMonitor<T>.

// Low-level change-token subscription:
void WatchForChanges(IConfiguration config)
{
    // GetReloadToken returns a one-shot token — must re-register after each fire:
    ChangeToken.OnChange(
        changeTokenProducer: () => config.GetReloadToken(),
        changeTokenConsumer: () =>
        {
            Console.WriteLine("Configuration reloaded at " + DateTime.UtcNow);
            // Re-read any cached config values here
        });
}

// IOptionsMonitor<T> uses the same mechanism internally — prefer it for options:
public class FeatureFlagCache
{
    private HashSet<string> _enabledFlags;

    public FeatureFlagCache(IOptionsMonitor<FeatureOptions> monitor)
    {
        _enabledFlags = BuildCache(monitor.CurrentValue);

        // OnChange callback fires whenever config changes on disk:
        monitor.OnChange(opts =>
        {
            _enabledFlags = BuildCache(opts); // rebuild the local cache
            Console.WriteLine("Feature flags reloaded");
        });
    }

    private static HashSet<string> BuildCache(FeatureOptions opts)
        => opts.EnabledFlags.ToHashSet(StringComparer.OrdinalIgnoreCase);

    public bool IsEnabled(string flag) => _enabledFlags.Contains(flag);
}

// Custom IChangeToken implementation (e.g., for a DB-backed config provider):
public class DbChangeToken : IChangeToken
{
    private readonly CancellationToken _token;
    public DbChangeToken(CancellationToken token) => _token = token;

    public bool HasChanged => _token.IsCancellationRequested;
    public bool ActiveChangeCallbacks => true;

    public IDisposable RegisterChangeCallback(Action<object?> callback, object? state)
        => _token.Register(callback, state);
}

Rule of thumb: Use IOptionsMonitor<T>.OnChange for most config-reload scenarios — it is the safe, high-level API. Drop down to IChangeToken directly only when building a custom configuration provider or when you need to watch for changes outside the Options pattern.

More ways to practice

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

or
Join our WhatsApp Channel