Skip to content

Configuration Guide

Complete guide to configuration management using the Options pattern in the ConnectSoft Library Template.

Overview

The template uses the Options pattern from Microsoft.Extensions.Options for strongly-typed, validated configuration. This pattern provides:

  • Type Safety: Compile-time checking of configuration
  • Validation: Configuration validated on startup
  • Binding: Automatic binding from configuration sources
  • Flexibility: Support for multiple configuration sources

Options Pattern Implementation

Basic Options Class

When UseOptions=true, the template generates a sample options class:

namespace YourLibraryName.Options
{
    using System.ComponentModel.DataAnnotations;

    /// <summary>
    /// YourLibraryName settings object.
    /// </summary>
    public class LibraryTemplateOptions
    {
        /// <summary>
        /// YourLibraryName section name.
        /// </summary>
        public const string LibraryTemplateSectionName = "LibraryTemplate";

        /// <summary>
        /// Gets or sets a YourLibraryName name.
        /// </summary>
        [Required]
        required public string LibraryTemplateName { get; set; }
    }
}

Key Components

  1. Section Name Constant: Defines the configuration section name
  2. Properties: Configuration properties with DataAnnotations
  3. Required Properties: Marked with [Required] attribute

Configuration Binding

Service Registration

Register options in your service collection:

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddMyLibrary(
        this IServiceCollection services,
        IConfiguration configuration)
    {
        // Bind options from configuration
        services.Configure<MyLibraryOptions>(
            configuration.GetSection(MyLibraryOptions.SectionName));

        // Add validation
        services.AddOptions<MyLibraryOptions>()
            .Bind(configuration.GetSection(MyLibraryOptions.SectionName))
            .ValidateDataAnnotations()
            .ValidateOnStart();

        return services;
    }
}

Configuration Sources

Options can be bound from multiple sources:

appsettings.json:

{
  "MyLibrary": {
    "LibraryTemplateName": "My Production Library",
    "MaxRetries": 3,
    "Timeout": 30
  }
}

Environment Variables:

export MyLibrary__LibraryTemplateName="My Library"
export MyLibrary__MaxRetries=5

Command Line Arguments:

dotnet run --MyLibrary:LibraryTemplateName="My Library"

Validation Patterns

DataAnnotations Validation

Use DataAnnotations attributes for validation:

public class MyLibraryOptions
{
    public const string SectionName = "MyLibrary";

    [Required]
    [MinLength(3)]
    [MaxLength(100)]
    public required string LibraryTemplateName { get; set; }

    [Range(1, 100)]
    public int MaxRetries { get; set; } = 3;

    [Url]
    public string? BaseUrl { get; set; }

    [EmailAddress]
    public string? ContactEmail { get; set; }
}

Available Attributes:

  • [Required]: Property must have a value
  • [MinLength(n)]: Minimum string length
  • [MaxLength(n)]: Maximum string length
  • [Range(min, max)]: Numeric range validation
  • [Url]: Valid URL format
  • [EmailAddress]: Valid email format
  • [RegularExpression(pattern)]: Custom regex validation

Source-Generated Validation

The template includes source-generated validation for better performance:

namespace YourLibraryName.Options
{
    using Microsoft.Extensions.Options;

    /// <summary>
    /// Source-generated validator for LibraryTemplateOptions.
    /// </summary>
    [OptionsValidator]
    public partial class ValidateLibraryTemplateOptions : IValidateOptions<LibraryTemplateOptions>
    {
    }
}

Benefits:

  • Compile-time validation code generation
  • Better performance than reflection-based validation
  • Type-safe validation

Usage:

services.AddOptions<MyLibraryOptions>()
    .Bind(configuration.GetSection(MyLibraryOptions.SectionName))
    .ValidateDataAnnotations()
    .ValidateOnStart();

Custom Validation

Implement IValidateOptions<T> for custom validation:

public class CustomMyLibraryOptionsValidator : IValidateOptions<MyLibraryOptions>
{
    public ValidateOptionsResult Validate(string name, MyLibraryOptions options)
    {
        var failures = new List<string>();

        if (options.MaxRetries > options.Timeout)
        {
            failures.Add("MaxRetries cannot be greater than Timeout");
        }

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

// Registration
services.AddSingleton<IValidateOptions<MyLibraryOptions>, CustomMyLibraryOptionsValidator>();

Configuration Examples

Simple Configuration

public class SimpleOptions
{
    public const string SectionName = "Simple";

    [Required]
    public required string ApiKey { get; set; }
}

appsettings.json:

{
  "Simple": {
    "ApiKey": "your-api-key-here"
  }
}

Complex Configuration

public class ComplexOptions
{
    public const string SectionName = "Complex";

    [Required]
    public required string ApiKey { get; set; }

    [Range(1, 100)]
    public int MaxRetries { get; set; } = 3;

    [Range(1, 300)]
    public int TimeoutSeconds { get; set; } = 30;

    public RetryPolicy RetryPolicy { get; set; } = new();
}

public class RetryPolicy
{
    [Required]
    public required string Strategy { get; set; }

    [Range(0, 1000)]
    public int BackoffMilliseconds { get; set; } = 100;
}

appsettings.json:

{
  "Complex": {
    "ApiKey": "your-api-key",
    "MaxRetries": 5,
    "TimeoutSeconds": 60,
    "RetryPolicy": {
      "Strategy": "Exponential",
      "BackoffMilliseconds": 200
    }
  }
}

Array Configuration

public class ArrayOptions
{
    public const string SectionName = "Array";

    public string[] AllowedHosts { get; set; } = Array.Empty<string>();

    public List<Endpoint> Endpoints { get; set; } = new();
}

public class Endpoint
{
    [Required]
    public required string Name { get; set; }

    [Url]
    public required string Url { get; set; }
}

appsettings.json:

{
  "Array": {
    "AllowedHosts": ["host1.com", "host2.com"],
    "Endpoints": [
      {
        "Name": "Primary",
        "Url": "https://api.example.com"
      },
      {
        "Name": "Secondary",
        "Url": "https://api2.example.com"
      }
    ]
  }
}

Environment-Specific Configuration

Development Configuration

appsettings.Development.json:

{
  "MyLibrary": {
    "LibraryTemplateName": "My Library (Development)",
    "MaxRetries": 1,
    "Timeout": 10
  }
}

Production Configuration

appsettings.Production.json:

{
  "MyLibrary": {
    "LibraryTemplateName": "My Library (Production)",
    "MaxRetries": 5,
    "Timeout": 60
  }
}

Configuration Hierarchy

Configuration sources are loaded in order (later sources override earlier ones):

  1. appsettings.json (base configuration)
  2. appsettings.{Environment}.json (environment-specific)
  3. Environment variables
  4. Command-line arguments

Using Options in Code

Constructor Injection

public class MyService
{
    private readonly MyLibraryOptions _options;

    public MyService(IOptions<MyLibraryOptions> options)
    {
        _options = options.Value;
    }

    public void DoWork()
    {
        var name = _options.LibraryTemplateName;
        // Use configuration
    }
}

IOptionsSnapshot (for reloadable configuration)

public class MyService
{
    private readonly IOptionsSnapshot<MyLibraryOptions> _options;

    public MyService(IOptionsSnapshot<MyLibraryOptions> options)
    {
        _options = options;
    }

    public void DoWork()
    {
        // Gets current configuration (reloadable)
        var currentOptions = _options.Value;
    }
}

IOptionsMonitor (for change notifications)

public class MyService : IDisposable
{
    private readonly IOptionsMonitor<MyLibraryOptions> _options;
    private readonly IDisposable _changeListener;

    public MyService(IOptionsMonitor<MyLibraryOptions> options)
    {
        _options = options;
        _changeListener = _options.OnChange(OnConfigurationChanged);
    }

    private void OnConfigurationChanged(MyLibraryOptions options)
    {
        // Handle configuration changes
    }

    public void Dispose()
    {
        _changeListener?.Dispose();
    }
}

Validation on Startup

ValidateOnStart

Ensure configuration is valid when the application starts:

services.AddOptions<MyLibraryOptions>()
    .Bind(configuration.GetSection(MyLibraryOptions.SectionName))
    .ValidateDataAnnotations()
    .ValidateOnStart(); // Validates immediately, throws on invalid config

Benefits:

  • Fail fast on invalid configuration
  • Catch configuration errors early
  • Prevent runtime errors

Validation Errors

If validation fails, an OptionsValidationException is thrown:

try
{
    // Service registration with validation
}
catch (OptionsValidationException ex)
{
    foreach (var failure in ex.Failures)
    {
        Console.WriteLine($"Validation error: {failure}");
    }
}

Best Practices

1. Use Constants for Section Names

public class MyLibraryOptions
{
    public const string SectionName = "MyLibrary"; // Use constant
    // ...
}

Benefits:

  • Type-safe section name references
  • Easy refactoring
  • Prevents typos

2. Mark Required Properties

[Required]
public required string ApiKey { get; set; }

Benefits:

  • Clear documentation of required fields
  • Validation on startup
  • Prevents runtime errors

3. Provide Sensible Defaults

public int MaxRetries { get; set; } = 3; // Default value

Benefits:

  • Works out of the box
  • Clear expected behavior
  • Reduces configuration burden

4. Use Strongly-Typed Nested Objects

public RetryPolicy RetryPolicy { get; set; } = new();

Benefits:

  • Type safety
  • IntelliSense support
  • Clear structure

5. Validate on Startup

.ValidateDataAnnotations()
.ValidateOnStart()

Benefits:

  • Fail fast
  • Early error detection
  • Better debugging

Common Patterns

Options with Service Registration

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddMyLibrary(
        this IServiceCollection services,
        IConfiguration configuration)
    {
        // Configure options
        services.Configure<MyLibraryOptions>(
            configuration.GetSection(MyLibraryOptions.SectionName));

        // Validate options
        services.AddOptions<MyLibraryOptions>()
            .Bind(configuration.GetSection(MyLibraryOptions.SectionName))
            .ValidateDataAnnotations()
            .ValidateOnStart();

        // Register services
        services.AddSingleton<IMyService, MyService>();

        return services;
    }
}

Options with Factory Pattern

services.AddSingleton<IMyService>(sp =>
{
    var options = sp.GetRequiredService<IOptions<MyLibraryOptions>>().Value;
    return new MyService(options);
});

Troubleshooting

Configuration Not Binding

Problem: Options properties are null or default values.

Solutions:

  1. Check section name matches SectionName constant
  2. Verify JSON structure matches class structure
  3. Ensure configuration source is loaded
  4. Check for typos in property names

Validation Failing

Problem: OptionsValidationException on startup.

Solutions:

  1. Check DataAnnotations attributes
  2. Verify required properties have values
  3. Check range constraints
  4. Review validation error messages

Environment Variables Not Working

Problem: Environment variables not overriding configuration.

Solutions:

  1. Use double underscore (__) for nested properties
  2. Check environment variable names match section structure
  3. Verify environment is set correctly
  4. Check configuration source order

References