.NET 11 async options validation reaches its RC1 form with an asynchronous path through the established options-validation pipeline. An IAsyncValidateOptions<TOptions> validator can await DNS, secret-store, service-discovery, or other I/O while Host.StartAsync is running. The host can therefore reject an unusable deployment before hosted services start, without hiding .GetAwaiter().GetResult() inside a synchronous IValidateOptions<TOptions> implementation.
The safe pattern is deliberately narrow: keep structural checks synchronous, put the bounded I/O check in ValidateAsync, register the validator through OptionsBuilder<T>.Validate<TValidator>(), and keep the existing ValidateOnStart() call. Treat the check as startup policy, not as continuous health monitoring. .NET 11 RC1 does not make IOptionsSnapshot<T> or configuration reload validation asynchronous.
Table of Contents
Understand the .NET 11 async options validation contract
IAsyncValidateOptions<TOptions> derives from IValidateOptions<TOptions>. An implementation must therefore provide both methods:
Task<ValidateOptionsResult> ValidateAsync(
string? name,
TOptions options,
CancellationToken cancellationToken = default);
ValidateOptionsResult Validate(string? name, TOptions options);
This inheritance preserves the existing registration and discovery path. Register an asynchronous validator with Validate<TValidator>() or as IValidateOptions<TOptions>; registering it only as IAsyncValidateOptions<TOptions> is not supported. The public startup call also remains ValidateOnStart(). There is no separate ValidateOnStartAsync() API to add.
At startup, the host detects the asynchronous capability and awaits it. Successful built-in startup validation seeds the shared options-monitor cache before hosted services are resolved. The older IStartupValidator contract is obsolete in RC1 with diagnostic SYSLIB0066; a custom startup validator should implement IAsyncStartupValidator.
This is a .NET 11 RC1 feature, so recheck the release notes and API surface before shipping against a later release. For the established lifetime and reload behavior of IOptions<T>, IOptionsSnapshot<T>, and IOptionsMonitor<T>, see Options Pattern in ASP.NET Core: Validation, Reloads, and Named Options.
Decide whether remote validation belongs on the startup path
An awaited validator makes a remote fact part of application availability. That is useful when starting without the fact would make every request fail—for example, when a configured host does not resolve, a required secret version does not exist, or service discovery cannot produce any endpoint.
It is a poor fit for a dependency that is optional, expected to be temporarily unavailable, or already protected by runtime retry and circuit-breaker policy. Turning a transient downstream outage into a startup failure can create a restart loop across every replica. A validator must therefore have a small timeout, honor cancellation, avoid unbounded retries, and return an actionable message. It must not log credentials or secret values.
Keep deterministic checks such as URI shape, allowed schemes, numeric ranges, and mutually dependent fields in the synchronous path. Reserve asynchronous validation for the smallest remote assertion that changes the start-or-stop decision. Liveness and readiness probes still own continuous runtime health.
Model the option and the probe separately
The example below validates a backend URL and then asks a small probe to resolve its host. The probe abstraction keeps network I/O out of the policy class and makes the startup boundary testable.
using System.Net;
public sealed class BackendOptions
{
public const string SectionName = "Backend";
public string BaseAddress { get; set; } = string.Empty;
public int ValidationTimeoutSeconds { get; set; } = 3;
}
public interface IBackendProbe
{
Task<bool> CanResolveAsync(
string hostName,
CancellationToken cancellationToken);
}
public sealed class DnsBackendProbe : IBackendProbe
{
public async Task<bool> CanResolveAsync(
string hostName,
CancellationToken cancellationToken)
{
IPAddress[] addresses = await Dns.GetHostAddressesAsync(
hostName,
cancellationToken);
return addresses.Length > 0;
}
}
DNS resolution demonstrates genuinely asynchronous work without turning validation into a full business transaction. Do not send a charge, publish a message, rotate a key, or perform another side effect from a validator. Startup validation can run again after a failed start or during tests, so the operation must be safe to repeat.
Implement both validation paths without sync-over-async
The validator repeats its inexpensive shape checks in the asynchronous path, then links the host cancellation token with a local timeout. The synchronous method never blocks on the asynchronous method. For the matching option name, it rejects synchronous materialization after the shape check because the required remote invariant has not been established.
using Microsoft.Extensions.Options;
using System.Net.Sockets;
public sealed class BackendOptionsValidator(
IBackendProbe probe) : IAsyncValidateOptions<BackendOptions>
{
public ValidateOptionsResult Validate(
string? name,
BackendOptions options)
{
if (!AppliesToDefaultInstance(name))
{
return ValidateOptionsResult.Skip;
}
string? shapeError = GetShapeError(options, out _);
if (shapeError is not null)
{
return ValidateOptionsResult.Fail(shapeError);
}
return ValidateOptionsResult.Fail(
"Backend validation requires the asynchronous startup path. " +
"Resolve these options only after the host has started.");
}
public async Task<ValidateOptionsResult> ValidateAsync(
string? name,
BackendOptions options,
CancellationToken cancellationToken = default)
{
if (!AppliesToDefaultInstance(name))
{
return ValidateOptionsResult.Skip;
}
string? shapeError = GetShapeError(options, out Uri? baseAddress);
if (shapeError is not null)
{
return ValidateOptionsResult.Fail(shapeError);
}
using var timeout = CancellationTokenSource.CreateLinkedTokenSource(
cancellationToken);
timeout.CancelAfter(TimeSpan.FromSeconds(
options.ValidationTimeoutSeconds));
try
{
bool resolved = await probe.CanResolveAsync(
baseAddress!.DnsSafeHost,
timeout.Token);
return resolved
? ValidateOptionsResult.Success
: ValidateOptionsResult.Fail(
"Backend:BaseAddress host returned no DNS addresses.");
}
catch (OperationCanceledException)
when (!cancellationToken.IsCancellationRequested)
{
return ValidateOptionsResult.Fail(
"Backend host validation exceeded its configured timeout.");
}
catch (Exception ex) when (ex is SocketException or ArgumentException)
{
return ValidateOptionsResult.Fail(
$"Backend host validation could not complete: {ex.Message}");
}
}
private static bool AppliesToDefaultInstance(string? name) =>
name is null || name == Options.DefaultName;
private static string? GetShapeError(
BackendOptions options,
out Uri? baseAddress)
{
baseAddress = null;
if (!Uri.TryCreate(
options.BaseAddress,
UriKind.Absolute,
out Uri? parsed) ||
parsed.Scheme != Uri.UriSchemeHttps)
{
return "Backend:BaseAddress must be an absolute HTTPS URL.";
}
if (options.ValidationTimeoutSeconds is < 1 or > 15)
{
return "Backend:ValidationTimeoutSeconds must be between 1 and 15.";
}
baseAddress = parsed;
return null;
}
}
The OperationCanceledException filter distinguishes the validator’s local timeout from host shutdown. Host cancellation is allowed to propagate. The failure text avoids echoing the configured URL, which may contain sensitive query data in a poorly designed deployment.
For named options, replace AppliesToDefaultInstance with an explicit name comparison. Return Skip for names the validator does not own. A null name means the validator may be asked to validate every name; do not silently treat it as an arbitrary concrete name.
Register the validator through OptionsBuilder
The final RC1 registration shape uses the ordinary options builder. Validate<TValidator>() keeps the validator visible through the existing IValidateOptions<BackendOptions> collection while preserving its asynchronous capability.
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.DependencyInjection;
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddSingleton<IBackendProbe, DnsBackendProbe>();
builder.Services
.AddOptions<BackendOptions>()
.BindConfiguration(BackendOptions.SectionName)
.Validate<BackendOptionsValidator>()
.ValidateOnStart();
using IHost host = builder.Build();
await host.StartAsync();
await host.WaitForShutdownAsync();
Do not add both Validate<BackendOptionsValidator>() and a second manual registration for the same validator. Duplicate registration can execute the same remote check more than once. Also avoid resolving IOptions<BackendOptions>.Value while configuring the container or before StartAsync; that takes the synchronous path before the asynchronous startup result can seed the cache.
Prove the startup failure with a deterministic test
The most useful test calls Host.StartAsync rather than invoking the validator directly. That verifies registration, startup ordering, asynchronous dispatch, and failure propagation together. Replace DNS with a fake so the test is deterministic.
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using Xunit;
public sealed class AsyncOptionsStartupTests
{
[Fact]
public async Task StartAsync_rejects_an_unreachable_backend()
{
HostApplicationBuilder builder = Host.CreateApplicationBuilder();
builder.Services.AddSingleton<IBackendProbe>(
new StubBackendProbe(canResolve: false));
builder.Services
.AddOptions<BackendOptions>()
.Configure(options =>
{
options.BaseAddress = "https://backend.example";
options.ValidationTimeoutSeconds = 3;
})
.Validate<BackendOptionsValidator>()
.ValidateOnStart();
using IHost host = builder.Build();
OptionsValidationException exception =
await Assert.ThrowsAsync<OptionsValidationException>(
() => host.StartAsync());
Assert.Contains(
exception.Failures,
failure => failure.Contains("no DNS addresses"));
}
private sealed class StubBackendProbe(bool canResolve)
: IBackendProbe
{
public Task<bool> CanResolveAsync(
string hostName,
CancellationToken cancellationToken) =>
Task.FromResult(canResolve);
}
}
Add a success test with canResolve: true, call StartAsync, and then resolve IOptions<BackendOptions>.Value from host.Services. That confirms the successfully validated value can be consumed after startup. Add separate unit cases for an HTTP URL, a timeout outside the allowed range, a mismatched option name, the local timeout, and caller cancellation.
Run the tests with the .NET 11 RC1 SDK selected by global.json:
dotnet --info
dotnet test --configuration Release
The commands above are the verification procedure. The sample was statically checked against the .NET 11 RC1 release notes and interface contract, but it was not compiled in this writing environment because the .NET SDK is unavailable here. Run the test suite in a pinned RC1 environment before merging application code.
Respect the startup-only boundary
Async options validation in RC1 is intentionally not a general asynchronous options factory. Its built-in support applies to startup validation. Important consequences follow:
IOptionsSnapshot<T>creation remains synchronous.IOptionsMonitor<T>change handling and configuration reload validation remain synchronous.- There is no built-in async reload, last-known-good value, or recovery pipeline.
- Pre-start synchronous access can fail even when the later async check would succeed.
- Custom
IOptionsFactory<T>or options-monitor implementations are outside the built-in async startup-seeding path.
If a value must be refreshed from a remote system, create a separate asynchronous provider with an explicit cache and failure policy. If configuration reload must be validated before adoption, build an application-owned reload coordinator that can await validation and atomically swap the accepted state. Do not assume IOptionsMonitor<T> has acquired those semantics.
Control production failure modes
Before making a remote check a startup gate, answer five operational questions:
- What exactly blocks startup? Prefer a narrow invariant such as DNS resolution or required secret metadata over a complete downstream transaction.
- How long may it delay a rollout? Set a local timeout below the orchestrator’s startup deadline and honor its cancellation token.
- What happens during a regional dependency outage? Decide whether refusing to start is safer than serving degraded traffic. Avoid synchronized retry storms across replicas.
- Is the check repeatable and side-effect free? A failed pod or test host may execute it again.
- How is the failure diagnosed? Return a stable, actionable message and emit safe telemetry, but never include secrets or bearer-bearing URLs.
Roll out the validator to a canary first. Observe startup duration and failure reasons, then expand gradually. Keep readiness separate so an application that starts correctly can still leave traffic when its dependency becomes unhealthy later. If rollback requires removing the async validator, make that a code/configuration rollback rather than a runtime switch that silently bypasses a required invariant.
Use a focused adoption checklist
Before enabling .NET 11 async options validation in a production service:
- Pin and verify the .NET 11 RC1 SDK and runtime used by CI and deployment.
- Implement both
ValidateandValidateAsync; never block the synchronous method. - Register through
Validate<TValidator>()orIValidateOptions<TOptions>, not only through the async interface. - Keep
ValidateOnStart()and callHost.StartAsyncin the integration test. - Put deterministic shape checks in both paths.
- Bound remote I/O with timeout and cancellation, without hidden retries.
- Return
Skipfor option names the validator does not own. - Resolve validated options only after successful startup.
- Document that snapshot and reload paths are still synchronous.
- Recheck the API and release notes when moving from RC1 to a later .NET 11 build.
.NET 11’s asynchronous validator removes the need to fake asynchronous I/O inside a synchronous options contract. It does not remove the need to choose the right availability policy. A small, deterministic, time-bounded startup assertion can prevent a broken deployment from accepting traffic; an expansive remote dependency check can instead turn a recoverable outage into a fleet-wide boot loop.
References
- .NET 11 RC1 announcement
- .NET Libraries in .NET 11 RC1 release notes
- dotnet/runtime pull request #131197
IAsyncValidateOptions<TOptions>source for release/11.0-rc1- Original asynchronous options validation proposal
Found this useful? Support more practical developer content.