dotnet-csharp-configuration

dotnet-csharp-configuration

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "dotnet-csharp-configuration" with this command: npx skills add novotnyllc/dotnet-artisan/novotnyllc-dotnet-artisan-dotnet-csharp-configuration

dotnet-csharp-configuration

Configuration patterns for .NET applications using Microsoft.Extensions.Configuration and Microsoft.Extensions.Options. Covers the Options pattern (IOptions<T> , IOptionsMonitor<T> , IOptionsSnapshot<T> ), validation, user secrets, environment-based configuration, and feature flags with Microsoft.FeatureManagement .

Scope

  • Options pattern (IOptions, IOptionsMonitor, IOptionsSnapshot)

  • Options validation and ValidateOnStart

  • User secrets and environment-based configuration

  • Feature flags with Microsoft.FeatureManagement

  • Configuration source precedence

Out of scope

  • DI container mechanics and service lifetimes -- see [skill:dotnet-csharp-dependency-injection]

  • EditorConfig and analyzer rule configuration -- see [skill:dotnet-editorconfig]

  • Structured logging pipeline configuration -- see [skill:dotnet-structured-logging]

Cross-references: [skill:dotnet-csharp-dependency-injection] for service registration patterns, [skill:dotnet-csharp-coding-standards] for naming conventions.

Configuration Sources and Precedence

Default configuration sources in WebApplication.CreateBuilder (last wins):

  • appsettings.json

  • appsettings.{Environment}.json

  • User secrets (Development only)

  • Environment variables

  • Command-line arguments

var builder = WebApplication.CreateBuilder(args); // Sources above are loaded automatically. Add custom sources: builder.Configuration.AddJsonFile("features.json", optional: true, reloadOnChange: true);

Options Pattern

Bind configuration sections to strongly typed classes and inject them via DI.

Defining Options Classes

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

public string Host { get; set; } = "";
public int Port { get; set; } = 587;
public string FromAddress { get; set; } = "";
public bool UseSsl { get; set; } = true;

}

Options classes use { get; set; } (not init ) because the configuration binder and PostConfigure need to mutate properties. Use [Required] via data annotations for mandatory fields instead.

Registration

builder.Services .AddOptions<SmtpOptions>() .BindConfiguration(SmtpOptions.SectionName) .ValidateDataAnnotations() .ValidateOnStart();

appsettings.json

{ "Smtp": { "Host": "smtp.example.com", "Port": 587, "FromAddress": "noreply@example.com", "UseSsl": true } }

Options Interfaces

Interface Lifetime Reload Behavior Use Case

IOptions<T>

Singleton Never reloads after startup Static config, most services

IOptionsSnapshot<T>

Scoped Reloads per request/scope Per-request config in ASP.NET

IOptionsMonitor<T>

Singleton Live reload + change notification Singletons, background services

Injection Examples

// Static -- most common, singleton-safe public sealed class EmailService(IOptions<SmtpOptions> options) { private readonly SmtpOptions _smtp = options.Value;

public Task SendAsync(string to, string subject, string body,
    CancellationToken ct = default)
{
    // Use _smtp.Host, _smtp.Port, etc.
    return Task.CompletedTask;
}

}

// Live reload in singletons -- monitors config file changes public sealed class FeatureService(IOptionsMonitor<FeatureOptions> monitor) { public bool IsEnabled(string feature) => monitor.CurrentValue.EnabledFeatures.Contains(feature); }

// Per-request in scoped services -- reads latest config each request public sealed class PricingService(IOptionsSnapshot<PricingOptions> snapshot) { public decimal GetMarkup() => snapshot.Value.MarkupPercent; }

Change Notifications with IOptionsMonitor<T>

public sealed class CacheService : IDisposable { private readonly IDisposable? _changeListener; private CacheOptions _current;

public CacheService(IOptionsMonitor&#x3C;CacheOptions> monitor)
{
    _current = monitor.CurrentValue;
    _changeListener = monitor.OnChange(updated =>
    {
        _current = updated;
        // React to config change -- flush cache, resize pool, etc.
    });
}

public void Dispose() => _changeListener?.Dispose();

}

Options Validation

Data Annotations

using System.ComponentModel.DataAnnotations;

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

[Required, MinLength(1)]
public string Host { get; set; } = "";

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

[Required, EmailAddress]
public string FromAddress { get; set; } = "";

}

builder.Services .AddOptions<SmtpOptions>() .BindConfiguration(SmtpOptions.SectionName) .ValidateDataAnnotations() .ValidateOnStart(); // Fail fast at startup, not on first use

IValidateOptions<T> (Complex Validation)

Use when validation logic requires cross-property checks or external dependencies.

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

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

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

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

}

// Register the validator builder.Services.AddSingleton<IValidateOptions<SmtpOptions>, SmtpOptionsValidator>();

ValidateOnStart (Fail Fast)

Always use .ValidateOnStart() to surface configuration errors at startup instead of at first resolution. Without it, invalid config only throws when IOptions<T>.Value is first accessed.

User Secrets (Development)

Store sensitive values outside source control during development.

Initialize (once per project)

dotnet user-secrets init

Set values

dotnet user-secrets set "Smtp:Host" "smtp.example.com" dotnet user-secrets set "ConnectionStrings:Default" "Server=..."

List all secrets

dotnet user-secrets list

Clear all

dotnet user-secrets clear

User secrets are stored in ~/.microsoft/usersecrets/<UserSecretsId>/secrets.json and override appsettings.json values in Development.

Key rules:

  • Never use user secrets in production -- use environment variables, Azure Key Vault, or other vault providers

  • User secrets are loaded automatically when ASPNETCORE_ENVIRONMENT=Development

  • For non-web hosts, explicitly add: builder.Configuration.AddUserSecrets<Program>()

Environment-Based Configuration

Environment Variables

// Hierarchical keys use __ (double underscore) as separator // Environment variable: Smtp__Host=smtp.prod.com // Maps to: configuration["Smtp:Host"]

Per-Environment Files

appsettings.json # Base (all environments) appsettings.Development.json # Overrides for dev appsettings.Staging.json # Overrides for staging appsettings.Production.json # Overrides for prod

// Set environment via ASPNETCORE_ENVIRONMENT or DOTNET_ENVIRONMENT // Defaults to "Production" if not set var env = builder.Environment.EnvironmentName; // "Development", "Staging", "Production"

Conditional Service Registration

if (builder.Environment.IsDevelopment()) { builder.Services.AddSingleton<IEmailSender, ConsoleEmailSender>(); } else { builder.Services.AddSingleton<IEmailSender, SmtpEmailSender>(); }

Feature Flags with Microsoft.FeatureManagement

Microsoft.FeatureManagement.AspNetCore provides structured feature flag support with filters, targeting, and gradual rollout.

Setup

dotnet add package Microsoft.FeatureManagement.AspNetCore

builder.Services.AddFeatureManagement();

Configuration

{ "FeatureManagement": { "NewDashboard": true, "BetaSearch": { "EnabledFor": [ { "Name": "Percentage", "Parameters": { "Value": 50 } } ] }, "DarkMode": { "EnabledFor": [ { "Name": "Targeting", "Parameters": { "Audience": { "Users": [ "alice@example.com" ], "Groups": [ { "Name": "Beta", "RolloutPercentage": 100 } ], "DefaultRolloutPercentage": 0 } } } ] } } }

Usage in Code

// Inject IFeatureManager public sealed class DashboardController(IFeatureManager featureManager) : ControllerBase { [HttpGet] public async Task<IActionResult> Get(CancellationToken ct = default) { if (await featureManager.IsEnabledAsync("NewDashboard")) { return Ok(new { version = "v2", dashboard = "new" }); }

    return Ok(new { version = "v1", dashboard = "legacy" });
}

}

Feature Gate Attribute

// Entire endpoint gated on feature flag [FeatureGate("BetaSearch")] [HttpGet("search")] public async Task<IActionResult> Search(string query, CancellationToken ct = default) { var results = await _searchService.SearchAsync(query, ct); return Ok(results); }

Feature Filters

Filter Purpose

Percentage

Enable for N% of requests (random)

TimeWindow

Enable between start/end dates

Targeting

Enable for specific users, groups, or rollout percentage

Custom Implement IFeatureFilter for domain-specific logic

Custom Feature Filter

[FilterAlias("Browser")] public sealed class BrowserFeatureFilter(IHttpContextAccessor accessor) : IFeatureFilter { public Task<bool> EvaluateAsync(FeatureFilterEvaluationContext context) { var userAgent = accessor.HttpContext?.Request.Headers.UserAgent.ToString() ?? ""; var settings = context.Parameters.Get<BrowserFilterSettings>();

    return Task.FromResult(
        settings?.AllowedBrowsers?.Any(b =>
            userAgent.Contains(b, StringComparison.OrdinalIgnoreCase)) ?? false);
}

}

public sealed class BrowserFilterSettings { public string[] AllowedBrowsers { get; init; } = []; }

// Register builder.Services.AddFeatureManagement() .AddFeatureFilter<BrowserFeatureFilter>();

Named Options

Use named options when you need multiple instances of the same options type (e.g., multiple API clients).

// Registration with names builder.Services .AddOptions<ApiClientOptions>("GitHub") .BindConfiguration("ApiClients:GitHub");

builder.Services .AddOptions<ApiClientOptions>("Jira") .BindConfiguration("ApiClients:Jira");

// Resolution via IOptionsSnapshot<T> or IOptionsMonitor<T> public sealed class ApiClientFactory(IOptionsSnapshot<ApiClientOptions> snapshot) { public HttpClient CreateFor(string name) { var options = snapshot.Get(name); // "GitHub" or "Jira" return new HttpClient { BaseAddress = new Uri(options.BaseUrl) }; } }

Post-Configuration

Apply defaults or overrides after all configuration sources have been processed.

builder.Services.PostConfigure<SmtpOptions>(options => { // Ensure a default port if none specified if (options.Port == 0) { options.Port = options.UseSsl ? 465 : 25; } });

Testing Configuration

[Fact] public void SmtpOptions_Validates_InvalidPort() { var options = new SmtpOptions { Host = "smtp.example.com", FromAddress = "test@example.com", Port = 25, UseSsl = true };

var validator = new SmtpOptionsValidator();
var result = validator.Validate(null, options);

Assert.True(result.Failed);
Assert.Contains("Port 25 does not support SSL", result.FailureMessage);

}

[Fact] public void Configuration_BindsCorrectly() { var config = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary<string, string?> { ["Smtp:Host"] = "smtp.test.com", ["Smtp:Port"] = "465", ["Smtp:FromAddress"] = "test@test.com", }) .Build();

var options = new SmtpOptions();
config.GetSection("Smtp").Bind(options);

Assert.Equal("smtp.test.com", options.Host);
Assert.Equal(465, options.Port);

}

References

  • Options pattern in .NET

  • Configuration in .NET

  • User secrets in development

  • Feature management in .NET

  • IValidateOptions

  • .NET Framework Design Guidelines

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

dotnet-ui

No summary provided by upstream source.

Repository SourceNeeds Review
General

dotnet-csharp

No summary provided by upstream source.

Repository SourceNeeds Review
General

dotnet-api

No summary provided by upstream source.

Repository SourceNeeds Review
General

dotnet-testing

No summary provided by upstream source.

Repository SourceNeeds Review