"Clean code always looks like it was written by someone who cares."
— Robert C. Martin (Uncle Bob)

Options Pattern

✅ One-liner Summary

Use the Options pattern to manage configuration settings in a type-safe, maintainable way.

💡 Short Explanation

The Options pattern provides a strongly-typed way to access configuration settings in your application.
Instead of directly accessing configuration values through strings, you create classes that represent your configuration sections,
making it easier to maintain, refactor, and validate your settings.

🚫 Bad Example

This approach is error-prone and hard to maintain:

public class EmailService
{
    private readonly IConfiguration _configuration;

    public EmailService(IConfiguration configuration)
    {
        _configuration = configuration;
    }

    public void SendEmail(string to)
    {
        var smtpServer = _configuration["Email:SmtpServer"]; // ⚠️ String-based access
        var port = int.Parse(_configuration["Email:Port"]); // ⚠️ Manual parsing
        var username = _configuration["Email:Username"];
        var password = _configuration["Email:Password"];
        
        // Use settings...
    }
}

✅ Good Example

First, define your options class:

public class EmailOptions
{
    public string SmtpServer { get; set; }
    public int Port { get; set; }
    public string Username { get; set; }
    public string Password { get; set; }
}

Then, use it in your service:

public class EmailService(IOptions<EmailOptions> options)
{
    private readonly EmailOptions _options = options.Value;

    public void SendEmail(string to)
    {
        // Use _options.SmtpServer, _options.Port, etc.
    }
}

Finally, register it in your dependency injection container:

// In Program.cs or Startup.cs
services.Configure<EmailOptions>(configuration.GetSection(nameof(EmailOptions)));

💬 Real-World Insight

The Options pattern is particularly valuable in microservices architectures where configuration management is crucial.
It helps prevent runtime errors from typos in configurations and makes it easier to track which parts of your application depend on specific configuration settings.
When you need to change a setting, the compiler will help you find all the places that need to be updated.

🧠 Key Takeaways

  • Provides type-safe access to configuration
  • Enables better IntelliSense support
  • Reduces runtime errors from configuration mistakes
  • Supports validation of configuration values
  • Makes testing easier through dependency injection

🛠️ Implementation Guide

To implement the Options pattern in your project:

  1. Install the required NuGet package:
<PackageReference Include="Microsoft.Extensions.Options" Version="X.Y.Z" />
  1. Define your configuration in appsettings.json :
{
  "EmailOptions": {
    "SmtpServer": "smtp.mail.com",
    "Port": 587,
    "Username": "my-email@mail.com",
    "Password": "my-password"
  }
}
  1. Create your options class:
public class EmailOptions
{
    public string SmtpServer { get; set; }
    public int Port { get; set; }
    public string Username { get; set; }
    public string Password { get; set; }
}
  1. Register the options in your dependency injection container:
// In Program.cs or Startup.cs
builder.Services.Configure<EmailOptions>(builder.Configuration.GetSection(nameof(EmailOptions)));
  1. Inject and use the options in your services:
public class EmailService(IOptions<EmailOptions> options)
{
    private readonly EmailOptions _options = options.Value;

    public void SendEmail(string to)
    {
        // Use _options.SmtpServer, _options.Port, etc.
    }
}

✅ Validation

You can add validation to your options classes using IValidatableObject:

public class EmailOptions : IValidatableObject
{
    public string SmtpServer { get; set; }
    public int Port { get; set; }
    public string Username { get; set; }
    public string Password { get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        if (string.IsNullOrEmpty(SmtpServer))
            yield return new ValidationResult("SmtpServer is required");
        
        if (Port <= 0 || Port > 65535)
            yield return new ValidationResult("Port must be between 1 and 65535");
            
        if (string.IsNullOrEmpty(Username))
            yield return new ValidationResult("Username is required");
    }
}

To enable validation, add the following to your service registration in Program.cs or Startup.cs:

builder.Services.AddOptions<EmailOptions>()
    .Bind(builder.Configuration.GetSection(nameof(EmailOptions)))
    .ValidateDataAnnotations()
    .ValidateOnStart();

This throws a OptionsValidationException on startup if any of the validation rules fail, ensuring your application won’t start with invalid configuration.