| File: OptionsBuilderExtensions.cs | Web Access |
| Project: src\runtime\src\libraries\Microsoft.Extensions.Options\src\Microsoft.Extensions.Options.csproj (Microsoft.Extensions.Options) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Diagnostics.CodeAnalysis; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; namespace Microsoft.Extensions.DependencyInjection { /// <summary> /// Extension methods for adding configuration-related options services to the DI container via <see cref="OptionsBuilder{TOptions}"/>. /// </summary> public static class OptionsBuilderExtensions { /// <summary> /// Enforces options validation check on start rather than at run time. /// </summary> /// <remarks> /// <para> /// With the built-in <see cref="IOptionsFactory{TOptions}"/>, asynchronous validation runs during startup and /// seeds the built-in <see cref="IOptions{TOptions}"/> and <see cref="IOptionsMonitor{TOptions}"/> instances /// when their caches do not already contain a value. Options requiring asynchronous validation cannot be /// accessed synchronously before startup completes. A value successfully created synchronously before or /// during startup remains the cache winner, and a faulted monitor cache entry causes startup validation to /// fail rather than being replaced. /// </para> /// <para> /// A derived or replacement <see cref="IOptionsFactory{TOptions}"/> uses synchronous startup validation and /// does not invoke <see cref="IAsyncValidateOptions{TOptions}.ValidateAsync"/>. Default-name asynchronous /// validation requires the built-in <see cref="IOptions{TOptions}"/> implementation; startup throws /// <see cref="InvalidOperationException"/> when a custom implementation is registered. The built-in /// <see cref="IOptionsSnapshot{TOptions}"/> validates synchronously in per-scope caches that startup validation /// does not populate. The built-in options monitor also reloads synchronously and does not invoke asynchronous /// validation. The built-in asynchronous validators therefore cause reload to fail and prevent change /// listeners from being notified; no asynchronous last-known-good guarantee is provided. /// </para> /// <para> /// Publication uses <see cref="IOptionsMonitorCache{TOptions}.GetOrAdd"/> so an existing monitor value is not /// replaced. For compatibility, this method exposes the built-in startup validator through /// <see cref="IStartupValidator"/> and <see cref="IAsyncStartupValidator"/> as the same singleton. A custom /// validator registered only as <see cref="IStartupValidator"/> takes precedence and suppresses all /// asynchronous startup validators. New custom startup validators should register only /// <see cref="IAsyncStartupValidator"/>. /// </para> /// </remarks> /// <typeparam name="TOptions">The type of options.</typeparam> /// <param name="optionsBuilder">The <see cref="OptionsBuilder{TOptions}"/> to configure options instance.</param> /// <returns>The <see cref="OptionsBuilder{TOptions}"/> so that additional calls can be chained.</returns> public static OptionsBuilder<TOptions> ValidateOnStart<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TOptions>(this OptionsBuilder<TOptions> optionsBuilder) where TOptions : class { ArgumentNullException.ThrowIfNull(optionsBuilder); string name = optionsBuilder.Name; // Both contracts alias one instance so the host can distinguish the built-in compatibility registration // from independent custom validators without inferring registration identity from implementation type. optionsBuilder.Services.TryAddSingleton<StartupValidator>(); #pragma warning disable SYSLIB0066 // IStartupValidator is obsolete but retained for compatibility. optionsBuilder.Services.TryAddSingleton<IStartupValidator>( static sp => sp.GetRequiredService<StartupValidator>()); #pragma warning restore SYSLIB0066 optionsBuilder.Services.TryAddEnumerable( ServiceDescriptor.Singleton<IAsyncStartupValidator, StartupValidator>( static sp => sp.GetRequiredService<StartupValidator>())); optionsBuilder.Services.AddOptions<StartupValidatorOptions>() .Configure<IOptions<TOptions>, IOptionsMonitor<TOptions>, IOptionsFactory<TOptions>, IOptionsMonitorCache<TOptions>>((vo, options, monitor, factory, sharedCache) => { // Sync path (custom sync-only IStartupValidator): force evaluation through the monitor, // which runs every validator, including an async validator's fail-fast synchronous Validate. vo._validators[(typeof(TOptions), name)] = () => monitor.Get(name); // Async path: run the complete validation (both sync and async validators) for this (type, name) // and seed empty caches so the first synchronous access after startup does not re-run the throwing // synchronous Validate. Values already materialized through a synchronous path remain the winners. vo._asyncValidators[(typeof(TOptions), name)] = async (CancellationToken ct) => { if (factory is OptionsFactory<TOptions> asyncFactory && asyncFactory.GetType() == typeof(OptionsFactory<TOptions>) && asyncFactory.HasAsyncValidators(name)) { UnnamedOptionsManager<TOptions>? optionsManager = null; if (name == Microsoft.Extensions.Options.Options.DefaultName) { optionsManager = options as UnnamedOptionsManager<TOptions> ?? throw new InvalidOperationException( SR.Format( SR.AsyncValidationUnsupportedIOptions, typeof(TOptions), options.GetType())); } TOptions validated = await asyncFactory.CreateAsync(name, ct).ConfigureAwait(false); // A successfully created pre-start IOptions value is used when the monitor cache is empty. // Otherwise, the existing monitor value remains the winner and seeds IOptions if necessary. TOptions winner = sharedCache.GetOrAdd( name, () => optionsManager?.GetOrSetValue(validated) ?? validated); if (optionsManager is not null) { optionsManager.GetOrSetValue(winner); } } else { // Sync-only validation and custom factories use the monitor so an existing cached // instance is preserved and configuration does not run again. monitor.Get(name); } }; }); return optionsBuilder; } } }