Repository navigation
Description
Description
Asynchronous options validation is a new .NET 11 feature (introduced in Preview 6 by dotnet/runtime#128788). Before it shipped in a stable release, .NET 11 RC 1 reworks its contract: Microsoft.Extensions.Options now unifies the synchronous and asynchronous options-validation contracts. IAsyncValidateOptions<TOptions> now derives from IValidateOptions<TOptions>, so asynchronous validators participate in the same validator collection as synchronous ones. As a result, when an options type is validated by an asynchronous-only validator and is accessed through a synchronous creation path, options creation now fails fast with an OptionsValidationException instead of silently returning an unvalidated instance.
This is a prerelease-to-prerelease change that affects code written against the .NET 11 Preview 6/7 async-validation API; it does not affect any stable (RTM) release. Introduced by dotnet/runtime#131197; approved API proposal dotnet/runtime#130719 (originally approved as dotnet/runtime#128100). Related follow-up proposal (not part of this change): dotnet/runtime#131906.
Version
.NET 11 RC 1
Previous behavior
The behavior below is the .NET 11 Preview 6/7 behavior of the then-new async options-validation API, so this migration targets preview adopters of that feature—not consumers of any stable release.
IAsyncValidateOptions<TOptions>was an independent, contravariant (in TOptions) interface, separate fromIValidateOptions<TOptions>. Async validators ran only on the asynchronous startup-validation path.- On synchronous creation paths—
IOptions<TOptions>.Value,IOptionsMonitor<TOptions>.CurrentValue/.Get(name),IOptionsSnapshot<TOptions>.Value, andIOptionsFactory<TOptions>.Create(name)—async validators were not executed. The options instance was returned unvalidated (validation was silently skipped). - A type could implement
IAsyncValidateOptions<TOptions>by providing onlyValidateAsync.
services.AddOptions<MyOptions>()
.Configure(o => o.Value = -1)
.ValidateAsync(o => Task.FromResult(o.Value > 0), "Value must be positive.");
// Synchronous access: The async validator was skipped, so an invalid instance
// was returned silently with no exception.
MyOptions value = provider.GetRequiredService<IOptions<MyOptions>>().Value; // Value == -1New behavior
IAsyncValidateOptions<TOptions>now derives fromIValidateOptions<TOptions>, and theincontravariance is removed.- On a synchronous creation path, the inherited
Validatereturns a failedValidateOptionsResult, soOptionsFactory<TOptions>throwsOptionsValidationException. The message directs you to runValidateOnStartand complete startup before synchronously accessing the options. - The built-in
AsyncValidateOptions<TOptions, ...>types now implement the synchronousValidatemethod (fail-fast). Custom types that implementIAsyncValidateOptions<TOptions>directly must now also implementValidate(inherited fromIValidateOptions<TOptions>).
services.AddOptions<MyOptions>()
.Configure(o => o.Value = -1)
.ValidateAsync(o => Task.FromResult(o.Value > 0), "Value must be positive.");
// Synchronous access now throws instead of returning an unvalidated instance.
MyOptions value = provider.GetRequiredService<IOptions<MyOptions>>().Value;
// Throws OptionsValidationException:
// "Options instance of type 'MyOptions' is validated by an asynchronous validator that
// cannot run during synchronous validation. ... use ValidateOnStart and complete startup
// before synchronously accessing IOptions<TOptions> or IOptionsMonitor<TOptions> ..."Recommended pattern:
services.AddOptions<MyOptions>()
.Configure(o => o.Value = 42)
.ValidateAsync(o => Task.FromResult(o.Value > 0), "Value must be positive.")
.ValidateOnStart(); // Runs async validation during host startup.
using IHost host = builder.Build();
await host.StartAsync(); // Async validation completes here and seeds the validated instance.
// Later synchronous IOptions<MyOptions>.Value access returns the already-validated instance.Type of breaking change
- Behavioral change: Existing binaries might behave differently at run time. Synchronous access of an async-validated options type now throws
OptionsValidationExceptioninstead of returning an unvalidated instance. - Source incompatible:
IAsyncValidateOptions<TOptions>now derives fromIValidateOptions<TOptions>and dropsin TOptionscontravariance. Types that implement the interface directly must add aValidatemethod, and code relying on the contravariant conversion might no longer compile.
A preview binary that implements
IAsyncValidateOptions<TOptions>directly (onlyValidateAsync) can also fail to load against the new assembly until recompiled, so the change is binary-incompatible in that narrow direct-implementer scenario. The common consumer scenario (registering validators through the built-in APIs) is a behavioral change only. All impact is scoped to code built against the .NET 11 Preview 6/7 async API.
Reason for change
Asynchronous options validation was originally approved (dotnet/runtime#128100) as a startup-only, independent async counterpart to IValidateOptions<TOptions>. Implementing post-startup revalidation exposed a correctness gap: Options have synchronous entry points (IOptions<T>.Value, IOptionsSnapshot<T>.Get, IOptionsMonitor<T>.Get, IOptionsFactory<T>.Create), and a validator that implemented only the async interface had no synchronous contract to run through. A value could therefore be returned and cached before any async validation ran, silently bypassing validation. Unifying IAsyncValidateOptions<TOptions> under IValidateOptions<TOptions> gives a single validator collection (preserving registration order and single invocation) and makes the previously silent gap explicit: Synchronous access of an async-validated type now fails fast with an actionable message. Because the async API had not yet reached a stable release, the contract was corrected before shipping. See the approved proposal dotnet/runtime#130719.
Recommended action
- Call
.ValidateOnStart()on the options builder and run async validation during host startup (await host.StartAsync()). This validates and seeds the initialIOptions<TOptions>/IOptionsMonitor<TOptions>instance so later synchronous access succeeds. - Avoid synchronously accessing (
IOptions<T>.Value,IOptionsMonitor<T>.CurrentValue/.Get) an options type that has only asynchronous validators before startup has completed. - Be aware of the paths
ValidateOnStartdoes not cover for async-validated types:IOptionsSnapshot<T>is validated synchronously per scope and is not seeded by startup validation, andIOptionsMonitor<T>recreation after a configuration change is also synchronous. Accessing those paths for an async-only-validated type still throws; keep at least one synchronous validator if you need those paths validated. - If you implement
IAsyncValidateOptions<TOptions>directly, add the now-required synchronousValidate(string?, TOptions)method (returnValidateOptionsResult.Skip, orFailto signal that synchronous validation is unsupported, mirroring the built-inAsyncValidateOptions<TOptions>), and recompile against .NET 11. - If you relied on the removed
in TOptionscontravariance ofIAsyncValidateOptions<TOptions>, adjust the affected code. - There is no
AppContextcompatibility switch; the supported migration isValidateOnStartplus completing startup.
Feature area
Extensions
Affected APIs
Microsoft.Extensions.Options.IAsyncValidateOptions<TOptions>—now derives fromMicrosoft.Extensions.Options.IValidateOptions<TOptions>;in TOptionscontravariance removed.Microsoft.Extensions.Options.AsyncValidateOptions<TOptions>and its dependency overloadsAsyncValidateOptions<TOptions, TDep>throughAsyncValidateOptions<TOptions, TDep1, TDep2, TDep3, TDep4, TDep5>—now exposeValidateOptionsResult Validate(string?, TOptions).- Synchronous options creation/access paths whose runtime behavior changes for async-validated types:
Microsoft.Extensions.Options.IOptions<TOptions>.ValueMicrosoft.Extensions.Options.IOptionsMonitor<TOptions>.CurrentValueMicrosoft.Extensions.Options.IOptionsMonitor<TOptions>.Get(System.String)Microsoft.Extensions.Options.IOptionsSnapshot<TOptions>.ValueMicrosoft.Extensions.Options.IOptionsSnapshot<TOptions>.Get(System.String)Microsoft.Extensions.Options.IOptionsFactory<TOptions>.Create(System.String)Microsoft.Extensions.Options.OptionsFactory<TOptions>.Create(System.String)
- Related registration APIs (async validators now register under
IValidateOptions<TOptions>):Microsoft.Extensions.Options.OptionsBuilder<TOptions>.ValidateAsync(...)andMicrosoft.Extensions.DependencyInjection.OptionsBuilderAsyncValidationExtensions.
Note
This issue was drafted with assistance from GitHub Copilot.
Metadata
Metadata
Labels
Type
Projects
- StatusShow more project fields✅ Done
Activity