Configuration and Options Pattern .NET Core

 

Configuration and Options Pattern in .NET Core

Quick Reference

  1. Configuration Providers
  2. Strongly-Typed Configuration (IOptions)
  3. Configuration Validation
  4. Environment-Specific Configurations
  5. Interview Q&A

Configuration Fundamentals

IConfiguration: Weakly-typed access to settings
IConfigurationBuilder: Builds configuration from multiple sources
IOptions<T>: Strongly-typed, validated configuration


Configuration Providers

1. JSON Configuration

appsettings.json

{
  "ConnectionStrings": {
    "DefaultConnection": "Server=localhost;Database=MyDb;User Id=sa;Password=Pass123"
  },
  "AppSettings": {
    "JwtSecret": "secret-key-min-32-chars",
    "ApiTimeout": 30
  }
}

Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    var connString = Configuration.GetConnectionString("DefaultConnection");
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(connString));
}

Hierarchical access with : separator:

// appsettings.json: "Database:Connection:Server"
var server = configuration["Database:Connection:Server"];

Multiple JSON files with priority:

var config = new ConfigurationBuilder()
    .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
    .AddJsonFile($"appsettings.{environment}.json", optional: true, reloadOnChange: true)
    .Build();

2. Environment Variables

// Windows CMD
setx DATABASE_CONNECTION "Server=localhost;Database=MyDb"

// PowerShell
$env:DATABASE_CONNECTION = "Server=localhost;Database=MyDb"

// Linux/macOS
export DATABASE_CONNECTION="Server=localhost;Database=MyDb"

Loading environment variables:

var config = new ConfigurationBuilder()
    .AddJsonFile("appsettings.json")
    .AddEnvironmentVariables()  // Double underscore __ maps to : nesting
    .Build();

// Environment variable: Database__Connection__Server
// Accessed via: configuration["Database:Connection:Server"]

3. Azure Key Vault

dotnet add package Azure.Identity
dotnet add package Azure.Extensions.AspNetCore.Configuration.Secrets

Program.cs

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureAppConfiguration((context, config) =>
        {
            config.AddJsonFile("appsettings.json");
            config.AddEnvironmentVariables();

            var builtConfig = config.Build();
            var keyVaultUrl = new Uri(builtConfig["KeyVault:Url"]);

            config.AddAzureKeyVault(
                keyVaultUrl,
                new DefaultAzureCredential()); // Uses managed identity
        })
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        });

appsettings.json

{
  "KeyVault": {
    "Url": "https://mykeyvault.vault.azure.net/"
  }
}

Strongly-Typed Configuration (IOptions)

Configuration Class

public class AppSettings
{
    public string JwtSecret { get; set; }
    public int ApiTimeout { get; set; }
    public DatabaseSettings Database { get; set; }
}

public class DatabaseSettings
{
    public string ConnectionString { get; set; }
    public int CommandTimeout { get; set; }
}

Register in DI Container

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<AppSettings>(Configuration.GetSection("AppSettings"));
    services.AddControllers();
}

Usage

public class UserService
{
    private readonly IOptions<AppSettings> _options;

    public UserService(IOptions<AppSettings> options)
    {
        _options = options;
    }

    public void ProcessUser()
    {
        var jwt = _options.Value.JwtSecret;
        var timeout = _options.Value.ApiTimeout;
        Console.WriteLine($"JWT: {jwt}, Timeout: {timeout}");
    }
}

IOptions Variants

TypeLifetimeReloadUse Case
IOptions<T>SingletonNeverStatic configuration
IOptionsMonitor<T>SingletonReal-time with notificationsDynamic updates with listening
IOptionsSnapshot<T>ScopedPer-requestWeb API (reloads per scope)
// IOptionsMonitor - Real-time with change detection
public class DynamicService
{
    private readonly IOptionsMonitor<AppSettings> _monitor;

    public DynamicService(IOptionsMonitor<AppSettings> monitor)
    {
        _monitor = monitor;
        // Listen to changes
        monitor.OnChange((opts) => Console.WriteLine("Config changed!"));
    }

    public void DoWork()
    {
        var timeout = _monitor.CurrentValue.ApiTimeout; // Always latest
    }
}

// IOptionsSnapshot - Per-request reload
public class PerRequestService
{
    private readonly IOptionsSnapshot<AppSettings> _snapshot;

    public PerRequestService(IOptionsSnapshot<AppSettings> snapshot)
    {
        _snapshot = snapshot;
    }

    public void ProcessRequest()
    {
        var settings = _snapshot.Value; // Fresh per request
    }
}

Configuration Validation

Data Annotations

public class AppSettings
{
    [Required(ErrorMessage = "JWT Secret is required")]
    [StringLength(256, MinimumLength = 32)]
    public string JwtSecret { get; set; }

    [Range(1, 300, ErrorMessage = "Timeout must be between 1 and 300 seconds")]
    public int ApiTimeout { get; set; }
}

// Register with validation
public void ConfigureServices(IServiceCollection services)
{
    services.Configure<AppSettings>(Configuration.GetSection("AppSettings"))
        .ValidateDataAnnotations();
}

Custom Validation

public class AppSettingsValidator : IValidateOptions<AppSettings>
{
    public ValidateOptionsResult Validate(string name, AppSettings options)
    {
        if (string.IsNullOrWhiteSpace(options.JwtSecret))
            return ValidateOptionsResult.Fail("JWT Secret cannot be empty");

        if (options.JwtSecret.Length < 32)
            return ValidateOptionsResult.Fail("JWT Secret must be at least 32 characters");

        if (options.ApiTimeout <= 0)
            return ValidateOptionsResult.Fail("API Timeout must be greater than 0");

        return ValidateOptionsResult.Success;
    }
}

// Register
services.Configure<AppSettings>(Configuration.GetSection("AppSettings"));
services.AddSingleton<IValidateOptions<AppSettings>, AppSettingsValidator>();

FluentValidation

dotnet add package FluentValidation
public class AppSettingsValidator : AbstractValidator<AppSettings>
{
    public AppSettingsValidator()
    {
        RuleFor(x => x.JwtSecret)
            .NotEmpty().WithMessage("JWT Secret is required")
            .MinimumLength(32).WithMessage("Must be at least 32 characters");

        RuleFor(x => x.ApiTimeout)
            .GreaterThan(0).WithMessage("Must be greater than 0")
            .LessThanOrEqualTo(300);
    }
}

// Register
services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());
services.Configure<AppSettings>(Configuration.GetSection("AppSettings"));

Environment-Specific Configurations

File Structure

appsettings.json                 (base/default)
appsettings.Development.json     (dev - loose settings)
appsettings.Staging.json         (staging - balanced)
appsettings.Production.json      (prod - strict security)

appsettings.json

{
  "Logging": { "LogLevel": { "Default": "Information" } },
  "ConnectionStrings": { "DefaultConnection": "Server=localhost;Database=MyDb" },
  "AppSettings": { "JwtSecret": "default-secret", "ApiTimeout": 30 }
}

appsettings.Development.json

{
  "Logging": { "LogLevel": { "Default": "Debug" } },
  "AppSettings": { "AllowSwagger": true, "EnableDetailedErrors": true }
}

appsettings.Production.json

{
  "Logging": { "LogLevel": { "Default": "Warning" } },
  "AppSettings": { "AllowSwagger": false, "EnableDetailedErrors": false, "RequireHttps": true }
}

Loading Configuration by Environment

public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureAppConfiguration((hostContext, config) =>
            {
                var env = hostContext.HostingEnvironment;
                
                config
                    .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
                    .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true)
                    .AddEnvironmentVariables();

                // Production: Load secrets from Key Vault
                if (env.IsProduction())
                {
                    var builtConfig = config.Build();
                    config.AddAzureKeyVault(
                        new Uri(builtConfig["KeyVault:Url"]),
                        new DefaultAzureCredential());
                }
            })
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });
}

Setting Environment

REM Windows
set ASPNETCORE_ENVIRONMENT=Development
dotnet run
# Linux/macOS
export ASPNETCORE_ENVIRONMENT=Production
dotnet run

launchSettings.json

{
  "profiles": {
    "WebAPI": {
      "commandName": "Project",
      "applicationUrl": "https://localhost:5001",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}

Using Environment in Code

public class Startup
{
    private readonly IWebHostEnvironment _env;

    public Startup(IWebHostEnvironment env)
    {
        _env = env;
    }

    public void ConfigureServices(IServiceCollection services)
    {
        if (_env.IsDevelopment())
        {
            services.AddSwaggerGen();
            services.AddCors(o => o.AddPolicy("dev", b => b.AllowAnyOrigin().AllowAnyMethod()));
        }
        else if (_env.IsProduction())
        {
            services.AddHsts(options => options.MaxAge = TimeSpan.FromDays(365));
        }

        services.AddControllers();
    }

    public void Configure(IApplicationBuilder app)
    {
        if (_env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
            app.UseSwagger();
        }
        else
        {
            app.UseExceptionHandler("/Error");
            app.UseHsts();
        }

        app.UseRouting();
        app.UseEndpoints(endpoints => endpoints.MapControllers());
    }
}

Configuration Priority (Last Wins)

// Load order (later overrides earlier):
// 1. appsettings.json
// 2. appsettings.{Environment}.json
// 3. Environment Variables
// 4. Azure Key Vault (production)

config
    .AddJsonFile("appsettings.json")
    .AddJsonFile($"appsettings.{env}.json", optional: true)
    .AddEnvironmentVariables()
    .AddAzureKeyVault(url, credential);

Interview Q&A

Q1: IConfiguration vs IOptions - Key Differences

IConfiguration - Weakly typed, direct access, supports reload detection
IOptions - Strongly typed, validated, intellisense support, three variants

// IConfiguration - String return, manual conversion
var timeout = int.Parse(configuration["ApiTimeout"]);

// IOptions - Strongly typed, intellisense
var timeout = options.Value.ApiTimeout; // int directly

Q2: IOptions, IOptionsMonitor, IOptionsSnapshot - When to Use?

TypeLifetimeReloadUse Case
IOptionsSingletonNeverStatic config, injected once and cached
IOptionsMonitorSingletonReal-timeDynamic updates, listen to changes with OnChange()
IOptionsSnapshotScopedPer-requestWeb requests, fresh value per scope

Q3: How to Secure Sensitive Configuration?

  1. Never hardcode secrets in source
  2. Use Azure Key Vault for production secrets
  3. Use User Secrets for development (local machine)
  4. Use environment variables as fallback
  5. Validate required secrets on startup
// Never do this
var secret = "hardcoded-secret"; // ❌

// Use Key Vault
config.AddAzureKeyVault(new Uri(keyVaultUrl), new DefaultAzureCredential()); // ✅

// Use User Secrets (development only)
// Command: dotnet user-secrets set "Jwt:Secret" "my-secret"

Q4: Configuration Reload Explained

IOptionsMonitor supports real-time reloading without app restart:

var monitor = new IOptionsMonitor<AppSettings>;
monitor.OnChange((options) => 
{
    // Triggered when appsettings.json changes
    Console.WriteLine($"Config updated: {options.ApiTimeout}");
});

var timeout = monitor.CurrentValue.ApiTimeout; // Always latest value

vs IOptions:

// IOptions cached at startup, never updates
var options = new IOptions<AppSettings>;
var timeout = options.Value.ApiTimeout; // Same value forever

Q5: Validate Configuration On Startup

// Fail-fast if required settings missing
services.Configure<AppSettings>(Configuration.GetSection("AppSettings"))
    .ValidateDataAnnotations() // Data Annotations validation
    .Validate(s => s.ApiTimeout > 0, "ApiTimeout must be > 0"); // Custom rule

// Custom validator
services.AddSingleton<IValidateOptions<AppSettings>, AppSettingsValidator>();

Q6: Configuration Provider Priority/Layering

Later overrides earlier:

config
    .AddJsonFile("appsettings.json")              // 1st (lowest priority)
    .AddJsonFile("appsettings.Production.json")   // 2nd
    .AddEnvironmentVariables()                    // 3rd
    .AddAzureKeyVault(url, credential);           // 4th (highest)

// Result: If same key exists in all, Key Vault value wins

Q7: Bind Nested Configuration Objects

// appsettings.json
{
  "Database": {
    "Connection": {
      "Server": "localhost",
      "Port": 1433
    }
  }
}

// Classes matching structure
public class AppSettings
{
    public DatabaseSettings Database { get; set; }
}

public class DatabaseSettings
{
    public ConnectionSettings Connection { get; set; }
}

public class ConnectionSettings
{
    public string Server { get; set; }
    public int Port { get; set; }
}

// Bind in Startup
services.Configure<AppSettings>(Configuration);
// Access: options.Value.Database.Connection.Server

Q8: Environment Variable Naming with Nesting

Double underscores __ map to : nesting in configuration:

// Environment variable: Database__Connection__Server
// Maps to: configuration["Database:Connection:Server"]

.AddEnvironmentVariables(); // Automatically handles __ mapping

// Example in Docker:
// environment:
//   - Database__Connection__Server=prod-server
//   - Database__Connection__Port=1433

Q9: Docker Configuration Best Practices

version: '3.8'
services:
  api:
    image: myapi
    environment:
      - ASPNETCORE_ENVIRONMENT=Production
      - ConnectionStrings__DefaultConnection=Server=db;Database=MyDb
      - AppSettings__JwtSecret=secure-key-here
      - AppSettings__ApiTimeout=45
    secrets:
      - jwt_secret

secrets:
  jwt_secret:
    file: ./secrets/jwt_secret.txt

Q10: Missing Required Configuration - Handle Gracefully

private void ValidateRequiredConfiguration()
{
    var required = new[] 
    { 
        "ConnectionStrings:DefaultConnection",
        "AppSettings:JwtSecret" 
    };

    var missing = required
        .Where(s => string.IsNullOrWhiteSpace(Configuration[s]))
        .ToList();

    if (missing.Any())
    {
        if (_env.IsProduction())
            throw new InvalidOperationException($"Missing: {string.Join(",", missing)}");
        
        // Log warning in dev
        Console.WriteLine($"Missing config: {string.Join(",", missing)}");
    }
}

Q11: IOptionsMonitor OnChange vs Reload

// IOptionsMonitor allows subscribing to changes
monitor.OnChange((newOptions, name) =>
{
    // Fired when config file changes
    Console.WriteLine($"Config reloaded: {newOptions.ApiTimeout}");
});

// IOptions supports GetReloadToken() for polling
var token = configuration.GetReloadToken();
token.RegisterChangeCallback((_) => 
{
    Console.WriteLine("Config changed");
}, null);

Q12: Override Configuration Programmatically

// Option 1: Configure with factory
services.Configure<AppSettings>(options =>
{
    var baseConfig = Configuration.GetSection("AppSettings").Get<AppSettings>();
    options.ApiTimeout = Math.Max(baseConfig.ApiTimeout, 60); // Ensure minimum
});

// Option 2: PostConfigure (applied after initial config)
services.PostConfigure<AppSettings>(options =>
{
    options.ApiTimeout = Math.Max(options.ApiTimeout, 10);
});

Q13: Azure Key Vault with Managed Identity

// No credentials needed - uses Azure AD managed identity
var credential = new DefaultAzureCredential();
config.AddAzureKeyVault(
    new Uri("https://mykeyvault.vault.azure.net/"),
    credential);

// DefaultAzureCredential tries (in order):
// 1. Environment variables
// 2. Managed identity (Azure VMs, App Service)
// 3. Azure CLI credentials
// 4. Visual Studio credentials

Q14: Real-World Scenario - Multi-Environment SaaS

// Configuration layering for SaaS with feature flags
config
    .AddJsonFile("appsettings.json")                      // Base
    .AddJsonFile($"appsettings.{env}.json", optional: true)  // Environment
    .AddEnvironmentVariables()                             // Container/K8s
    .AddAzureAppConfiguration(opts =>                     // Feature flags
    {
        opts.Connect(connString)
            .Select(KeyFilter.Any, LabelFilter.Null)
            .Select(KeyFilter.Any, env);
    })
    .AddAzureKeyVault(vaultUrl, credential);             // Secrets

// Results in priority: Base → Env → EnvVars → AppConfig → KeyVault

Key Takeaways

✅ Use IConfiguration for direct config access
✅ Use IOptions for strongly-typed, validated settings
✅ Validate on startup to fail fast
✅ Layer providers appropriately (Json → EnvVars → KeyVault)
✅ Never commit secrets - use Key Vault or User Secrets
✅ Use environment-specific files for different deployments
✅ Choose right IOptions variant (IOptions/Monitor/Snapshot)
✅ Document configuration requirements clearly
✅ Test in multiple environments before production
✅ Use Azure managed identity instead of connection strings

Comments

Popular posts from this blog