ASP.NET Core 6嵌套配置选项的动态验证问题求解
问题场景
定义了如下嵌套结构的配置选项:
using System.ComponentModel.DataAnnotations; public record AzureOption { public AzureGraphOption? Graph { get; init; } } public record AzureGraphOption { public AzureGraphSecretOption? Secret { get; init; } } public record AzureGraphSecretOption { [Required] public string TenantId { get; init; } [Required] public string ClientId { get; init; } [Required] public string ClientSecret { get; init; } }
注册Azure服务的扩展类:
using Azure.Core; using Azure.Identity; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Microsoft.Graph; public static class AzureServiceExtensions { public static IServiceCollection AddAzureServices(this IServiceCollection services) { services.AddAzureOptions(); services.AddScoped<TokenCredential>(provider => { var azureOption = provider.GetRequiredService<IOptionsMonitor<AzureOption>>().CurrentValue; return new ClientSecretCredential( azureOption.Graph?.Secret?.TenantId, azureOption.Graph?.Secret?.ClientId, azureOption.Graph?.Secret?.ClientSecret, new TokenCredentialOptions { AuthorityHost = AzureAuthorityHosts.AzurePublicCloud }); }); services.AddScoped(provider => new GraphServiceClient(provider.GetService<TokenCredential>(), new[] { "https://graph.microsoft.com/.default" })); return services; } public static IServiceCollection AddAzureOptions(this IServiceCollection services) { services.AddOptions<AzureOption>() .BindConfiguration("Azure") .ValidateDataAnnotations() .ValidateOnStart(); return services; } }
读取秘密配置的扩展:
using Microsoft.Extensions.Configuration; public static class ServiceCollectionExtensions { public static IConfigurationBuilder AddSecretConfig(this IConfigurationBuilder config) { config.AddJsonFile("appsettings.Secret.json", true, true); return config; } }
ASP.NET Core启动代码:
using WebApplication = Microsoft.AspNetCore.Builder.WebApplication; var builder = WebApplication.CreateBuilder(args); builder.Configuration.AddSecretConfig(); builder.Services .AddAzureServices(); var app = builder.Build(); app.Run();
当配置文件中Secret节点为空时:
{ "Azure": { "Graph": { "Secret": { } } } }
应用仍能启动,但Azure:Graph:Secret会被绑定为null,导致[Required]验证未触发,运行时访问azureOption.Graph.Secret.TenantId会抛出空引用异常。手动逐个注册嵌套选项验证过于繁琐,需要无需手动注册的动态验证方案。
解决方案
方法1:修改配置类,强制嵌套对象不可空并添加[Required]
将嵌套属性的可空修饰符去掉,并添加[Required]特性,这样配置绑定过程中如果嵌套节点缺失或为空,会直接触发验证失败,应用启动时就会报错:
using System.ComponentModel.DataAnnotations; public record AzureOption { [Required] public AzureGraphOption Graph { get; init; } } public record AzureGraphOption { [Required] public AzureGraphSecretOption Secret { get; init; } } public record AzureGraphSecretOption { [Required] public string TenantId { get; init; } [Required] public string ClientId { get; init; } [Required] public string ClientSecret { get; init; } }
此时只需保持原有的AddAzureOptions代码即可,ValidateDataAnnotations()会递归验证所有嵌套的必填项,启动时如果配置不符合要求就会直接抛出异常,避免运行时空引用。
方法2:自定义递归验证扩展
如果不想修改配置类的可空性,可以编写一个递归验证的扩展方法,自动遍历所有嵌套属性并验证DataAnnotations:
using System.ComponentModel.DataAnnotations; using Microsoft.Extensions.Options; using System.Reflection; using System.Collections.Generic; public static class OptionsValidationExtensions { public static OptionsBuilder<TOptions> ValidateDataAnnotationsRecursively<TOptions>(this OptionsBuilder<TOptions> builder) where TOptions : class { return builder.Validate(options => { var validationResults = new List<ValidationResult>(); ValidateObjectRecursively(options, validationResults); return validationResults.Count == 0; }); } private static void ValidateObjectRecursively(object obj, List<ValidationResult> validationResults) { if (obj == null) return; var validationContext = new ValidationContext(obj); Validator.TryValidateObject(obj, validationContext, validationResults, validateAllProperties: true); // 递归验证所有引用类型属性(排除字符串) foreach (var property in obj.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance) .Where(p => p.PropertyType.IsClass && p.PropertyType != typeof(string))) { var nestedObj = property.GetValue(obj); ValidateObjectRecursively(nestedObj, validationResults); } } }
然后修改AddAzureOptions方法,替换ValidateDataAnnotations()为自定义的递归验证:
public static IServiceCollection AddAzureOptions(this IServiceCollection services) { services.AddOptions<AzureOption>() .BindConfiguration("Azure") .ValidateDataAnnotationsRecursively() .ValidateOnStart(); return services; }
这个方法会自动遍历所有嵌套的引用类型属性,验证它们的DataAnnotations,即使嵌套属性是可空类型,只要配置中存在该节点(哪怕为空),就会触发其内部的[Required]验证。
总结
- 方法1最直接,通过强制嵌套对象不可空并添加
[Required],让框架自动递归验证,启动时就暴露配置问题。 - 方法2适合无法修改配置类的场景,通过自定义扩展实现递归验证,无需手动注册每个嵌套选项。
内容的提问来源于stack exchange,提问作者Steve Fan
相关产品推荐
相关产品推荐

