Skip to content

Unify sync/async Options validation contract - #131197

Merged
oroztocil merged 17 commits into
mainfrom
async-validation-options-contract-rework
Aug 13, 2026
Merged

Unify sync/async Options validation contract#131197
oroztocil merged 17 commits into
mainfrom
async-validation-options-contract-rework

Conversation

@oroztocil

@oroztociloroztocil commented Jul 22, 2026

Copy link
Copy Markdown
Member

Summary

Unifies synchronous and asynchronous options validation under one validator collection while keeping synchronous and asynchronous startup validation as independent compatibility routes.

Fixes#130719
Fixes#131906

Approved contract shape

The two approved rules are intentionally different:

ContractConsumer behaviorCorrect registration
IAsyncValidateOptions<TOptions> : IValidateOptions<TOptions>Enumerate IValidateOptions<TOptions> and cast for asynchronous capabilityRegister as IValidateOptions<TOptions> or through OptionsBuilder<TOptions>.Validate<TValidator>()
Independent IStartupValidator and IAsyncStartupValidator interfacesResolve each startup contract independently and select the applicable routeRegister only as the startup route the implementation is intended to provide

IStartupValidator is now obsolete (SYSLIB0066) in favor of IAsyncStartupValidator. IAsyncStartupValidator remains independent rather than inheriting IStartupValidator, matching the API shape approved in #131906.

Changes

Unified options-validation contract

  • IAsyncValidateOptions<TOptions> now derives from IValidateOptions<TOptions>, dropping its previous contravariance.
  • All validators are registered and enumerated through IValidateOptions<TOptions>. Async paths detect IAsyncValidateOptions<TOptions> as a capability and call ValidateAsync; sync-only validators continue through Validate.
  • Built-in async-delegate validators implement synchronous Validate by returning Skip for nonmatching names or a failed result explaining that the validator requires asynchronous validation. They never perform sync-over-async.
  • OptionsBuilder<TOptions>.Validate<TValidator>() preserves the async capability while applying the builder's options name.
  • DataAnnotations registration preserves synchronous behavior and named-options filtering while participating in the unified collection.

Startup-validation routing

  • ValidateOnStart exposes the built-in startup validator through both compatibility contracts as the same singleton instance.
  • A custom synchronous IStartupValidator retains precedence for compatibility and runs the synchronous startup route.
  • Otherwise, the host runs every registered IAsyncStartupValidator and aggregates validation failures.
  • Startup validation now runs before hosted services are resolved so hosted-service constructors observe a successfully startup-validated value.
  • Failure during early startup no longer causes StopAsync to dereference an unresolved hosted-service collection.

Serving the startup-validated value

  • For the built-in OptionsFactory<TOptions>, ValidateOnStart uses an internal async creation path that invokes each validator exactly once in registration order.
  • A successfully validated value is published into the shared monitor cache when that cache is empty and seeds the built-in IOptions<TOptions> singleton slot for the default name.
  • An options value already materialized through a successful synchronous path remains the cache winner; startup validation does not replace it.
  • Sync-only options retain the existing synchronous creation and caching behavior.

Scope and limitations

This PR provides asynchronous validation during host startup; it does not add asynchronous equivalents for every synchronous options API.

  • IOptions<TOptions>.Value can serve the startup-seeded value after successful ValidateOnStart. Before startup, an async-only validator causes synchronous access to fail explicitly instead of returning an unvalidated value.
  • IOptionsSnapshot<TOptions> remains synchronous. Async-only validators therefore make snapshot creation fail explicitly.
  • Configuration reload through IOptionsMonitor<TOptions> remains synchronous. This PR does not implement asynchronous reload validation, last-known-good retention, or recovery from a faulted reload cache entry.
  • A pre-startup synchronous monitor access that faults is not replaced by later startup validation.
  • Async startup creation is supported only by the built-in OptionsFactory<TOptions>. Custom or derived factories use their existing synchronous creation path.
  • Seeding the default unnamed value requires the built-in IOptions<TOptions> implementation. Replacing it with a custom implementation is not supported for async startup seeding.
  • Registering a validator only as IAsyncValidateOptions<TOptions> is invalid because the options pipeline enumerates IValidateOptions<TOptions>. Use IValidateOptions<TOptions> or OptionsBuilder<TOptions>.Validate<TValidator>(); this PR does not add runtime detection for that static registration error.
  • A custom legacy IStartupValidator intentionally replaces the built-in async startup route. New startup validators should register only as IAsyncStartupValidator.
  • No public asynchronous options factory, snapshot, monitor-get, or reload API is added.

Testing

  • Unified sync/async validator dispatch, ordering, name filtering, and failure aggregation.
  • Startup-validator selection, compatibility aliasing, multiple async validators, cancellation, and exception aggregation.
  • Startup cache seeding, existing cache winners, named options, concurrent access, custom caches, custom options implementations, and derived factories.
  • Explicit behavior for pre-startup access, snapshots, and configuration reload.
  • DataAnnotations registration deduplication and named-options behavior.
  • Failed startup followed by StopAsync.

Note

This PR description was drafted with GitHub Copilot assistance.

Make IAsyncValidateOptions<TOptions> derive from IValidateOptions<TOptions>
so asynchronous validators are enumerated on the synchronous validation path
(no longer silently skipped); their synchronous Validate fails fast with a
clear message. OptionsFactory.CreateAsync dispatches validators by capability.
Rework host startup validation: ValidateOnStart registers the built-in
validator as a single IStartupValidator (back-compat) and as an enumerable
IAsyncStartupValidator (TryAddEnumerable). The host runs a custom synchronous
IStartupValidator when present (it overrides async validation), otherwise runs
every registered IAsyncStartupValidator.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 6e8ab6f8-780c-4890-8e3d-7a7eb497b844
CopilotAI lite review requested due to automatic review settings July 22, 2026 09:22
@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.

CopilotAI 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 updates Microsoft.Extensions.Options and Microsoft.Extensions.Hosting to unify synchronous/asynchronous options validation by making IAsyncValidateOptions<TOptions> derive from IValidateOptions<TOptions>, and adjusts startup validation orchestration to prefer a single async path.

Changes:

  • Unifies async validators into the IValidateOptions<TOptions> collection and adds an async-capable creation path in OptionsFactory<TOptions>.
  • Updates ValidateOnStart registration/orchestration to drive startup validation through IStartupValidator plus an enumerable IAsyncStartupValidator.
  • Updates tests to reflect the single-path startup validation model and new override/precedence behavior.

Reviewed changes

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

Show a summary per file
FileDescription
src/libraries/Microsoft.Extensions.Options/tests/Microsoft.Extensions.Options.Tests/AsyncOptionsValidationTests.csUpdates/options startup validation tests for single-path orchestration and validator precedence; adds new coverage for enumerable async startup validators.
src/libraries/Microsoft.Extensions.Options/src/ValidateOnStart.csRemoves outdated comment implying a two-stage host startup validation model.
src/libraries/Microsoft.Extensions.Options/src/StartupValidatorOptions.csUpdates documentation comment for async startup validation entries (now intended to represent complete validation).
src/libraries/Microsoft.Extensions.Options/src/Resources/Strings.resxAdds a new resource string for sync validation failing when only async validation is supported.
src/libraries/Microsoft.Extensions.Options/src/OptionsFactory.csRefactors sync creation to share helper logic and adds CreateAsync to dispatch per-validator capability.
src/libraries/Microsoft.Extensions.Options/src/OptionsBuilderExtensions.csReworks ValidateOnStart registrations and async validation delegate construction.
src/libraries/Microsoft.Extensions.Options/src/OptionsBuilder.csSwitches async validation registrations to register under IValidateOptions<TOptions>.
src/libraries/Microsoft.Extensions.Options/src/IAsyncValidateOptions.csChanges IAsyncValidateOptions<TOptions> to inherit IValidateOptions<TOptions> (and removes contravariance).
src/libraries/Microsoft.Extensions.Options/src/AsyncValidateOptions.csImplements the inherited synchronous Validate method with a fail-fast message for async-only validators.
src/libraries/Microsoft.Extensions.Options/ref/Microsoft.Extensions.Options.csUpdates public ref surface for IAsyncValidateOptions<TOptions> inheritance and adds Validate to async validator types.
src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/OptionsBuilderExtensionsTests.csAdds hosting-level tests for sync startup validator precedence and multiple async startup validators.
src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.csChanges startup validation ordering and orchestration (custom sync validator takes precedence; otherwise runs async startup validators).

Comment threadsrc/libraries/Microsoft.Extensions.Options/src/OptionsBuilderExtensions.cs Outdated
@oroztociloroztocil changed the title Unify sync/async options validation contractUnify sync/async Options validation contractJul 22, 2026
For an async-validated options type, a synchronous IOptions<T>.Value read
otherwise fails fast because the asynchronous validator's synchronous Validate
is unsupported. Seed the shared monitor cache with the instance validated during
ValidateOnStart and return it from IOptions<T>.Value, so synchronous access
after startup returns the validated value instead of re-running the throwing
synchronous Validate.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 6e8ab6f8-780c-4890-8e3d-7a7eb497b844
CopilotAI review requested due to automatic review settings July 24, 2026 15:50

CopilotAI 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 14 out of 14 changed files in this pull request and generated 3 comments.

Comment threadsrc/libraries/Microsoft.Extensions.Options/src/UnnamedOptionsManager.cs Outdated
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 6e8ab6f8-780c-4890-8e3d-7a7eb497b844
CopilotAI review requested due to automatic review settings July 24, 2026 16:47
@oroztocil
oroztocil marked this pull request as ready for review July 24, 2026 16:48
@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.

CopilotAI 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 14 out of 14 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs:108

  • In Host.StartAsync, the async path only runs Services.GetServices(). If the resolved IStartupValidator also implements IAsyncStartupValidator but is only registered under IStartupValidator, this code enters the async path and then runs zero validators, effectively skipping startup validation. Consider ensuring an async-capable startupValidator still runs when no IAsyncStartupValidator services are registered.
 IStartupValidator? startupValidator = Services.GetService<IStartupValidator>();
if (startupValidator is not null and not IAsyncStartupValidator)
{
// For back-compatibility, a custom IStartupValidator takes precedence and fully controls
// startup validation, overriding any registered IAsyncStartupValidator instances,
// including the one registered by ValidateOnStart.
startupValidator.Validate();
}
else
{

@oroztociloroztocil added the NO-MERGE The PR is not ready for merge yet (see discussion for detailed reasons) label Jul 24, 2026
@oroztocil

Copy link
Copy Markdown
MemberAuthor

I am setting NO-MERGE label until we get approval for the API proposal update sent out by @halter73

@tarekghtarekgh left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Reviewed the unified sync/async validation contract. The core refactor looks correct: registration-order dispatch in OptionsFactory.CreateAsync, running startup validation before hosted services, and failure aggregation in Host.StartAsync. A few reliability and compatibility issues are worth resolving or documenting before merge. The most important are the config-reload behavior and the startup-validator selection in Host.cs.

Comment threadsrc/libraries/Microsoft.Extensions.Options/src/UnnamedOptionsManager.cs Outdated
Comment threadsrc/libraries/Microsoft.Extensions.Options/src/OptionsCache.cs Outdated

@tarekghtarekgh left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Follow-up on items beyond the inline findings above. These are mostly scope and decision items rather than line-level bugs.

API shape for IAsyncStartupValidator is unresolved. The ref surface ships two independent interfaces (IAsyncStartupValidator does not derive from IStartupValidator) and IStartupValidator is not marked [Obsolete]. This matches neither the approved proposal (derive from IStartupValidator) nor the revised proposal on the issue (obsolete IStartupValidator, no inheritance). This needs an API-review decision and the ref should match the outcome.

Startup-validator idempotency. A manual IAsyncStartupValidator.ValidateAsync() followed by the host call re-runs CreateAsync and validates a second time. Consider making the built-in validator idempotent so a user pre-validating does not double-validate at startup.

Breaking-change documentation. The issue carries needs-breaking-change-doc. The silent-skip -> throw behavior change (synchronous access of an async-validated type now fails instead of returning an unvalidated value) needs a preview migration note.

Test coverage. Please add regression tests for: config reload of an async-validated type, IOptionsSnapshot<T> throwing for async-validated types, a custom IOptionsMonitorCache<T>, a custom validator implementing both startup interfaces registered only as IStartupValidator, named async-validated options, and the seed vs GetOrAdd race.

Comment threadsrc/libraries/Microsoft.Extensions.Options/src/OptionsBuilder.cs Outdated
…eplace
Address review feedback on the async options validation contract:
- UnnamedOptionsManager: read the startup-seeded value through the
IOptionsMonitorCache<T> contract (GetOrAdd) so a custom monitor cache is
served, not only the built-in OptionsCache<T>.
- Host startup validation: run the resolved IStartupValidator's Validate()
unless its runtime type is present in the IAsyncStartupValidator collection,
so a validator implementing both interfaces but registered only as
IStartupValidator is no longer silently skipped.
- OptionsCache.AddOrReplace: bounded TryRemove+TryAdd retry on the derived-cache
path so a concurrent GetOrAdd can't make TryAdd a no-op and drop the seeded
value.
Add regression tests for all three.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 6e8ab6f8-780c-4890-8e3d-7a7eb497b844
CopilotAI review requested due to automatic review settings July 24, 2026 23:15

CopilotAI 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 14 out of 14 changed files in this pull request and generated 2 comments.

Comment threadsrc/libraries/Microsoft.Extensions.Options/src/OptionsBuilderExtensions.cs Outdated
…errides
When the resolved IStartupValidator is not async-capable it cannot appear in the
IAsyncStartupValidator collection, so run its Validate() without resolving that
collection at all. This avoids instantiating async validators (and any
constructor/DI side effects) that would be overridden and never run. The
runtime-type match is retained for the case of a validator implementing both
interfaces but registered only as IStartupValidator.
Add a regression test asserting async validators are not resolved when a
sync-only IStartupValidator is present.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 6e8ab6f8-780c-4890-8e3d-7a7eb497b844

CopilotAI 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 30 out of 30 changed files in this pull request and generated no new comments.

Comment threadsrc/libraries/Microsoft.Extensions.Options/src/OptionsBuilderExtensions.cs Outdated
Comment threadsrc/libraries/Microsoft.Extensions.Options/src/OptionsBuilder.cs Outdated
- Preserve the built-in startup validator as one singleton exposed through both startup contracts for compatibility.
- Match the shared compatibility registration by reference instead of inferring registration identity from implementation type.
- Keep independent IStartupValidator registrations on the legacy replacement path without activating suppressed async validators.
- Guide new startup validators toward IAsyncStartupValidator-only registration and retain the required IValidateOptions<T> registration guidance for async options validators.
- Resolve direct framework test consumers through IAsyncStartupValidator while retaining coverage for the obsolete synchronous compatibility path.
- Verify async-validated IOptions<T> values are published before hosted-service construction.
- Cover sync-only, async-only, shared dual-contract, and distinct same-type startup-validator registrations.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings August 12, 2026 14:31

CopilotAI 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 30 out of 30 changed files in this pull request and generated no new comments.

- Document the synchronous fail-fast behavior of built-in asynchronous options validators.
- Explain startup validation seeding and pre-start options access semantics.
- Describe custom IOptions, factory, cache, snapshot, and monitor reload limitations.
- Keep new startup validator registration guidance focused on IAsyncStartupValidator.
- Correct the sync-only startup validator routing description.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings August 12, 2026 16:37

CopilotAI 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 30 out of 30 changed files in this pull request and generated no new comments.

Suppressed comments (1)

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

  • API approval check: issue #130719 is labeled api-approved, and the approved API shape (bartonjs comment on 2026-07-21) has IAsyncStartupValidator : IStartupValidator. This change obsoletes IStartupValidator and (in this PR) keeps IAsyncStartupValidator as an independent interface, which does not match the currently approved proposal. Please either align the public API to the approved shape, or obtain updated API approval for the revised design and then update the ref contract accordingly before merge.
 {
void PostConfigure(string? name, TOptions options);
}
[System.ObsoleteAttribute("Implement IAsyncStartupValidator instead.", DiagnosticId = "SYSLIB0066", UrlFormat = "https://aka.ms/dotnet-warnings/{0}")]
public partial interface IStartupValidator
{
void Validate();

- Preserve existing cache entries during startup validation
- Retain validation failures when infrastructure errors occur
- Simplify validator registration and name metadata
- Add regression coverage and update API documentation
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings August 12, 2026 23:29

CopilotAI 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 29 out of 29 changed files in this pull request and generated no new comments.

Suppressed comments (2)

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

  • The current api-approved issue (#130719) includes an approved API shape comment that has IAsyncStartupValidator : IStartupValidator, but this PR keeps IAsyncStartupValidator independent and instead obsoletes IStartupValidator. Please ensure the approved API shape is updated (or adjust the implementation) before merge so the shipped ref contract matches the approved proposal.
 public interface IAsyncStartupValidator
{

src/libraries/Microsoft.Extensions.Diagnostics/tests/MetricsSubscriptionManagerTests.cs:26

  • This synchronous test blocks on an async startup validation (ValidateAsync().GetAwaiter().GetResult()), which can deadlock under a SynchronizationContext and makes failures harder to diagnose. Prefer making the test async and awaiting startup validation.

// Make sure the subscription manager is started.
serviceProvider.GetRequiredService<IAsyncStartupValidator>().ValidateAsync().GetAwaiter().GetResult();

@tarekghtarekgh left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM, thanks @ViveliDuCh!

Will be good to get @halter73 take one last look.

@ViveliDuChViveliDuCh removed the NO-MERGE The PR is not ready for merge yet (see discussion for detailed reasons) label Aug 13, 2026
CopilotAI review requested due to automatic review settings August 13, 2026 05:53

CopilotAI 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 29 out of 29 changed files in this pull request and generated no new comments.

Suppressed comments (3)

src/libraries/Microsoft.Extensions.Diagnostics/tests/DebugConsoleMetricListenerTests.cs:29

  • This test blocks on ValidateAsync() via GetResult(). If ValidateAsync later performs real async work, sync-over-async here can lead to deadlocks and less readable exceptions. Consider converting the RemoteExecutor.Invoke callback and this test to async so ValidateAsync can be awaited.
 sp.GetRequiredService<IAsyncStartupValidator>().ValidateAsync().GetAwaiter().GetResult();

src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs:167

  • Mixed AggregateException values (e.g., the built-in StartupValidator can aggregate OptionsValidationException plus an unexpected exception) currently get treated as a single unexpected exception and may end up nested inside the host-level AggregateException, which makes failures harder to inspect. Consider flattening AggregateException.InnerExceptions here (continuing only if they’re all OptionsValidationException; otherwise stop after adding them) so the host reports a single, flat set of failures.
 catch (Exception ex)
{
// An unexpected (non-validation) failure stops further validation, but any
// validation failures already collected are retained and reported alongside it.
(validationFailures ??= new()).Add(ex);

src/libraries/Microsoft.Extensions.Diagnostics/tests/MetricsSubscriptionManagerTests.cs:26

  • This test blocks on an async startup validation call using GetResult(). Since xUnit supports async tests, converting this to an async test and awaiting ValidateAsync avoids sync-over-async and produces cleaner failure behavior if ValidateAsync ever does real async work.
 serviceProvider.GetRequiredService<IAsyncStartupValidator>().ValidateAsync().GetAwaiter().GetResult();

@halter73halter73 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

:shipit:

@oroztocil
oroztocil merged commit a3a0683 into mainAug 13, 2026
82 checks passed
@oroztocil
oroztocil deleted the async-validation-options-contract-rework branch August 13, 2026 16:10
@dotnet-milestone-botdotnet-milestone-botBot modified the milestones: 11.0.0, 11.0-rc1Aug 14, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[API Proposal]: Obsolete IStartupValidator in favor of IAsyncStartupValidator [API Proposal]: Unify sync and async options validation contracts

6 participants

@oroztocil@halter73@tarekgh@ViveliDuCh@rosebyte