Skip to content

Options Feature Compatibility

davidkallesen edited this page Mar 12, 2026 · 1 revision

Options Feature Compatibility Matrix

This page documents which [OptionsBinding] features can be combined and which combinations are invalid.


Lifetime × Feature Matrix

Feature Singleton (IOptions<T>) Scoped (IOptionsSnapshot<T>) Monitor (IOptionsMonitor<T>)
ValidateDataAnnotations ✅ ✅ ✅
ValidateOnStart ✅ ✅ ✅
ErrorOnMissingKeys ✅ ✅ ✅
Validator (custom) ✅ ✅ ✅
PostConfigure ✅ ✅ ✅
OnChange ❌ ATCOPT004 ❌ ATCOPT004 ✅
Name (named options) ✅ ✅ ✅
ChildSections ✅ ✅ ✅
ConfigureAll ✅ ✅ ✅
AlsoRegisterDirectType ✅ ✅ ✅

Key Constraint: OnChange Requires Monitor

OnChange generates a hosted service (IHostedService) that subscribes to IOptionsMonitor<T>.OnChange(). This API is only available on IOptionsMonitor<T>, so OptionsLifetime.Monitor is required.


Named Options × Feature Matrix

Feature Single Instance Named Options
ValidateDataAnnotations ✅ ✅
ValidateOnStart ✅ ✅
ErrorOnMissingKeys ✅ ✅
Validator (custom) ✅ ✅
PostConfigure ✅ ❌ ATCOPT008
OnChange ✅ ❌ ATCOPT005
ConfigureAll ❌ ATCOPT011 ✅ (requires 2+)
AlsoRegisterDirectType ✅ ✅

Key Constraints

  • OnChange + named options: Not supported because OnChange generates a single hosted service — it can't distinguish between named instances.
  • PostConfigure + named options: Not supported because PostConfigure applies to a single instance. Use ConfigureAll to set defaults across all named instances.
  • ConfigureAll requires 2+ named options: It only makes sense when you have multiple named instances to apply defaults to.

ChildSections Constraints

Rule Diagnostic
Cannot combine ChildSections with Name ATCOPT014
ChildSections requires at least 2 items ATCOPT015
Array items cannot be null or empty ATCOPT016

ChildSections is syntactic sugar that generates multiple [OptionsBinding] attributes with Name set automatically. Therefore you cannot also set Name manually.


Decision Tree: Choosing the Right Lifetime

Do you need live-reload of configuration values?
├── YES → Do you need change notifications?
│   ├── YES → Use Monitor + OnChange
│   └── NO  → Use Monitor (reads latest value on every access)
└── NO → Do you need per-request isolation?
    ├── YES → Use Scoped (IOptionsSnapshot<T>)
    └── NO  → Use Singleton (IOptions<T>) — DEFAULT

When to Use Each Lifetime

Lifetime Interface Behavior Best For
Singleton (default) IOptions<T> Reads config once at startup Static configuration, connection strings
Scoped IOptionsSnapshot<T> Reads latest per request/scope Per-request settings, multi-tenant config
Monitor IOptionsMonitor<T> Always reads latest, supports OnChange Feature flags, runtime toggles

Decision Tree: Validation Strategy

Do you have DataAnnotation attributes on properties?
├── YES → Set ValidateDataAnnotations = true
│   └── Do you want fail-fast at startup?
│       ├── YES → Also set ValidateOnStart = true
│       └── NO  → Validation runs on first access
└── NO → Do you need custom validation logic?
    ├── YES → Create IValidateOptions<T> + set Validator = typeof(...)
    │   └── Also set ValidateOnStart = true for fail-fast
    └── NO → Is the section required to exist?
        ├── YES → Set ErrorOnMissingKeys = true, ValidateOnStart = true
        └── NO  → No validation needed

Valid Combination Examples

Simple validated options

[OptionsBinding("Database", ValidateDataAnnotations = true, ValidateOnStart = true)]
public partial class DatabaseOptions { }

Feature flags with live reload

[OptionsBinding("Features", Lifetime = OptionsLifetime.Monitor, OnChange = nameof(OnChanged))]
public partial class FeatureFlags
{
    public bool EnableNewUI { get; set; }
    internal static void OnChanged(FeatureFlags options, string? name) { }
}

Named options with shared defaults

[OptionsBinding("Email", ChildSections = new[] { "Primary", "Secondary" }, ConfigureAll = nameof(SetDefaults))]
public partial class EmailOptions
{
    public int MaxRetries { get; set; }
    internal static void SetDefaults(EmailOptions o) { o.MaxRetries = 3; }
}

Post-configuration normalization

[OptionsBinding("Storage", PostConfigure = nameof(Normalize))]
public partial class StorageOptions
{
    public string BasePath { get; set; } = string.Empty;
    private static void Normalize(StorageOptions o)
    {
        if (!o.BasePath.EndsWith(Path.DirectorySeparatorChar))
            o.BasePath += Path.DirectorySeparatorChar;
    }
}

See Also

Clone this wiki locally