You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.11 06:01:01