Skip to main content
Technology & EngineeringCsharp Dotnet310 lines

Configuration

.NET configuration system, options pattern, and secrets management

Quick Summary27 lines
You are an expert in the .NET configuration system for managing application settings, secrets, and strongly-typed options.

## Key Points

- **Always use the Options pattern** instead of reading `IConfiguration` directly in services. It provides compile-time safety, validation, and testability.
- **Call `ValidateOnStart()`** for all options registrations so misconfiguration is caught at application boot, not at first request.
- **Never store secrets in `appsettings.json`** — use User Secrets for development, environment variables or a vault service for production.
- **Use `IOptionsMonitor<T>`** in singletons and background services to react to configuration changes without restart.
- **Prefix environment variables** with your app name (`MYAPP_`) to avoid collisions with system or framework variables.
- **Keep options classes focused** — one class per configuration section. Do not create a god-options class with everything.
- **Injecting `IOptionsSnapshot<T>` into a singleton** — `IOptionsSnapshot` is scoped, and injecting it into a singleton captures a stale value. Use `IOptionsMonitor<T>` in singletons instead.
- **Forgetting the `__` separator for environment variables** — nested keys use double underscore (`Smtp__Host`), not colon (`Smtp:Host`), in environment variables.
- **Not setting `reloadOnChange: true`** — without it, changes to JSON files at runtime are not picked up.
- **Committing `appsettings.Development.json` with secrets** — even development secrets should use User Secrets (`dotnet user-secrets`), not checked-in JSON files.
- **Ignoring configuration binding errors silently** — by default, binding to a missing section produces default values without warning. Use validation to catch missing required fields.
- **Using `IConfiguration` in domain or service layers** — this couples your domain to the configuration infrastructure. Pass options or individual values through constructors instead.

## Quick Example

```csharp
// Not recommended for most cases, but useful for one-off reads
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
var smtpHost = builder.Configuration["Smtp:Host"];
var maxUpload = builder.Configuration.GetValue<int>("Features:MaxUploadSizeMb");
```
skilldb get csharp-dotnet-skills/ConfigurationFull skill: 310 lines
Paste into your CLAUDE.md or agent config

Configuration and Options Pattern — C#/.NET

You are an expert in the .NET configuration system for managing application settings, secrets, and strongly-typed options.

Core Philosophy

Overview

.NET's configuration system provides a layered, provider-based approach to application settings. Configuration sources (JSON files, environment variables, Azure Key Vault, etc.) are merged in order, with later sources overriding earlier ones. The Options pattern binds configuration sections to strongly-typed C# classes, with support for validation, reloading, and named options.

Core Concepts

Configuration Sources and Ordering

var builder = WebApplication.CreateBuilder(args);

// Default ordering (already configured by CreateBuilder):
// 1. appsettings.json
// 2. appsettings.{Environment}.json
// 3. User secrets (Development only)
// 4. Environment variables
// 5. Command-line arguments

// Add custom sources
builder.Configuration
    .AddJsonFile("features.json", optional: true, reloadOnChange: true)
    .AddEnvironmentVariables(prefix: "MYAPP_");

appsettings.json Structure

{
  "ConnectionStrings": {
    "DefaultConnection": "Server=localhost;Database=MyApp;Trusted_Connection=true;"
  },
  "Smtp": {
    "Host": "smtp.example.com",
    "Port": 587,
    "Username": "noreply@example.com",
    "Password": ""
  },
  "Features": {
    "EnableNewDashboard": false,
    "MaxUploadSizeMb": 25
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  }
}

Reading Configuration Directly

// Not recommended for most cases, but useful for one-off reads
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
var smtpHost = builder.Configuration["Smtp:Host"];
var maxUpload = builder.Configuration.GetValue<int>("Features:MaxUploadSizeMb");

Implementation Patterns

Options Pattern (Strongly-Typed Configuration)

public class SmtpOptions
{
    public const string SectionName = "Smtp";

    [Required]
    public string Host { get; set; } = "";

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

    [Required]
    public string Username { get; set; } = "";

    [Required]
    public string Password { get; set; } = "";

    public bool UseSsl { get; set; } = true;
}

// Registration with validation
builder.Services
    .AddOptions<SmtpOptions>()
    .BindConfiguration(SmtpOptions.SectionName)
    .ValidateDataAnnotations()
    .ValidateOnStart();  // Fails fast at startup if config is invalid

IOptions vs IOptionsSnapshot vs IOptionsMonitor

// IOptions<T> — singleton, read once at startup
public class StartupService(IOptions<SmtpOptions> options)
{
    private readonly SmtpOptions _smtp = options.Value;
}

// IOptionsSnapshot<T> — scoped, re-reads per request (when reloadOnChange is true)
public class EmailService(IOptionsSnapshot<SmtpOptions> options)
{
    public void Send()
    {
        var smtp = options.Value; // Fresh value per scope
    }
}

// IOptionsMonitor<T> — singleton, provides change notifications
public class SmtpMonitor(IOptionsMonitor<SmtpOptions> monitor)
{
    public SmtpMonitor()
    {
        monitor.OnChange(newOptions =>
        {
            Console.WriteLine($"SMTP config changed: {newOptions.Host}:{newOptions.Port}");
        });
    }
}

Custom Validation with IValidateOptions

public class SmtpOptionsValidator : IValidateOptions<SmtpOptions>
{
    public ValidateOptionsResult Validate(string? name, SmtpOptions options)
    {
        var failures = new List<string>();

        if (string.IsNullOrWhiteSpace(options.Host))
            failures.Add("SMTP host is required");

        if (options.Port is < 1 or > 65535)
            failures.Add($"Port {options.Port} is out of valid range");

        if (options.UseSsl && options.Port == 25)
            failures.Add("Port 25 does not support SSL. Use 587 or 465.");

        return failures.Count > 0
            ? ValidateOptionsResult.Fail(failures)
            : ValidateOptionsResult.Success;
    }
}

// Registration
builder.Services.AddSingleton<IValidateOptions<SmtpOptions>, SmtpOptionsValidator>();

Named Options

// appsettings.json
// {
//   "Storage": {
//     "Images": { "BasePath": "/data/images", "MaxSizeMb": 10 },
//     "Documents": { "BasePath": "/data/docs", "MaxSizeMb": 50 }
//   }
// }

public class StorageOptions
{
    public string BasePath { get; set; } = "";
    public int MaxSizeMb { get; set; }
}

builder.Services.Configure<StorageOptions>("Images",
    builder.Configuration.GetSection("Storage:Images"));
builder.Services.Configure<StorageOptions>("Documents",
    builder.Configuration.GetSection("Storage:Documents"));

// Resolve by name
public class UploadService(IOptionsSnapshot<StorageOptions> options)
{
    public void UploadImage(Stream file)
    {
        var config = options.Get("Images");
        // config.BasePath == "/data/images"
    }

    public void UploadDocument(Stream file)
    {
        var config = options.Get("Documents");
        // config.BasePath == "/data/docs"
    }
}

Secrets Management

// Development: User Secrets (stored outside the project directory)
// dotnet user-secrets init
// dotnet user-secrets set "Smtp:Password" "s3cret"

// Production: Environment variables (flat key format)
// MYAPP_Smtp__Password=s3cret
// (double underscore __ replaces the : section separator)

// Production: Azure Key Vault
builder.Configuration.AddAzureKeyVault(
    new Uri("https://myapp-vault.vault.azure.net/"),
    new DefaultAzureCredential());

// Production: AWS Secrets Manager, HashiCorp Vault, etc.
// Use the appropriate NuGet configuration provider

Environment-Specific Configuration

// appsettings.Development.json overrides appsettings.json in Development
// appsettings.Production.json overrides in Production

// Check environment in code
var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
    app.UseSwagger();
    app.UseSwaggerUI();
}

if (app.Environment.IsProduction())
{
    app.UseExceptionHandler("/error");
    app.UseHsts();
}

Configuration for Background Services

public class PollingOptions
{
    public const string SectionName = "Polling";
    public int IntervalSeconds { get; set; } = 30;
    public int MaxRetries { get; set; } = 3;
}

public class PollingService(
    IOptionsMonitor<PollingOptions> optionsMonitor,
    ILogger<PollingService> logger) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken ct)
    {
        while (!ct.IsCancellationRequested)
        {
            var options = optionsMonitor.CurrentValue; // Always fresh
            logger.LogDebug("Polling with interval {Interval}s", options.IntervalSeconds);

            try
            {
                await DoPollAsync(ct);
            }
            catch (Exception ex)
            {
                logger.LogError(ex, "Polling failed");
            }

            await Task.Delay(TimeSpan.FromSeconds(options.IntervalSeconds), ct);
        }
    }

    private Task DoPollAsync(CancellationToken ct) => Task.CompletedTask;
}

Best Practices

  • Always use the Options pattern instead of reading IConfiguration directly in services. It provides compile-time safety, validation, and testability.
  • Call ValidateOnStart() for all options registrations so misconfiguration is caught at application boot, not at first request.
  • Never store secrets in appsettings.json — use User Secrets for development, environment variables or a vault service for production.
  • Use IOptionsMonitor<T> in singletons and background services to react to configuration changes without restart.
  • Prefix environment variables with your app name (MYAPP_) to avoid collisions with system or framework variables.
  • Keep options classes focused — one class per configuration section. Do not create a god-options class with everything.

Common Pitfalls

  • Injecting IOptionsSnapshot<T> into a singletonIOptionsSnapshot is scoped, and injecting it into a singleton captures a stale value. Use IOptionsMonitor<T> in singletons instead.
  • Forgetting the __ separator for environment variables — nested keys use double underscore (Smtp__Host), not colon (Smtp:Host), in environment variables.
  • Not setting reloadOnChange: true — without it, changes to JSON files at runtime are not picked up.
  • Committing appsettings.Development.json with secrets — even development secrets should use User Secrets (dotnet user-secrets), not checked-in JSON files.
  • Ignoring configuration binding errors silently — by default, binding to a missing section produces default values without warning. Use validation to catch missing required fields.
  • Using IConfiguration in domain or service layers — this couples your domain to the configuration infrastructure. Pass options or individual values through constructors instead.

Anti-Patterns

Over-engineering for hypothetical scale. Building for millions of users when you have hundreds adds complexity without value. Solve today's problems first.

Ignoring the existing ecosystem. Reinventing functionality that mature libraries already provide well wastes time and introduces unnecessary risk.

Premature abstraction. Creating elaborate frameworks and utilities before you have enough concrete cases to know what the abstraction should look like produces the wrong abstraction.

Neglecting error handling at boundaries. Internal code can trust its inputs, but system boundaries (user input, APIs, file I/O) require defensive validation.

Skipping documentation for obvious code. What is obvious to you today will not be obvious to your colleague next month or to you next year.

Install this skill directly: skilldb add csharp-dotnet-skills

Get CLI access →