Configuration and Options Pattern .NET Core
Configuration and Options Pattern in .NET Core
Quick Reference
- Configuration Providers
- Strongly-Typed Configuration (IOptions)
- Configuration Validation
- Environment-Specific Configurations
- 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
| Type | Lifetime | Reload | Use Case |
|---|---|---|---|
| IOptions<T> | Singleton | Never | Static configuration |
| IOptionsMonitor<T> | Singleton | Real-time with notifications | Dynamic updates with listening |
| IOptionsSnapshot<T> | Scoped | Per-request | Web 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?
| Type | Lifetime | Reload | Use Case |
|---|---|---|---|
| IOptions | Singleton | Never | Static config, injected once and cached |
| IOptionsMonitor | Singleton | Real-time | Dynamic updates, listen to changes with OnChange() |
| IOptionsSnapshot | Scoped | Per-request | Web requests, fresh value per scope |
Q3: How to Secure Sensitive Configuration?
- Never hardcode secrets in source
- Use Azure Key Vault for production secrets
- Use User Secrets for development (local machine)
- Use environment variables as fallback
- 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
Post a Comment