Skip to content

Unify sync and async options validation contracts#131048

Draft
oroztocil wants to merge 15 commits into
mainfrom
async-options-validation-130719
Draft

Unify sync and async options validation contracts#131048
oroztocil wants to merge 15 commits into
mainfrom
async-options-validation-130719

Conversation

@oroztocil

@oroztocil oroztocil commented Jul 19, 2026

Copy link
Copy Markdown
Member

Implements API proposal #130719. Builds on top of #130263 and addresses its review feedback.

Currently, this is a prototype implementation used for exploring the proposed design change.

Contract unification

  • IAsyncValidateOptions<T> now extends IValidateOptions<T> and is invariant (was standalone IAsyncValidateOptions<in TOptions>).
  • IAsyncStartupValidator now extends IStartupValidator.
  • Built-in delegate AsyncValidateOptions<…> (all 6 arities) gain a conservative synchronous Validate that returns Skip for non-matching names, and fails for matching names (never sync-over-async).
  • Single validator collection: all validators are registered as IValidateOptions<T>, async is a capability checked at runtime. Create always runs Validate, CreateAsync prefers ValidateAsync when the validator also implements IAsyncValidateOptions<T> (never both). Registration order preserved.
  • Misregistration detection: a validator registered as IAsyncValidateOptions<T> instead of IValidateOptions<T> produces an InvalidOperationException the first time the options value is created.

Startup validation changes

  • Single startup path: Host.StartAsync resolves one IStartupValidator, calls ValidateAsync when it's also IAsyncStartupValidator, and runs it before hosted-service construction in order to seed the options cache before potential synchronous access (e.g., an IOptions<T>.Value read during service startup).
  • Sync accessors serve the startup-validated value for async-validated types. Once startup validation has seeded a validated instance, IOptions<T> and IOptionsSnapshot<T>  return it instead of re-creating and validating synchronously (which would fail). This means IOptionsSnapshot<T> returns the shared startup-validated instance rather than a fresh per-scope instance for these types. No change in behavior for types with sync-only validators.

Opt-in async revalidation on config reload

  • ValidateOnChange options-builder extension that enables eager async revalidation on configuration reload (default reload stays lazy). Each reload re-creates and re-validates the options in the background, keeps serving the last valid value until the new config passes, then swaps it in atomically.
  • Users can specify behavior on failure: either KeepLastGood (keep the old value) or FailReads (next read throws).
  • Failure observability: Users can optionally provide an onError callback (e.g., for logging via ILogger). Failures are also emitted to a new EventSource.

Source generator changes (addressing feedback from #130263)

  • Always emits synchronous Validate in addition to optional ValidateAsync.
  • New diagnostics: SYSLIB1218 (async validation requested but target < net11) and SYSLIB1219 (type already implements ValidateAsync).
  • More precise signature matching which also handles explicit interface implementations of Validate/ValidateAsync.

Copilot AI review requested due to automatic review settings July 19, 2026 23:16
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 3 pipeline(s).
13 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-extensions-options
See info in area-owners.md if you want to be subscribed.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR extends the Microsoft.Extensions.Options validation pipeline to support asynchronous validation end-to-end: the [OptionsValidator] source generator can emit ValidateAsync (when available), runtime options validation can dispatch/await async-capable validators, hosting startup validation flows through a single async path, and options can opt into eager async revalidation on configuration reload (ValidateOnChange).

Changes:

  • Add async-validation support to the OptionsValidator source generator (async symbol discovery, async code emission, and new diagnostics/strings).
  • Update the runtime options validation and startup-validation plumbing to run sync + async validation in a single path and seed caches for sync accessors.
  • Introduce eager async reload revalidation opt-in (ValidateOnChange) with new public API types, plus extensive new tests.

Reviewed changes

Copilot reviewed 47 out of 47 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
src/libraries/Microsoft.Extensions.Options/tests/SourceGenerationTests/Resources/Strings.resx Adds test-resource strings for new async generator diagnostics.
src/libraries/Microsoft.Extensions.Options/tests/SourceGeneration.Unit.Tests/Resources/Strings.resx Adds unit-test resource strings for new async generator diagnostics.
src/libraries/Microsoft.Extensions.Options/tests/SourceGeneration.Unit.Tests/OptionsRuntimeTests.cs Adds runtime parity/behavior tests for generated async validation.
src/libraries/Microsoft.Extensions.Options/tests/SourceGeneration.Unit.Tests/Main.cs Adds generator tests for async emission and downlevel diagnostics.
src/libraries/Microsoft.Extensions.Options/tests/Microsoft.Extensions.Options.Tests/OptionsMonitorTest.cs Adds tests for new cache replace helper behavior.
src/libraries/Microsoft.Extensions.Options/tests/Microsoft.Extensions.Options.Tests/OptionsBuilderTest.cs Updates startup validator resolution to match new DI shape.
src/libraries/Microsoft.Extensions.Options/tests/Microsoft.Extensions.Options.Tests/Microsoft.Extensions.Options.Tests.csproj Adjusts test project references to compile against implementation sources.
src/libraries/Microsoft.Extensions.Options/tests/Microsoft.Extensions.Options.Tests/AsyncOptionsValidationTests.cs Expands tests for single-path startup validation and reload validation behavior.
src/libraries/Microsoft.Extensions.Options/src/ValidateOnStart.cs Updates commentary to reflect new startup validation orchestration.
src/libraries/Microsoft.Extensions.Options/src/UnnamedOptionsManager.cs Allows consulting the shared validated cache for async-validated types.
src/libraries/Microsoft.Extensions.Options/src/StartupValidatorOptions.cs Updates comments to reflect combined sync+async validation behavior.
src/libraries/Microsoft.Extensions.Options/src/Resources/Strings.resx Adds new runtime error messages for async-only/misregistered validators.
src/libraries/Microsoft.Extensions.Options/src/ReloadValidationConfiguration.cs Introduces public type for reload-validation opt-ins.
src/libraries/Microsoft.Extensions.Options/src/OptionsReloadValidationBehavior.cs Introduces public enum controlling reload failure behavior.
src/libraries/Microsoft.Extensions.Options/src/OptionsMonitor.cs Adds eager async reload revalidation and publishing semantics.
src/libraries/Microsoft.Extensions.Options/src/OptionsManager.cs Enables serving startup-validated values from shared cache for async-validated types.
src/libraries/Microsoft.Extensions.Options/src/OptionsFactory.cs Adds async validation dispatch, CreateAsync, and misregistration detection.
src/libraries/Microsoft.Extensions.Options/src/OptionsCache.cs Adds internal AddOrReplace helper used by startup/reload publishing.
src/libraries/Microsoft.Extensions.Options/src/OptionsBuilderExtensions.cs Updates ValidateOnStart wiring and adds new public ValidateOnChange API.
src/libraries/Microsoft.Extensions.Options/src/OptionsBuilder.cs Registers async validators through IValidateOptions to unify dispatch.
src/libraries/Microsoft.Extensions.Options/src/IAsyncValidateOptions.cs Changes async validation interface shape to align with unified dispatch.
src/libraries/Microsoft.Extensions.Options/src/IAsyncStartupValidator.cs Changes startup validator interface shape to unify sync/async paths.
src/libraries/Microsoft.Extensions.Options/src/AsyncValidateOptions.cs Adds sync Validate implementation to fail fast when async-only is used synchronously.
src/libraries/Microsoft.Extensions.Options/ref/Microsoft.Extensions.Options.cs Updates public surface area (new APIs and interface/ctor changes).
src/libraries/Microsoft.Extensions.Options/gen/SymbolLoader.cs Loads optional async validation symbols for generator emission decisions.
src/libraries/Microsoft.Extensions.Options/gen/SymbolHolder.cs Stores optional async-related symbols for generator pipeline.
src/libraries/Microsoft.Extensions.Options/gen/Resources/xlf/Strings.zh-Hant.xlf Adds new localized string entries (state=new) for generator diagnostics.
src/libraries/Microsoft.Extensions.Options/gen/Resources/xlf/Strings.zh-Hans.xlf Adds new localized string entries (state=new) for generator diagnostics.
src/libraries/Microsoft.Extensions.Options/gen/Resources/xlf/Strings.tr.xlf Adds new localized string entries (state=new) for generator diagnostics.
src/libraries/Microsoft.Extensions.Options/gen/Resources/xlf/Strings.ru.xlf Adds new localized string entries (state=new) for generator diagnostics.
src/libraries/Microsoft.Extensions.Options/gen/Resources/xlf/Strings.pt-BR.xlf Adds new localized string entries (state=new) for generator diagnostics.
src/libraries/Microsoft.Extensions.Options/gen/Resources/xlf/Strings.pl.xlf Adds new localized string entries (state=new) for generator diagnostics.
src/libraries/Microsoft.Extensions.Options/gen/Resources/xlf/Strings.ko.xlf Adds new localized string entries (state=new) for generator diagnostics.
src/libraries/Microsoft.Extensions.Options/gen/Resources/xlf/Strings.ja.xlf Adds new localized string entries (state=new) for generator diagnostics.
src/libraries/Microsoft.Extensions.Options/gen/Resources/xlf/Strings.it.xlf Adds new localized string entries (state=new) for generator diagnostics.
src/libraries/Microsoft.Extensions.Options/gen/Resources/xlf/Strings.fr.xlf Adds new localized string entries (state=new) for generator diagnostics.
src/libraries/Microsoft.Extensions.Options/gen/Resources/xlf/Strings.es.xlf Adds new localized string entries (state=new) for generator diagnostics.
src/libraries/Microsoft.Extensions.Options/gen/Resources/xlf/Strings.de.xlf Adds new localized string entries (state=new) for generator diagnostics.
src/libraries/Microsoft.Extensions.Options/gen/Resources/xlf/Strings.cs.xlf Adds new localized string entries (state=new) for generator diagnostics.
src/libraries/Microsoft.Extensions.Options/gen/Resources/Strings.resx Adds generator resource strings for new diagnostics.
src/libraries/Microsoft.Extensions.Options/gen/Parser.cs Extends model analysis to decide when/how to generate async validation, including synthesized validator caching by capability.
src/libraries/Microsoft.Extensions.Options/gen/Model/ValidatedModel.cs Adds async/self-validate flags to generator model.
src/libraries/Microsoft.Extensions.Options/gen/Model/ValidatedMember.cs Tracks whether nested validators emit async for correct dispatch.
src/libraries/Microsoft.Extensions.Options/gen/Emitter.cs Emits ValidateAsync when requested/available and correctly dispatches nested async validation.
src/libraries/Microsoft.Extensions.Options/gen/DiagDescriptors.cs Adds SYSLIB1218/1219 descriptors for async generation scenarios.
src/libraries/Microsoft.Extensions.Options.DataAnnotations/src/OptionsBuilderDataAnnotationsExtensions.cs Adjusts DataAnnotations registrations to match unified async validation path.
src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs Switches startup validation to prefer a single ValidateAsync path when available.

Comment thread src/libraries/Microsoft.Extensions.Options/src/OptionsMonitor.cs Outdated
Comment thread src/libraries/Microsoft.Extensions.Options/src/OptionsMonitor.cs Outdated
Comment on lines +118 to +123
if (GetType() != typeof(OptionsCache<TOptions>))
{
TryRemove(name);
TryAdd(name, options);
return;
}
Copilot AI review requested due to automatic review settings July 20, 2026 08:35

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 47 out of 47 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (3)

src/libraries/Microsoft.Extensions.Options/src/OptionsMonitor.cs:194

  • InvokeEagerReload runs RefreshAsync as a fire-and-forget Task. If an OnChange listener throws here, the exception will fault the background task and can surface as an UnobservedTaskException later. Since this is running off-thread, it should not allow user callbacks to fault the refresh loop.
    src/libraries/Microsoft.Extensions.Options/ref/Microsoft.Extensions.Options.cs:13
  • This PR changes the public surface area (e.g., new ValidateOnChange, OptionsReloadValidationBehavior, ReloadValidationConfiguration, and interface/ctor changes elsewhere in this file). New public API in dotnet/runtime requires an approved "api-approved" issue and the ref shape must match that approval. I couldn't verify the required API-approval linkage from the provided PR metadata here, so this should be blocked until the api-approved issue and approved shape are linked/validated (or these APIs are made internal until approval).
    public static partial class OptionsBuilderExtensions
    {
        public static Microsoft.Extensions.Options.OptionsBuilder<TOptions> ValidateOnStart<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TOptions>(this Microsoft.Extensions.Options.OptionsBuilder<TOptions> optionsBuilder) where TOptions : class { throw null; }
        public static Microsoft.Extensions.Options.OptionsBuilder<TOptions> ValidateOnChange<TOptions>(this Microsoft.Extensions.Options.OptionsBuilder<TOptions> optionsBuilder, Microsoft.Extensions.Options.OptionsReloadValidationBehavior behavior = Microsoft.Extensions.Options.OptionsReloadValidationBehavior.KeepLastGood, System.Action<string?, System.Exception>? onError = null) where TOptions : class { throw null; }
    }

src/libraries/Microsoft.Extensions.Options/src/OptionsCache.cs:113

  • The XML doc says "atomically", but AddOrReplace falls back to TryRemove+TryAdd when OptionsCache is derived (line 118+), which is not atomic. This should be reworded to avoid overstating the contract.
        /// <summary>
        /// Adds or replaces a named options instance atomically.
        /// </summary>
        /// <param name="name">The name of the options instance.</param>
        /// <param name="options">The options instance.</param>
        internal void AddOrReplace(string? name, TOptions options)
        {

Copilot AI review requested due to automatic review settings July 20, 2026 11:13

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 49 out of 49 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (4)

src/libraries/Microsoft.Extensions.Options/src/OptionsCache.cs:110

  • The new AddOrReplace doc comment claims the operation is atomic, but the implementation falls back to TryRemove/TryAdd when OptionsCache is derived, which is not atomic. The summary should avoid promising atomicity across all implementations.
    src/libraries/Microsoft.Extensions.Options/ref/Microsoft.Extensions.Options.cs:13
  • This PR adds new public surface (e.g., ValidateOnChange / OptionsReloadValidationBehavior / ReloadValidationConfiguration / new OptionsMonitor ctor, plus interface shape changes). Per the header comment in this file, these changes must follow the api-review process, but the provided PR metadata doesn’t reference an api-approved issue. Please link the approved API proposal (issue with the api-approved label) or make the new APIs/internal interface changes internal until approved.
    public static partial class OptionsBuilderExtensions
    {
        public static Microsoft.Extensions.Options.OptionsBuilder<TOptions> ValidateOnStart<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TOptions>(this Microsoft.Extensions.Options.OptionsBuilder<TOptions> optionsBuilder) where TOptions : class { throw null; }
        public static Microsoft.Extensions.Options.OptionsBuilder<TOptions> ValidateOnChange<TOptions>(this Microsoft.Extensions.Options.OptionsBuilder<TOptions> optionsBuilder, Microsoft.Extensions.Options.OptionsReloadValidationBehavior behavior = Microsoft.Extensions.Options.OptionsReloadValidationBehavior.KeepLastGood, System.Action<string?, System.Exception>? onError = null) where TOptions : class { throw null; }
    }

src/libraries/Microsoft.Extensions.Options/src/IAsyncStartupValidator.cs:14

  • Changing IAsyncStartupValidator to inherit IStartupValidator is a breaking change for any existing implementers (they must now implement Validate()). If the intent is to simplify host startup orchestration, consider alternatives that don’t introduce a breaking interface inheritance change, or ensure this is explicitly approved via API review and documented as a breaking change.
    /// <summary>
    /// Used by hosts to asynchronously validate options during startup.
    /// </summary>
    public interface IAsyncStartupValidator : IStartupValidator
    {
        /// <summary>

src/libraries/Microsoft.Extensions.Options/src/IAsyncValidateOptions.cs:14

  • Changing IAsyncValidateOptions to inherit IValidateOptions and removing contravariance (previously in TOptions) is a source-breaking change for implementers and callers relying on variance. If the goal is to ensure async validators participate in the synchronous validation pipeline, consider keeping the existing interface shape and having implementations also implement IValidateOptions explicitly, unless API review explicitly approves the breaking change.
    /// <summary>
    /// Asynchronously validates options.
    /// </summary>
    /// <typeparam name="TOptions">The options type to validate.</typeparam>
    public interface IAsyncValidateOptions<TOptions> : IValidateOptions<TOptions> where TOptions : class
    {

Comment thread src/libraries/Microsoft.Extensions.Options/src/OptionsMonitor.cs
@oroztocil oroztocil changed the title [OptionsValidator] source generator support for async validation Unify sync and async options validation contracts Jul 20, 2026
Copilot AI review requested due to automatic review settings July 20, 2026 13:55

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 49 out of 49 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (2)

src/libraries/Microsoft.Extensions.Options/src/IAsyncValidateOptions.cs:14

  • Changing IAsyncValidateOptions to inherit IValidateOptions and removing contravariance (previously in TOptions) is a breaking public API change: existing implementations of IAsyncValidateOptions will no longer compile unless they also add a Validate method, and variance-dependent assignments stop working. If the goal is capability-detection during options creation, that can be achieved without altering the interface shape (e.g., keep IAsyncValidateOptions separate/contravariant and dispatch via validate is IAsyncValidateOptions<TOptions> when enumerating IValidateOptions).
    /// <summary>
    /// Asynchronously validates options.
    /// </summary>
    /// <typeparam name="TOptions">The options type to validate.</typeparam>
    public interface IAsyncValidateOptions<TOptions> : IValidateOptions<TOptions> where TOptions : class
    {

src/libraries/Microsoft.Extensions.Options/ref/Microsoft.Extensions.Options.cs:16

  • This PR adds new public API surface (e.g., OptionsBuilderExtensions.ValidateOnChange and new public types/constructors elsewhere in this ref file). Per dotnet/runtime policy, new public APIs require a linked issue labeled api-approved with an approved API shape comment; the provided PR metadata doesn’t include any such linkage, so API approval can’t be verified.
    public static partial class OptionsBuilderExtensions
    {
        public static Microsoft.Extensions.Options.OptionsBuilder<TOptions> ValidateOnStart<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TOptions>(this Microsoft.Extensions.Options.OptionsBuilder<TOptions> optionsBuilder) where TOptions : class { throw null; }
        public static Microsoft.Extensions.Options.OptionsBuilder<TOptions> ValidateOnChange<TOptions>(this Microsoft.Extensions.Options.OptionsBuilder<TOptions> optionsBuilder, Microsoft.Extensions.Options.OptionsReloadValidationBehavior behavior = Microsoft.Extensions.Options.OptionsReloadValidationBehavior.KeepLastGood, System.Action<string?, System.Exception>? onError = null) where TOptions : class { throw null; }
    }
    public static partial class OptionsServiceCollectionExtensions
    {
        public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddOptions(this Microsoft.Extensions.DependencyInjection.IServiceCollection services) { throw null; }

Comment on lines +109 to +125
private void InvokeEagerReload(string name, ReloadValidationConfiguration<TOptions> config)
{
EagerReloadState state = _eagerStates!.GetOrAdd(name, static _ => new EagerReloadState());

int generation;
CancellationToken cancellationToken;
lock (state)
{
// Latest-wins: supersede any in-flight refresh for this name so only the newest reload publishes.
state.Cancel();
state.Cts = new CancellationTokenSource();
cancellationToken = state.Cts.Token;
generation = ++state.Generation;
}

_ = RefreshAsync(name, config, state, generation, cancellationToken);
}
@oroztocil
oroztocil changed the base branch from main to vivianad-optionsvalidator-sourcegen July 20, 2026 14:09
@oroztocil
oroztocil changed the base branch from vivianad-optionsvalidator-sourcegen to main July 20, 2026 14:10
@tarekgh tarekgh added this to the 11.0.0 milestone Jul 20, 2026
@tarekgh tarekgh added the NO-MERGE The PR is not ready for merge yet (see discussion for detailed reasons) label Jul 20, 2026
@tarekgh
tarekgh requested review from ViveliDuCh and halter73 July 20, 2026 15:21
@tarekgh

tarekgh commented Jul 20, 2026

Copy link
Copy Markdown
Member

@oroztocil thanks for opening this PR. I am temporally marked it no-merge till the proposed APIs get design reviewed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-Extensions-Options NO-MERGE The PR is not ready for merge yet (see discussion for detailed reasons)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants