Repository navigation
Description
Background and motivation
This proposal originated in a follow-up by @halter73 on #130719 and is split out here for API review.
#130719 approved making IAsyncStartupValidator derive from IStartupValidator. During implementation in #131197, we found that startup validation does not need the same relationship as options validation.
IAsyncValidateOptions<TOptions> must support both synchronous and asynchronous entry points because options can be retrieved synchronously. Host startup orchestration, however, is already asynchronous through Host.StartAsync. Requiring IAsyncStartupValidator implementations to also provide an unused Validate() method creates ambiguity about whether one or both methods run and encourages unnecessary sync-over-async implementations.
This proposal therefore:
- Keeps
IAsyncStartupValidatorindependent fromIStartupValidator. - Obsoletes
IStartupValidatorin favor of the asynchronous contract. - Preserves existing
IStartupValidatorbehavior for compatibility. - Allows all registered
IAsyncStartupValidatorimplementations to run.
The IAsyncValidateOptions<TOptions> : IValidateOptions<TOptions> portion of #130719 is unchanged.
Related discussion: #130719 (comment)
API Proposal
namespace Microsoft.Extensions.Options;
[Obsolete("Implement IAsyncStartupValidator instead.")] // NEW
public partial interface IStartupValidator
{
// EXISTING
// void Validate();
}
// EXISTING: remains independent of IStartupValidator.
// public interface IAsyncStartupValidator
// {
// Task ValidateAsync(CancellationToken cancellationToken = default);
// }ValidateOnStart continues to expose the built-in validator through IStartupValidator for compatibility. Host.StartAsync uses asynchronous startup validation while preserving the precedence of a user-provided legacy IStartupValidator.
Prototype: 4650821
API Usage
Host startup has a single asynchronous entry point:
public sealed class StartupValidator : IAsyncStartupValidator
{
public async Task ValidateAsync(
CancellationToken cancellationToken = default)
{
string state = await File.ReadAllTextAsync(
"startup-state.txt",
cancellationToken);
if (string.IsNullOrWhiteSpace(state))
{
throw new InvalidOperationException(
"The startup state is invalid.");
}
}
}
services.AddSingleton<IAsyncStartupValidator, StartupValidator>();
await host.StartAsync(cancellationToken);Host.StartAsync invokes ValidateAsync; there is no synchronous host startup entry point that invokes Validate. With the inheritance approved in #130719, the same validator would nevertheless be forced to add a synchronous member:
// Required only because IAsyncStartupValidator derives from IStartupValidator.
public void Validate()
{
// Host.StartAsync never calls this member. Duplicating the validation here
// or blocking on ValidateAsync would be unnecessary and potentially unsafe.
throw new NotSupportedException(
"Startup validation is invoked asynchronously.");
}Keeping the interfaces independent makes ValidateAsync the single, complete startup-validation contract and removes any ambiguity over whether Validate, ValidateAsync, or both should run. This differs from IAsyncValidateOptions<TOptions>, which still needs Validate for synchronous options access through APIs such as IOptions<TOptions>.Value.
Alternative Designs
- Make
IAsyncStartupValidatorderive fromIStartupValidator. This was the shape approved in [API Proposal]: Unify sync and async options validation contracts #130719, but it requires an unused synchronous method and makes invocation semantics unclear. - Keep both interfaces without obsoleting
IStartupValidator. This provides no migration signal and allows callers to continue using a path that cannot execute asynchronous validators. - Add
ValidateAsyncdirectly toIStartupValidator. Adding a member to the existing interface would break current implementations.
Risks
- Existing implementers and direct consumers of
IStartupValidatorreceive an obsoletion warning, but retain binary and runtime compatibility. - Host startup must preserve the existing behavior where a custom
IStartupValidatorreplaces the built-in validator. - Running every
IAsyncStartupValidatorregistration differs from the legacy single-service behavior, but better matches normal multi-registration expectations. IAsyncStartupValidatorhas not shipped in a stable release, so keeping it independent is a preview-only compatibility change.
Activity
dotnet-policy-service commentedon Aug 5, 2026
Tagging subscribers to this area: @dotnet/area-extensions-options
See info in area-owners.md if you want to be subscribed.
JakeYallop commentedon Aug 6, 2026
Has the split of
IAsyncStartupValidatorfromIStartupValidatoralready been approved somewhere, or is that what this proposal is about, in addition to the obsoletion? (I'm just curious)bartonjs commentedon Aug 11, 2026
Video
Looks good as proposed. (Using the next available warning number)
Unify sync/async Options validation contract (#131197)