Background and motivation
Microsoft.Extensions.Options validation integrates with System.ComponentModel.DataAnnotations through DataAnnotationValidateOptions<T> and ValidateDataAnnotations(), which wire Validator.TryValidateObject into the Options pipeline. This is the most common way .NET Aspire, ASP.NET Core, and other consumers apply DataAnnotations-based validation to configuration types at startup.
#128096 added AsyncValidationAttribute, IAsyncValidatableObject, and Validator.TryValidateObjectAsync to System.ComponentModel.DataAnnotations. #128100 added the async Options pipeline — IAsyncValidateOptions<T>, AsyncValidateOptions<T,...>, and ValidateOnStart async execution — to Microsoft.Extensions.Options.
This proposal completes the bridge: the Microsoft.Extensions.Options.DataAnnotations convenience layer that connects the two. Without DataAnnotationValidateOptionsAsync<T> and ValidateDataAnnotationsAsync(), users who decorate configuration types with AsyncValidationAttribute attributes have no ergonomic path to wire them into Options startup validation. They would need to manually implement IAsyncValidateOptions<T> and call Validator.TryValidateObjectAsync directly — exactly the boilerplate the existing ValidateDataAnnotations() convenience eliminates on the sync side.
Notable consumer: .NET Aspire is a significant consumer of ValidateDataAnnotations() + ValidateOnStart(). The async counterparts directly benefit Aspire-hosted services that need I/O-bound configuration validation (e.g., connection string reachability checks) at startup.
Related: dotnet/runtime#128096, dotnet/runtime#128100, dotnet/aspnetcore#46349
API Proposal
Microsoft.Extensions.Options.DataAnnotations
namespace Microsoft.Extensions.Options;
// New class: async counterpart to DataAnnotationValidateOptions<T>
public class DataAnnotationValidateOptionsAsync<TOptions>
: IAsyncValidateOptions<TOptions> where TOptions : class
{
[RequiresUnreferencedCode("Uses DataAnnotationValidateOptionsAsync which is unsafe given that " +
"the options type passed in when calling ValidateAsync cannot be statically analyzed so its " +
"members may be trimmed.")]
public DataAnnotationValidateOptionsAsync(string? name);
public string? Name { get; }
public Task<ValidateOptionsResult> ValidateAsync(
string? name,
TOptions options,
CancellationToken cancellationToken = default);
}
namespace Microsoft.Extensions.DependencyInjection;
// Existing class — new method added
public static class OptionsBuilderDataAnnotationsExtensions
{
// Existing — unchanged
[RequiresUnreferencedCode("...")]
public static OptionsBuilder<TOptions> ValidateDataAnnotations<TOptions>(
this OptionsBuilder<TOptions> optionsBuilder) where TOptions : class;
// New: async counterpart to ValidateDataAnnotations()
[RequiresUnreferencedCode("Uses DataAnnotationValidateOptionsAsync which is unsafe given that " +
"the options type passed in when calling ValidateAsync cannot be statically analyzed so its " +
"members may be trimmed.")]
public static OptionsBuilder<TOptions> ValidateDataAnnotationsAsync<[DynamicallyAccessedMembers(
DynamicallyAccessedMemberTypes.PublicProperties |
DynamicallyAccessedMemberTypes.NonPublicProperties)] TOptions>(
this OptionsBuilder<TOptions> optionsBuilder) where TOptions : class;
}
API Usage
Scenario 1: Async DataAnnotations at startup
public class TenantDatabaseSettings
{
[Required]
public string TenantName { get; set; } = "";
[Required]
[AsyncConnectionStringValid] // AsyncValidationAttribute: tests DB connectivity
public string ConnectionString { get; set; } = "";
[Range(1, 300)]
public int CommandTimeoutSeconds { get; set; } = 60;
}
// Program.cs
builder.Services.AddOptions<TenantDatabaseSettings>()
.Bind(builder.Configuration.GetSection("Database"))
.ValidateDataAnnotationsAsync() // registers DataAnnotationValidateOptionsAsync<T>
.ValidateOnStart(); // runs async validators at Host.StartAsync()
// What happens at startup:
// Host.StartAsync()
// → IAsyncStartupValidator.ValidateAsync(ct)
// → DataAnnotationValidateOptionsAsync<TenantDatabaseSettings>.ValidateAsync()
// → Validator.TryValidateObjectAsync(settings, ctx, results, true, ct)
// → [Required], [Range] run synchronously (Phase 1)
// → [AsyncConnectionStringValid] runs asynchronously (Phase 2)
// → OptionsValidationException if validation fails → app won't start
Scenario 2: Mixed sync + async DataAnnotations
Sync and async pipelines coexist. Sync validators run inside OptionsFactory.Create() on every IOptions<T>.Value access; async validators run once during Host.StartAsync().
builder.Services.AddOptions<SmtpSettings>()
.Bind(builder.Configuration.GetSection("Smtp"))
.ValidateDataAnnotations() // sync [Required], [Range] — runs in Create()
.ValidateDataAnnotationsAsync() // async [AsyncSmtpReachable] — runs at startup
.ValidateOnStart();
Alternative Designs
Fold into ValidateDataAnnotations()
Merging async registration into the existing sync method was considered and rejected: every existing call to ValidateDataAnnotations() would silently register async startup infrastructure even on options types with zero AsyncValidationAttribute instances — adding a reflection walk at startup for all existing users. The explicit opt-in model (ValidateDataAnnotationsAsync() as a separate call) is consistent with how the rest of #128100 is scoped.
Cross-references
Risks
No response
Background and motivation
Microsoft.Extensions.Optionsvalidation integrates withSystem.ComponentModel.DataAnnotationsthroughDataAnnotationValidateOptions<T>andValidateDataAnnotations(), which wireValidator.TryValidateObjectinto the Options pipeline. This is the most common way .NET Aspire, ASP.NET Core, and other consumers apply DataAnnotations-based validation to configuration types at startup.#128096 added
AsyncValidationAttribute,IAsyncValidatableObject, andValidator.TryValidateObjectAsynctoSystem.ComponentModel.DataAnnotations. #128100 added the async Options pipeline —IAsyncValidateOptions<T>,AsyncValidateOptions<T,...>, andValidateOnStartasync execution — toMicrosoft.Extensions.Options.This proposal completes the bridge: the
Microsoft.Extensions.Options.DataAnnotationsconvenience layer that connects the two. WithoutDataAnnotationValidateOptionsAsync<T>andValidateDataAnnotationsAsync(), users who decorate configuration types withAsyncValidationAttributeattributes have no ergonomic path to wire them into Options startup validation. They would need to manually implementIAsyncValidateOptions<T>and callValidator.TryValidateObjectAsyncdirectly — exactly the boilerplate the existingValidateDataAnnotations()convenience eliminates on the sync side.Notable consumer: .NET Aspire is a significant consumer of
ValidateDataAnnotations()+ValidateOnStart(). The async counterparts directly benefit Aspire-hosted services that need I/O-bound configuration validation (e.g., connection string reachability checks) at startup.Related: dotnet/runtime#128096, dotnet/runtime#128100, dotnet/aspnetcore#46349
API Proposal
Microsoft.Extensions.Options.DataAnnotationsAPI Usage
Scenario 1: Async DataAnnotations at startup
Scenario 2: Mixed sync + async DataAnnotations
Sync and async pipelines coexist. Sync validators run inside
OptionsFactory.Create()on everyIOptions<T>.Valueaccess; async validators run once duringHost.StartAsync().Alternative Designs
Fold into
ValidateDataAnnotations()Merging async registration into the existing sync method was considered and rejected: every existing call to
ValidateDataAnnotations()would silently register async startup infrastructure even on options types with zeroAsyncValidationAttributeinstances — adding a reflection walk at startup for all existing users. The explicit opt-in model (ValidateDataAnnotationsAsync()as a separate call) is consistent with how the rest of #128100 is scoped.Cross-references
AsyncValidationAttribute,IAsyncValidatableObject,Validator.TryValidateObjectAsync(approved, merged Add async validation support for System.ComponentModel.DataAnnotations #128656)IAsyncValidateOptions<T>,AsyncValidateOptions<T,...>,ValidateOnStartasync pipeline (approved)Risks
No response