Unify sync and async options validation contracts#131048
Conversation
…hesized validator cache
…alidator extend IStartupValidator
|
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. |
|
Tagging subscribers to this area: @dotnet/area-extensions-options |
There was a problem hiding this comment.
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. |
| if (GetType() != typeof(OptionsCache<TOptions>)) | ||
| { | ||
| TryRemove(name); | ||
| TryAdd(name, options); | ||
| return; | ||
| } |
There was a problem hiding this comment.
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)
{
There was a problem hiding this comment.
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-approvedlabel) 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
{
There was a problem hiding this comment.
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 viavalidate 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-approvedwith 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; }
| 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 thanks for opening this PR. I am temporally marked it no-merge till the proposed APIs get design reviewed. |
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 extendsIValidateOptions<T>and is invariant (was standaloneIAsyncValidateOptions<in TOptions>).IAsyncStartupValidatornow extendsIStartupValidator.AsyncValidateOptions<…>(all 6 arities) gain a conservative synchronousValidatethat returnsSkipfor non-matching names, and fails for matching names (never sync-over-async).IValidateOptions<T>, async is a capability checked at runtime.Createalways runsValidate,CreateAsyncprefersValidateAsyncwhen the validator also implementsIAsyncValidateOptions<T>(never both). Registration order preserved.IAsyncValidateOptions<T>instead ofIValidateOptions<T>produces anInvalidOperationExceptionthe first time the options value is created.Startup validation changes
Host.StartAsyncresolves oneIStartupValidator, callsValidateAsyncwhen it's alsoIAsyncStartupValidator, and runs it before hosted-service construction in order to seed the options cache before potential synchronous access (e.g., anIOptions<T>.Valueread during service startup).IOptions<T>andIOptionsSnapshot<T>return it instead of re-creating and validating synchronously (which would fail). This meansIOptionsSnapshot<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
ValidateOnChangeoptions-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.KeepLastGood(keep the old value) orFailReads(next read throws).onErrorcallback (e.g., for logging viaILogger). Failures are also emitted to a newEventSource.Source generator changes (addressing feedback from #130263)
Validatein addition to optionalValidateAsync.SYSLIB1218(async validation requested but target < net11) andSYSLIB1219(type already implementsValidateAsync).Validate/ValidateAsync.