Add async startup validation for Microsoft.Extensions.Options#128788
Add async startup validation for Microsoft.Extensions.Options#128788ViveliDuCh wants to merge 7 commits into
Conversation
Implement the async validation API surface approved in dotnet#128100 (DataAnnotations layer): New types: - AsyncValidationAttribute: abstract base class for async validation scenarios. IsValid(object?, ValidationContext) is abstract override; IsValidAsync is the primary async entry point. Sealed override of IsValid(object?) delegates to the context overload. - IAsyncValidatableObject: extends IValidatableObject with ValidateAsync returning IAsyncEnumerable<ValidationResult>. No default interface method. New Validator async methods (8 total): - TryValidateObjectAsync (2 overloads) - TryValidatePropertyAsync - TryValidateValueAsync - ValidateObjectAsync (2 overloads) - ValidatePropertyAsync - ValidateValueAsync All return Task/Task<bool> per API review decision. Implementation details: - 3-step async pipeline matching sync structure: property validation, type attributes, IAsyncValidatableObject/IValidatableObject. - Property validation runs in parallel via Task.WhenAny with linked CancellationTokenSource for cooperative breakOnFirstError. - Per-value validation is two-phase: sync attributes first (abort early), then async attributes in parallel. - try/finally blocks observe in-flight tasks on all exit paths to prevent UnobservedTaskException from the finalizer thread. Refactoring: - Extracted EnsureValidationResultErrorMessage as private protected helper in ValidationAttribute, shared by sync GetValidationResult and async GetValidationResultAsync. - Added RequiresValidationContext XML remark clarifying async behavior. - Added ValidationContext.Items thread-safety remark for parallel async validation. Tests: ~112 test cases covering all async Validator methods, mixed sync/async attributes, breakOnFirstError with parallel async, cancellation propagation, IAsyncValidatableObject, class-level async attributes, error message formatting, sync fallback, and gate-based concurrency probing for deterministic parallelism verification. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add IAsyncValidateOptions<T> and IAsyncStartupValidator interfaces - Add AsyncValidateOptions<T, TDep1..TDep5> lambda-based validators - Add async Validate overloads on OptionsBuilder<T> (0-5 dependencies) - Extend ValidateOnStart to prefer IAsyncStartupValidator when available - Add DataAnnotationValidateOptionsAsync<T> and ValidateDataAnnotationsAsync extension - Wire IAsyncStartupValidator into Host.StartAsync
|
Tagging subscribers to this area: @dotnet/area-system-componentmodel-dataannotations |
There was a problem hiding this comment.
Pull request overview
This PR adds asynchronous startup validation to Microsoft.Extensions.Options, building on async DataAnnotations support from PR #128656. It introduces a parallel async validation pipeline that runs during Host.StartAsync() while leaving the synchronous IOptions<T>.Value / OptionsFactory.Create() path untouched, so existing behavior is preserved.
Changes:
- New public APIs:
IAsyncValidateOptions<T>,IAsyncStartupValidator,AsyncValidateOptions<TOptions>(0–5 deps),DataAnnotationValidateOptionsAsync<T>, asyncValidateoverloads onOptionsBuilder<T>, andValidateDataAnnotationsAsync(). StartupValidatornow also implementsIAsyncStartupValidator(running sync validators first, then async);ValidateOnStart()registers async entries whenIAsyncValidateOptions<T>services are present.Host.StartAsync()prefersIAsyncStartupValidatoroverIStartupValidatorwhen both are registered, with corresponding ref-assembly updates and System.ComponentModel.Annotations async validation primitives (AsyncValidationAttribute,IAsyncValidatableObject, asyncValidatormethods).
Reviewed changes
Copilot reviewed 24 out of 24 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/AsyncValidationAttribute.cs |
New abstract base for async validation attributes. |
src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/IAsyncValidatableObject.cs |
New interface extending IValidatableObject with IAsyncEnumerable results. |
src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/Validator.cs |
Adds 8 async Validator methods with two-phase parallel async validation. |
src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/ValidationAttribute.cs |
Adds EnsureValidationResultErrorMessage helper used by both sync and async paths. |
src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/ValidationContext.cs |
Doc-only thread-safety remarks for Items. |
src/libraries/System.ComponentModel.Annotations/src/System.ComponentModel.Annotations.csproj |
Includes new files in compilation. |
src/libraries/System.ComponentModel.Annotations/ref/System.ComponentModel.Annotations.cs |
Ref assembly for new async APIs. |
src/libraries/System.ComponentModel.Annotations/tests/System/ComponentModel/DataAnnotations/ValidatorTests.cs |
Extensive tests for Validator.*Async methods, parallelism, cancellation, and IAsyncValidatableObject. |
src/libraries/System.ComponentModel.Annotations/tests/System/ComponentModel/DataAnnotations/ValidationAttributeTests.cs |
Tests for AsyncValidationAttribute. |
src/libraries/Microsoft.Extensions.Options/src/IAsyncValidateOptions.cs |
New interface for async options validation. |
src/libraries/Microsoft.Extensions.Options/src/IAsyncStartupValidator.cs |
New interface for async startup validation. |
src/libraries/Microsoft.Extensions.Options/src/AsyncValidateOptions.cs |
Async validator classes (0–5 dependencies). |
src/libraries/Microsoft.Extensions.Options/src/OptionsBuilder.cs |
Async Validate(...) overloads. |
src/libraries/Microsoft.Extensions.Options/src/OptionsBuilderExtensions.cs |
ValidateOnStart now registers IAsyncStartupValidator and async validator entries. |
src/libraries/Microsoft.Extensions.Options/src/StartupValidatorOptions.cs |
Adds _asyncValidators dictionary. |
src/libraries/Microsoft.Extensions.Options/src/ValidateOnStart.cs |
StartupValidator implements IAsyncStartupValidator. |
src/libraries/Microsoft.Extensions.Options/ref/Microsoft.Extensions.Options.cs |
Ref assembly updates for async API surface. |
src/libraries/Microsoft.Extensions.Options.DataAnnotations/src/DataAnnotationValidateOptionsAsync.cs |
Async DataAnnotations validator for options (recursive [ValidateObjectMembers]/[ValidateEnumeratedItems]). |
src/libraries/Microsoft.Extensions.Options.DataAnnotations/src/OptionsBuilderDataAnnotationsExtensions.cs |
Marks the class partial. |
src/libraries/Microsoft.Extensions.Options.DataAnnotations/src/OptionsBuilderDataAnnotationsExtensions.Async.cs |
Adds ValidateDataAnnotationsAsync<T> extension. |
src/libraries/Microsoft.Extensions.Options.DataAnnotations/src/Microsoft.Extensions.Options.DataAnnotations.csproj |
Excludes async sources for non-NetCoreAppCurrent. |
src/libraries/Microsoft.Extensions.Options.DataAnnotations/ref/Microsoft.Extensions.Options.DataAnnotations.csproj / .Async.cs |
Ref assembly entries for async API. |
src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs |
Prefers IAsyncStartupValidator over IStartupValidator during StartAsync. |
|
The PR currently has no test coverage for the Options layer. All test files in the diff ( The following Options-specific scenarios have no tests:
|
|
The PR description mentions that the |
tarekgh
left a comment
There was a problem hiding this comment.
Modulo the open comments, LGTM. Thanks!
Filed #128882 to track the source generator work and linked it from the PR description. Thanks @tarekgh |
…operators, use IsTargetFrameworkCompatible, add async validation tests
… async extensions
16afa8e to
572547a
Compare
…ionsAsync, DataAnnotationValidateOptionsAsync)
| <Compile Include="System\ComponentModel\DataAnnotations\AsyncValidationAttribute.cs" /> | ||
| <Compile Include="System\ComponentModel\DataAnnotations\AssociationAttribute.cs" /> |
| public virtual Microsoft.Extensions.Options.OptionsBuilder<TOptions> Validate(System.Func<TOptions, System.Threading.CancellationToken, System.Threading.Tasks.Task<bool>> validation) { throw null; } | ||
| public virtual Microsoft.Extensions.Options.OptionsBuilder<TOptions> Validate(System.Func<TOptions, System.Threading.CancellationToken, System.Threading.Tasks.Task<bool>> validation, string failureMessage) { throw null; } | ||
| public virtual Microsoft.Extensions.Options.OptionsBuilder<TOptions> Validate<TDep>(System.Func<TOptions, TDep, System.Threading.CancellationToken, System.Threading.Tasks.Task<bool>> validation) where TDep : notnull { throw null; } | ||
| public virtual Microsoft.Extensions.Options.OptionsBuilder<TOptions> Validate<TDep>(System.Func<TOptions, TDep, System.Threading.CancellationToken, System.Threading.Tasks.Task<bool>> validation, string failureMessage) where TDep : notnull { throw null; } | ||
| public virtual Microsoft.Extensions.Options.OptionsBuilder<TOptions> Validate<TDep1, TDep2>(System.Func<TOptions, TDep1, TDep2, System.Threading.CancellationToken, System.Threading.Tasks.Task<bool>> validation) where TDep1 : notnull where TDep2 : notnull { throw null; } |
| var validators = new List<IAsyncValidateOptions<TOptions>>(asyncValidators); | ||
| if (validators.Count > 0) | ||
| { | ||
| vo._asyncValidators[(typeof(TOptions), optionsBuilder.Name)] = async (CancellationToken ct) => | ||
| { | ||
| // Retrieve the options value (already created by sync Validate() call) | ||
| TOptions optionsValue = options.Get(optionsBuilder.Name); | ||
|
|
||
| // Run async validators | ||
| List<string>? failures = null; | ||
| foreach (IAsyncValidateOptions<TOptions> validator in validators) | ||
| { | ||
| ValidateOptionsResult result = await validator.ValidateAsync(optionsBuilder.Name, optionsValue, ct).ConfigureAwait(false); | ||
| if (result is not null && result.Failed) | ||
| { | ||
| failures ??= new List<string>(); | ||
| failures.AddRange(result.Failures); | ||
| } | ||
| } | ||
|
|
||
| if (failures is not null && failures.Count > 0) | ||
| { | ||
| throw new OptionsValidationException(optionsBuilder.Name, typeof(TOptions), failures); | ||
| } | ||
| }; |
| if (exceptions.Count > 1) | ||
| { | ||
| throw new AggregateException(exceptions); | ||
| } |
| /// <exception cref="System.AggregateException"> | ||
| /// Multiple validators return failed <see cref="ValidateOptionsResult"/> results when validating. | ||
| /// </exception> |
Fixes #128100
Implements async startup validation for
Microsoft.Extensions.Optionsas approved in API review.Follow-up to #128656
Source generator support (
[OptionsValidator]emittingValidateAsync()) is tracked separately in #128882.OptionsFactory.Create()andIOptions<T>.Valueremain fully synchronous. Async validators run in a separate step duringHost.StartAsync()only. Lazy validation via.Valueand runtime reload viaIOptionsMonitor<T>are not affected. See design rationale.What's included
IAsyncValidateOptions<TOptions>— async counterpart toIValidateOptions<T>returningTask<ValidateOptionsResult>IAsyncStartupValidator— async counterpart toIStartupValidatorfor host-level startup validationAsyncValidateOptions<TOptions>throughAsyncValidateOptions<TOptions, TDep1..TDep5>— lambda-based async validators (0–5 dependencies), mirroring the syncValidateOptions<T, TDep>familyValidateoverloads onOptionsBuilder<TOptions>(0–5 dependencies) — registersIAsyncValidateOptions<T>via lambdaDataAnnotationValidateOptionsAsync<TOptions>— async counterpart toDataAnnotationValidateOptions<T>, callsValidator.TryValidateObjectAsyncand walks[ValidateObjectMembers]/[ValidateEnumeratedItems]recursivelyValidateDataAnnotationsAsync()extension method onOptionsBuilderDataAnnotationsExtensionsStartupValidatorextended to implementIAsyncStartupValidator— runs sync validators first, then async validators, collecting allOptionsValidationExceptionsHost.StartAsync()updated to preferIAsyncStartupValidatorwhen available, falling back to syncIStartupValidatorValidateOnStart()extended to register async validator entries alongside sync entries whenIAsyncValidateOptions<T>services are presentNot in scope
IAsyncOptions<T>,IAsyncOptionsSnapshot<T>, orIAsyncOptionsMonitor<T>— lazy async resolution is blocked byIOptions<T>.Valuebeing a C# property andOptionsCacheusingLazy<T>/ConcurrentDictionary(no async counterparts in the BCL)IOptionsMonitor<T>config changes —OnChangecallbacks remain sync-only[OptionsValidator]source generator emittingValidateAsync()forIAsyncValidateOptions<T>is tracked separatelyImplementation notes
StartupValidator.ValidateAsync()calls syncValidate()first (which triggersIOptions<T>.Value→OptionsFactory.Create()→ sync validators), then iterates registered async validators sequentiallyValidateOnStart()registers both sync and async entries inStartupValidatorOptions. Async entries are only added whenIAsyncValidateOptions<T>services are detected at configure-timeHost.StartAsync()resolvesIAsyncStartupValidatorfirst; if not available, falls back toIStartupValidator(backward compatible)OptionsValidationExceptions from different async validators are aggregated into anAggregateExceptionwhen more than one failsAPI review decisions reflected
Task<ValidateOptionsResult>return type onIAsyncValidateOptions<T>(notValueTask)IAsyncStartupValidatoras a separate interface (not extendingIStartupValidator)Validateoverloads onOptionsBuilder<T>acceptFunc<TOptions, ..., CancellationToken, Task<bool>>StartupValidatorimplements bothIStartupValidatorandIAsyncStartupValidator, registered as a single service