Instruction file imported from klod68/littlerae (
.github/instructions/configuration.instructions.md). Copyright stays with the author.
--- scope: "configuration" applyTo: "**/*.cs" priority: "feature"
Configuration Management — Standards
Apply these rules for all .NET projects. Configuration is policy — it defines what the system does, not how. The governing principle is Separate Policy from Mechanism: changing a configuration value must never require editing an implementation class, recompiling, or redeploying.
The Options Pattern
Use IOptions<T> and its variants as the standard configuration mechanism. Never
read IConfiguration directly inside business logic — always bind to a typed options
class.
| Interface | Lifetime | Reloads | When to Use |
|---|---|---|---|
IOptions<T> |
Singleton | No | Static configuration that never changes at runtime |
IOptionsSnapshot<T> |
Scoped | Per-request | Configuration that may change between requests (appsettings reload) |
IOptionsMonitor<T> |
Singleton | On-change callback | Long-lived services that must react to configuration changes (background services) |
// Correct — typed options injected via constructor
internal sealed class OrderExportService : IOrderExportService
{
private readonly ExportOptions _options;
private readonly ILogger<OrderExportService> _logger;
public OrderExportService(
IOptions<ExportOptions> options,
ILogger<OrderExportService> logger)
{
_options = options.Value;
_logger = logger;
}
}
// Wrong — reading IConfiguration directly
internal sealed class OrderExportService : IOrderExportService
{
public OrderExportService(IConfiguration config)
{
var pageSize = config.GetValue<int>("Export:PageSize"); // VIOLATION
}
}
Rules:
IOptions<T>for application services with request-scoped or singleton lifetime.IOptionsMonitor<T>forBackgroundServiceand long-running singletons that must respond to live reloads.- Never inject
IConfigurationinto any class outsideProgram.csand composition roots. - Never call
config["SomeKey"]orconfig.GetValue<T>("SomeKey")in business logic.
Options Class Design
Options classes are the typed contract between configuration sources and consuming code.
// Correct — focused, validated, documented
public sealed class ExportOptions
{
/// <summary>Default configuration section name.</summary>
public const string SectionName = "Export";
/// <summary>Maximum number of records per export batch.</summary>
public int BatchSize { get; set; } = 1000;
/// <summary>Output format for exported files.</summary>
public string Format { get; set; } = "csv";
/// <summary>Directory path for export output files.</summary>
public string OutputDirectory { get; set; } = "exports";
/// <summary>Whether export functionality is enabled.</summary>
public bool Enabled { get; set; } = true;
}
Naming: {Feature}Options — e.g., ExportOptions, RetryOptions, CacheOptions.
Never Settings, Config, or Params — the .NET ecosystem convention is Options.
Exception: static black-box libraries use {Component}Settings when they predate or
operate outside the DI container (see Componentization rules).
Rules:
- One options class per feature or concern — never a god
AppOptionswith 30 properties. - Every property has a sensible default value — the application must work with zero configuration.
public const string SectionNamefield for binding:services.Configure<ExportOptions>(config.GetSection(ExportOptions.SectionName)).- XML
<summary>on every property — options are the public API of configuration. - Options classes are
public sealed classwith{ get; set; }properties (required by the binder). - Never put business logic or computed properties in options classes — they are data bags.
Options Validation
Validate configuration at startup — never discover invalid configuration at runtime when the first request exercises a code path.
// In Program.cs or composition root
services.AddOptions<ExportOptions>()
.Bind(builder.Configuration.GetSection(ExportOptions.SectionName))
.ValidateDataAnnotations() // honors [Required], [Range], etc.
.ValidateOnStart(); // fail at startup, not at first use
// Or with FluentValidation for complex rules
services.AddOptions<ExportOptions>()
.Bind(builder.Configuration.GetSection(ExportOptions.SectionName))
.Validate(options =>
{
if (options.BatchSize <= 0)
return false;
if (!new[] { "csv", "json", "xlsx" }.Contains(options.Format))
return false;
return true;
}, "ExportOptions: BatchSize must be > 0 and Format must be csv, json, or xlsx.")
.ValidateOnStart();
Rules:
- Always call
.ValidateOnStart()— fail fast at application startup, not on first request. - Use
[Required],[Range],[MinLength]data annotations for simple constraints. - Use
.Validate()lambda orIValidateOptions<T>for cross-property validation. - Validation errors must include the options class name and the violated constraint.
Configuration Source Priority
Configuration sources are layered. Later sources override earlier ones.
1. In-code defaults (property initializers on options classes) ← lowest priority
2. appsettings.json ← base settings
3. appsettings.{Environment}.json ← environment overrides
4. Environment variables ← deployment overrides
5. Command-line arguments ← operator overrides
6. User secrets (Development only) ← developer-local secrets
7. Key Vault / external provider ← highest priority (production)
Rules:
appsettings.jsoncontains defaults suitable for Development — it is committed to source control.appsettings.Development.jsoncontains developer-specific overrides — it may be committed or gitignored per team convention.appsettings.Production.jsonmust NOT contain secrets — use environment variables or Key Vault.- Never commit
appsettings.Local.json— add to.gitignore. - Secrets (connection strings, API keys, tokens) must never appear in any committed configuration file.
Section Organization
{
"ConnectionStrings": {
"DefaultConnection": "..."
},
"Export": {
"BatchSize": 1000,
"Format": "csv",
"OutputDirectory": "exports",
"Enabled": true
},
"Retry": {
"MaxAttempts": 3,
"BaseDelaySeconds": 2,
"UseExponentialBackoff": true
},
"Cache": {
"DefaultExpirationMinutes": 30,
"SlidingExpirationMinutes": 10
}
}
Rules:
- Top-level sections map 1:1 to options classes via
SectionName. - Section names are PascalCase — they match the C# class name without the
Optionssuffix. ConnectionStringsis the only exception — it follows the .NET convention.- Flat structure preferred — nesting beyond 2 levels is a signal the options class needs splitting.
Library Configuration (Static Libraries)
Libraries that operate without a DI container use a different pattern: embedded resource defaults with consumer override.
// Three-tier configuration precedence (see embedded-resource-configuration skill):
// 1. Embedded JSON defaults compiled into the assembly
// 2. Consumer appsettings.json overrides (when available)
// 3. Hardcoded fallback values in the settings class
public static class MyLibrarySettings
{
public static string DefaultConnection { get; private set; } = "fallback-value";
public static int MaxRetries { get; private set; } = 3;
public static void Initialize(IConfiguration? configuration = null)
{
// Load embedded defaults, then overlay consumer config
}
}
Rules:
- Static libraries expose a
{Component}Settingsclass withpublic staticproperties. - Default values are always provided — the library must work with zero configuration.
- Consumer override is opt-in — never required.
- See the
embedded-resource-configurationskill for the complete pattern.
Magic Numbers and Hardcoded Policy
Every numeric literal and string constant that represents a policy decision must live in an options class — never inline in implementation code.
// Wrong — policy values hardcoded in implementation
public async Task<IReadOnlyList<Order>> GetRecentAsync(CancellationToken ct)
{
var cutoff = DateTimeOffset.UtcNow.AddDays(-30); // magic number
return await _db.Orders
.Where(o => o.CreatedAt >= cutoff)
.Take(100) // magic number
.ToListAsync(ct);
}
// Correct — policy values from options
public async Task<IReadOnlyList<Order>> GetRecentAsync(CancellationToken ct)
{
var cutoff = DateTimeOffset.UtcNow.AddDays(-_options.RecentDaysThreshold);
return await _db.Orders
.Where(o => o.CreatedAt >= cutoff)
.Take(_options.MaxResults)
.ToListAsync(ct);
}
Exceptions (acceptable hardcoded values):
- Mathematical constants (
Math.PI, buffer sizes from protocol specs). - Framework-mandated values (HTTP status codes like
200,404). - Loop bounds derived from data (
array.Length,list.Count).
Composition Root Registration
All configuration binding and validation is centralized in the composition root —
either Program.cs or a dedicated IServiceCollection extension method per feature.
// Correct — extension method per feature
public static class ExportServiceCollectionExtensions
{
public static IServiceCollection AddExportFeature(
this IServiceCollection services,
IConfiguration configuration)
{
services.AddOptions<ExportOptions>()
.Bind(configuration.GetSection(ExportOptions.SectionName))
.ValidateDataAnnotations()
.ValidateOnStart();
services.AddScoped<IOrderExportService, OrderExportService>();
return services;
}
}
// In Program.cs
builder.Services.AddExportFeature(builder.Configuration);
Reviewer Fitness Functions
| Signal | Severity |
|---|---|
IConfiguration injected outside composition root |
BLOCKER |
config["Key"] or config.GetValue<T>() in business logic |
BLOCKER |
Options class without .ValidateOnStart() |
MAJOR |
| Magic number in implementation class | MAJOR |
| Options class without default values | MAJOR |
| Secrets in committed appsettings file | BLOCKER |
| God options class with 15+ properties | MAJOR |
| Options class with business logic or computed properties | MINOR |
Common Anti-Patterns
| Anti-Pattern | Fix |
|---|---|
IConfiguration injected into services |
Bind to typed IOptions<T> |
config["ConnectionStrings:Default"] in code |
Use IOptions<T> with bound section |
Missing .ValidateOnStart() |
Always validate at startup |
Options class named *Settings in DI apps |
Use *Options (except static libraries) |
| God options class for entire app | One options class per feature |
| Magic number for timeout, batch size, threshold | Move to options class property with default |
Secrets in appsettings.json committed to VCS |
Use user-secrets (dev) or Key Vault (prod) |
| No default values on options properties | Every property must have a sensible default |
| Nested configuration 3+ levels deep | Flatten or split into separate options classes |
IOptionsMonitor in request-scoped service |
Use IOptions or IOptionsSnapshot |
See Also
resilience.md— Polly v8 resilience pipelines, retry, circuit breaker, timeoutworker-service.md— BackgroundService patterns, periodic/queue workers, health checkspersistence.md— Stored-procedure persistence layer, CrudPersistenceHelper patternsnaming.md— Naming conventions for types, methods, properties, namespaces