Add async startup validation for Microsoft.Extensions.Options#128788
Open
ViveliDuCh wants to merge 2 commits into
Open
Add async startup validation for Microsoft.Extensions.Options#128788ViveliDuCh wants to merge 2 commits into
ViveliDuCh wants to merge 2 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
Contributor
|
Tagging subscribers to this area: @dotnet/area-system-componentmodel-dataannotations |
Contributor
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. |
| { | ||
| vo._asyncValidators[(typeof(TOptions), optionsBuilder.Name)] = async (CancellationToken ct) => | ||
| { | ||
| // Retrieve the options value (already created by sync Validate() call) |
Comment on lines
+100
to
+109
| IAsyncStartupValidator? asyncValidator = Services.GetService<IAsyncStartupValidator>(); | ||
| if (asyncValidator is not null) | ||
| { | ||
| await asyncValidator.ValidateAsync(cancellationToken).ConfigureAwait(false); | ||
| } | ||
| else | ||
| { | ||
| IStartupValidator? validator = Services.GetService<IStartupValidator>(); | ||
| validator?.Validate(); | ||
| } |
Comment on lines
+56
to
+75
| public async Task ValidateAsync(CancellationToken cancellationToken = default) | ||
| { | ||
| // Run sync validators first (this triggers options creation + sync validation) | ||
| Validate(); | ||
|
|
||
| // Then run async validators | ||
| List<Exception>? exceptions = null; | ||
|
|
||
| foreach (Func<CancellationToken, Task> asyncValidator in _validatorOptions._asyncValidators.Values) | ||
| { | ||
| try | ||
| { | ||
| await asyncValidator(cancellationToken).ConfigureAwait(false); | ||
| } | ||
| catch (OptionsValidationException ex) | ||
| { | ||
| exceptions ??= new(); | ||
| exceptions.Add(ex); | ||
| } | ||
| } |
Comment on lines
+42
to
+71
| .Configure<IOptionsMonitor<TOptions>, IEnumerable<IAsyncValidateOptions<TOptions>>>((vo, options, asyncValidators) => | ||
| { | ||
| // Materialize the validators into a list to check if any are registered | ||
| 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); | ||
| } | ||
| }; | ||
| } | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #128100
Implements async startup validation for
Microsoft.Extensions.Optionsas approved in API review.Follow-up to #128656
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