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¶
- Section Name Constant: Defines the configuration section name
- Properties: Configuration properties with DataAnnotations
- 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:
Environment Variables:
Command Line Arguments:
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:
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):
appsettings.json(base configuration)appsettings.{Environment}.json(environment-specific)- Environment variables
- 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¶
Benefits:
- Clear documentation of required fields
- Validation on startup
- Prevents runtime errors
3. Provide Sensible Defaults¶
Benefits:
- Works out of the box
- Clear expected behavior
- Reduces configuration burden
4. Use Strongly-Typed Nested Objects¶
Benefits:
- Type safety
- IntelliSense support
- Clear structure
5. Validate on Startup¶
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:
- Check section name matches
SectionNameconstant - Verify JSON structure matches class structure
- Ensure configuration source is loaded
- Check for typos in property names
Validation Failing¶
Problem: OptionsValidationException on startup.
Solutions:
- Check DataAnnotations attributes
- Verify required properties have values
- Check range constraints
- Review validation error messages
Environment Variables Not Working¶
Problem: Environment variables not overriding configuration.
Solutions:
- Use double underscore (
__) for nested properties - Check environment variable names match section structure
- Verify environment is set correctly
- Check configuration source order
Related Documentation¶
- Features - Options feature overview
- Architecture - Design patterns
- Parameters - Template parameters
- Use Cases - Real-world examples