Skip to content

Bits of .NET

Daily micro-tips for C#, SQL, performance, and scalable backend engineering.

  • Asp.Net Core
  • C#
  • SQL
  • JavaScript
  • CSS
  • About
  • ErcanOPAK.com
  • No Access
Asp.Net Core

.NET Core Configuration Magic: How IOptions Pattern Solves Multi-Environment Headaches

- 05.02.26 - ErcanOPAK

Tired of connection strings breaking between dev/test/prod? IOptions pattern with validation ensures your app never starts with wrong config.

The Configuration Nightmare:

// The old way - scattered magic strings
public class OrderService
{
    private readonly string _connectionString;
    
    public OrderService(IConfiguration configuration)
    {
        _connectionString = configuration.GetConnectionString("DefaultConnection");
        // What if configuration key doesn't exist?
        // What if connection string format is wrong?
        // What if required setting is missing?
    }
}

// Multiple environments, multiple problems
// appsettings.Development.json
// appsettings.Staging.json  
// appsettings.Production.json
// appsettings.Local.json
// Secrets.json
// Environment variables
// Azure Key Vault

// Which value wins? When does validation happen?
// Answer: TOO LATE - at runtime when service tries to use it!

Solution: Strongly Typed IOptions Pattern

// 1. Define configuration class with validation
public class DatabaseSettings
{
    [Required(ErrorMessage = "Connection string is required")]
    [RegularExpression("^Server=.+;Database=.+;User Id=.+;Password=.+;", 
        ErrorMessage = "Invalid connection string format")]
    public string ConnectionString { get; set; }
    
    [Range(1, 100, ErrorMessage = "Max connections must be between 1 and 100")]
    public int MaxConnections { get; set; } = 20;
    
    [Range(1, 300, ErrorMessage = "Timeout must be between 1 and 300 seconds")]
    public int CommandTimeout { get; set; } = 30;
    
    public bool EnableSensitiveDataLogging { get; set; }
}

public class EmailSettings
{
    [Required]
    public string SmtpServer { get; set; }
    
    [Range(1, 65535)]
    public int Port { get; set; } = 587;
    
    [Required]
    public string Username { get; set; }
    
    public string Password { get; set; }
    
    public bool EnableSsl { get; set; } = true;
    
    [Range(1, 100)]
    public int RetryCount { get; set; } = 3;
}

// 2. Register with validation in Program.cs
builder.Services.AddOptions()
    .Bind(builder.Configuration.GetSection("Database"))
    .ValidateDataAnnotations() // Automatic validation
    .Validate(settings => 
    {
        // Custom validation logic
        return !settings.ConnectionString.Contains("localhost") 
            || builder.Environment.IsDevelopment();
    }, "Cannot use localhost in production!")
    .ValidateOnStart(); // Critical: Validate at startup, not runtime!

builder.Services.AddOptions()
    .Bind(builder.Configuration.GetSection("Email"))
    .ValidateDataAnnotations()
    .ValidateOnStart();

Usage with Dependency Injection:

// Service using IOptions
public class OrderService
{
    private readonly DatabaseSettings _dbSettings;
    private readonly ILogger _logger;
    
    // Inject IOptions, not raw settings
    public OrderService(IOptions dbOptions, 
                       ILogger logger)
    {
        _dbSettings = dbOptions.Value; // Already validated!
        _logger = logger;
        
        // Safe to use - validation happened at startup
        using var connection = new SqlConnection(_dbSettings.ConnectionString);
        // No null/format checks needed here
    }
}

// Or use IOptionsSnapshot for scoped settings (changes per request)
public class ScopedService
{
    private readonly EmailSettings _emailSettings;
    
    public ScopedService(IOptionsSnapshot emailOptions)
    {
        _emailSettings = emailOptions.Value;
        // IOptionsSnapshot reloads config if it changes
        // Perfect for feature flags that change at runtime
    }
}

// Or IOptionsMonitor for change notifications
public class NotificationService : IDisposable
{
    private readonly IOptionsMonitor _emailMonitor;
    private readonly IDisposable _changeListener;
    
    public NotificationService(IOptionsMonitor emailMonitor)
    {
        _emailMonitor = emailMonitor;
        
        // Listen for configuration changes
        _changeListener = _emailMonitor.OnChange(settings =>
        {
            Console.WriteLine($"Email settings changed at {DateTime.UtcNow}");
            // Reconfigure email client with new settings
            ReconfigureEmailClient(settings);
        });
    }
    
    public void Dispose() => _changeListener?.Dispose();
}

Advanced: Configuration Source Chain

// Program.cs - Complete setup
var builder = WebApplication.CreateBuilder(args);

// 1. Base appsettings.json (lowest priority)
// 2. appsettings.{Environment}.json (overrides base)
// 3. User secrets (development only)
// 4. Environment variables (overrides JSON)
// 5. Command line arguments (highest priority)
// 6. Azure Key Vault (production secrets)

builder.Configuration
    .SetBasePath(Directory.GetCurrentDirectory())
    .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
    .AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", 
        optional: true, reloadOnChange: true);
    
if (builder.Environment.IsDevelopment())
{
    builder.Configuration.AddUserSecrets();
}

builder.Configuration.AddEnvironmentVariables();

if (args != null)
{
    builder.Configuration.AddCommandLine(args);
}

// Azure Key Vault for production
if (builder.Environment.IsProduction())
{
    var keyVaultUrl = builder.Configuration["AzureKeyVault:Url"];
    if (!string.IsNullOrEmpty(keyVaultUrl))
    {
        builder.Configuration.AddAzureKeyVault(
            new Uri(keyVaultUrl),
            new DefaultAzureCredential());
    }
}

// Now register options with this layered configuration
builder.Services.AddOptions()
    .Bind(builder.Configuration.GetSection("Database"))
    .ValidateDataAnnotations()
    .ValidateOnStart();

// The magic: Settings are resolved in this order:
// 1. Command line: --Database:ConnectionString "Server=prod;..."
// 2. Environment: DATABASE__CONNECTIONSTRING (note double underscore)
// 3. User secrets: Database:ConnectionString
// 4. appsettings.Production.json
// 5. appsettings.json

Real-World Configuration Example:

// appsettings.Production.json
{
  "Database": {
    "ConnectionString": "Server=production-db.database.windows.net;...",
    "MaxConnections": 50,
    "CommandTimeout": 60,
    "EnableSensitiveDataLogging": false
  },
  "Email": {
    "SmtpServer": "smtp.sendgrid.net",
    "Port": 587,
    "Username": "apikey",
    "EnableSsl": true,
    "RetryCount": 5
  },
  "FeatureFlags": {
    "EnableNewCheckout": true,
    "EnableDarkMode": false,
    "MaintenanceMode": false
  },
  "ExternalApis": {
    "PaymentGateway": {
      "Url": "https://api.payment.com/v2",
      "ApiKey": "",
      "TimeoutSeconds": 30
    },
    "SmsService": {
      "Url": "https://api.twilio.com",
      "AccountSid": "",
      "AuthToken": ""
    }
  }
}

// appsettings.Development.json - overrides Production settings
{
  "Database": {
    "ConnectionString": "Server=(localdb)\\mssqllocaldb;Database=MyApp_Dev;Trusted_Connection=True;",
    "EnableSensitiveDataLogging": true  // See parameter values in logs
  },
  "FeatureFlags": {
    "EnableNewCheckout": false  // Disable in dev
  }
}

// secrets.json (never committed!)
{
  "Email:Password": "SG.abc123...",
  "ExternalApis:PaymentGateway:ApiKey": "pk_test_456...",
  "ExternalApis:SmsService:AccountSid": "AC123...",
  "ExternalApis:SmsService:AuthToken": "xyz789..."
}

Related posts:

Why Minimal APIs Improve Cold Start

EF Core Performance Boost: Use AsNoTracking() When Reading Data

Boost Async Performance in .NET with ConfigureAwait(false)

Post Views: 3

Post navigation

JavaScript Fetch API Secret: How AbortController Stops Memory Leaks in Single Page Apps
HTML5 Semantic Secret: How Proper Structure Can Boost SEO by 40% Without Extra Content

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

February 2026
M T W T F S S
 1
2345678
9101112131415
16171819202122
232425262728  
« Jan    

Most Viewed Posts

  • Get the User Name and Domain Name from an Email Address in SQL (934)
  • How to add default value for Entity Framework migrations for DateTime and Bool (830)
  • Get the First and Last Word from a String or Sentence in SQL (822)
  • How to select distinct rows in a datatable in C# (799)
  • How to make theater mode the default for Youtube (708)
  • Add Constraint to SQL Table to ensure email contains @ (572)
  • How to enable, disable and check if Service Broker is enabled on a database in SQL Server (552)
  • Average of all values in a column that are not zero in SQL (517)
  • How to use Map Mode for Vertical Scroll Mode in Visual Studio (473)
  • Find numbers with more than two decimal places in SQL (436)

Recent Posts

  • C#: Use init Accessor to Create Immutable Objects Without Constructor Boilerplate
  • C#: Use Index and Range Operators for Cleaner Array Slicing
  • C#: Use Null-Coalescing Assignment to Simplify Lazy Initialization
  • SQL: Use CHAR Instead of VARCHAR for Fixed-Length Columns to Save Space
  • SQL: Use CROSS APPLY Instead of Subqueries for Better Performance
  • .NET Core: Use Required Modifier to Force Property Initialization
  • .NET Core: Use Global Using Directives to Avoid Repeating Imports
  • Git: Use git restore to Unstage Files Without Losing Changes
  • Git: Use git bisect to Find Which Commit Introduced a Bug
  • AJAX: Use Fetch with Signal to Cancel Requests When User Navigates Away

Most Viewed Posts

  • Get the User Name and Domain Name from an Email Address in SQL (934)
  • How to add default value for Entity Framework migrations for DateTime and Bool (830)
  • Get the First and Last Word from a String or Sentence in SQL (822)
  • How to select distinct rows in a datatable in C# (799)
  • How to make theater mode the default for Youtube (708)

Recent Posts

  • C#: Use init Accessor to Create Immutable Objects Without Constructor Boilerplate
  • C#: Use Index and Range Operators for Cleaner Array Slicing
  • C#: Use Null-Coalescing Assignment to Simplify Lazy Initialization
  • SQL: Use CHAR Instead of VARCHAR for Fixed-Length Columns to Save Space
  • SQL: Use CROSS APPLY Instead of Subqueries for Better Performance

Social

  • ErcanOPAK.com
  • GoodReads
  • LetterBoxD
  • Linkedin
  • The Blog
  • Twitter
© 2026 Bits of .NET | Built with Xblog Plus free WordPress theme by wpthemespace.com