繁体   English   中英

在启动期间验证 ASP.NET Core 选项

[英]Validation of ASP.NET Core options during startup

Core2 有一个用于验证从appsettings.json读取的选项的钩子:

services.PostConfigure<MyConfig>(options => {
  // do some validation
  // maybe throw exception if appsettings.json has invalid data
});

此验证代码在第一次使用MyConfig触发,之后每次都触发。 所以我收到多个运行时错误。

然而,在启动期间运行验证更明智- 如果配置验证失败,我希望应用程序立即失败。 文档暗示这就是它的工作方式,但事实并非如此。

那我做对了吗? 如果是这样并且这是设计使然,那么我该如何改变我正在做的事情,让它按照我想要的方式工作?

(另外, PostConfigurePostConfigureAll有什么区别?在这种情况下没有区别,所以我什么时候应该使用任何一个?)

在启动期间没有真正的方法来运行配置验证。 正如您已经注意到的,post configure 动作会像普通的 configure 动作一样在请求选项对象时延迟运行。 这完全是设计使然,并允许许多重要功能,例如在运行时重新加载配置或选项缓存失效。

后配置操作通常用于验证“如果有问题,则抛出异常” ,而是“如果有问题,回退到合理的默认值并使其工作”

例如,身份验证堆栈中有一个后配置步骤,可确保始终为远程身份验证处理程序设置一个SignInScheme

options.SignInScheme = options.SignInScheme ?? _authOptions.DefaultSignInScheme ?? _authOptions.DefaultScheme;

如您所见,这不会失败,而只是提供了多个回退。

从这个意义上说,记住选项和配置实际上是两个独立的东西也很重要。 只是配置是配置选项的常用来源。 因此,有人可能会争辩说,验证配置是否正确实际上并不是选项的工作。

因此,在配置选项之前实际检查启动中的配置可能更有意义。 像这样的东西:

var myOptionsConfiguration = Configuration.GetSection("MyOptions");

if (string.IsNullOrEmpty(myOptionsConfiguration["Url"]))
    throw new Exception("MyOptions:Url is a required configuration");

services.Configure<MyOptions>(myOptionsConfiguration);

当然,这很容易变得非常过分,并且可能会迫使您手动绑定/解析许多属性。 它还将忽略选项模式支持的配置链接(即配置具有多个源/操作的单个选项对象)。

因此,您在这里可以做的是保留您的后期配置操作以进行验证,并在启动期间通过实际请求选项对象简单地触发验证。 例如,您可以简单地将IOptions<MyOptions>添加为Startup.Configure方法的依赖项:

public void Configure(IApplicationBuilder app, IOptions<MyOptions> myOptions)
{
    // all configuration and post configuration actions automatically run

    // …
}

如果您有多个这些选项,您甚至可以将其移动到单独的类型中:

public class OptionsValidator
{
    public OptionsValidator(IOptions<MyOptions> myOptions, IOptions<OtherOptions> otherOptions)
    { }
}

那时,您还可以将逻辑从配置后操作移到OptionsValidator 因此,您可以在应用程序启动过程中显式触发验证:

public void Configure(IApplicationBuilder app, OptionsValidator optionsValidator)
{
    optionsValidator.Validate();

    // …
}

如您所见,对此没有唯一的答案。 你应该考虑你的要求,看看什么对你的情况最有意义。 当然,整个验证仅对某些配置有意义。 特别是,当工作配置在运行时发生变化时,您会遇到困难(您可以使用自定义选项监视器使其工作,但这可能不值得麻烦)。 但是由于大多数自己的应用程序通常只使用缓存的IOptions<T> ,您可能不需要它。


至于PostConfigurePostConfigureAll ,它们都注册了IPostConfigure<TOptions> 区别仅在于前者只会匹配单个命名选项(默认情况下未命名选项——如果您不关心选项名称),而PostConfigureAll将针对所有名称运行。

例如,命名选项用于身份验证堆栈,其中每个身份验证方法都由其方案名称标识。 因此,您可以例如添加多个 OAuth 处理程序并使用PostConfigure("oauth-a", …)配置一个,使用PostConfigure("oauth-b", …)配置另一个,或使用PostConfigureAll(…)配置它们.

在 ASP.NET Core 2.2 项目中,我按照以下步骤进行了急切验证...

给定一个像这样的 Options 类:

public class CredCycleOptions
{
    [Range(1753, int.MaxValue, ErrorMessage = "Please enter a valid integer Number.")]
    public int VerifiedMinYear { get; set; }
    [Range(1753, int.MaxValue, ErrorMessage = "Please enter a valid integer Number.")]
    public int SignedMinYear { get; set; }
    [Range(1753, int.MaxValue, ErrorMessage = "Please enter a valid integer Number.")]
    public int SentMinYear { get; set; }
    [Range(1753, int.MaxValue, ErrorMessage = "Please enter a valid integer Number.")]
    public int ConfirmedMinYear { get; set; }
}

Startup.cs将这些行添加到ConfigureServices方法:

services.AddOptions();

// This will validate Eagerly...
services.ConfigureAndValidate<CredCycleOptions>("CredCycle", Configuration);

ConfigureAndValidate这里的扩展方法。

public static class OptionsExtensions
{
    private static void ValidateByDataAnnotation(object instance, string sectionName)
    {
        var validationResults = new List<ValidationResult>();
        var context = new ValidationContext(instance);
        var valid = Validator.TryValidateObject(instance, context, validationResults);

        if (valid)
            return;

        var msg = string.Join("\n", validationResults.Select(r => r.ErrorMessage));

        throw new Exception($"Invalid configuration for section '{sectionName}':\n{msg}");
    }

    public static OptionsBuilder<TOptions> ValidateByDataAnnotation<TOptions>(
        this OptionsBuilder<TOptions> builder,
        string sectionName)
        where TOptions : class
    {
        return builder.PostConfigure(x => ValidateByDataAnnotation(x, sectionName));
    }

    public static IServiceCollection ConfigureAndValidate<TOptions>(
        this IServiceCollection services,
        string sectionName,
        IConfiguration configuration)
        where TOptions : class
    {
        var section = configuration.GetSection(sectionName);

        services
            .AddOptions<TOptions>()
            .Bind(section)
            .ValidateByDataAnnotation(sectionName)
            .ValidateEagerly();

        return services;
    }

    public static OptionsBuilder<TOptions> ValidateEagerly<TOptions>(this OptionsBuilder<TOptions> optionsBuilder) where TOptions : class
    {
        optionsBuilder.Services.AddTransient<IStartupFilter, StartupOptionsValidation<TOptions>>();

        return optionsBuilder;
    }
}

我在ConfigureAndValidate探测了ValidateEargerly扩展方法。 它从这里使用另一个类:

public class StartupOptionsValidation<T> : IStartupFilter
{
    public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
    {
        return builder =>
        {
            var options = builder.ApplicationServices.GetService(typeof(IOptions<>).MakeGenericType(typeof(T)));

            if (options != null)
            {
                // Retrieve the value to trigger validation
                var optionsValue = ((IOptions<object>)options).Value;
            }

            next(builder);
        };
    }
}

这允许我们向CredCycleOptions添加数据注释,并在应用程序开始时CredCycleOptions获得很好的错误反馈,使其成为理想的解决方案。

在此处输入图片说明

如果某个选项缺失或值错误,我们不希望用户在运行时捕获这些错误。 那将是一次糟糕的经历。

NuGet 包提供了一个ConfigureAndValidate<TOptions>扩展方法,该方法在启动时使用IStartupFilter验证选项。

它基于 Microsoft.Extensions.Options.DataAnnotations。 但与 Microsoft 的包不同的是,它甚至可以验证嵌套属性。 它与 .NET Core 3.1 和 .NET 5 兼容。

文档和源代码 (GitHub)

TL; 博士

  1. 创建您的选项类
  2. 用数据注释装饰您的选项
  3. 在您的IServiceCollection上调用ConfigureAndValidate<T>(Action<T> configureOptions)

ConfigureAndValidate将配置您的选项(调用基本的Configure方法),但还将检查构建的配置是否尊重数据注释,否则在应用程序启动后立即抛出 OptionsValidationException(包含详细信息)。 运行时没有错误配置惊喜!

服务集合扩展

services.ConfigureAndValidate<TOptions>(configureOptions)

语法糖是为了

services
    .AddOptions<TOptions>()
        .Configure(configureOptions) // Microsoft
        .ValidateDataAnnotationsRecursively() // Based on Microsoft's ValidateDataAnnotations, but supports nested properties
        .ValidateEagerly() // Validate on startup
        .Services

选项构建器扩展

以递归方式验证数据注释

此方法注册此选项实例以在第一次依赖注入时验证其 DataAnnotations。 支持嵌套对象。

立即验证

此方法在应用程序启动时而不是在第一次依赖注入时验证此选项实例。

自定义验证

您可以结合自己的选项验证:

services
    .AddOptions<TOptions>()
        .Configure(configureOptions)
        //...
        .Validate(options => { /* custom */ }, message)
        .Validate<TDependency1, TDependency2>((options, dependency1, dependency2) =>
            { 
                // custom validation
            },
            "Custom error message")
        //...
        .ValidateDataAnnotationsRecursively()
        .ValidateEagerly()

Microsoft 选项验证文档

有使用IStartupFilterIValidateOptions进行验证的简单方法。

您可以将 ASP.NET Core 项目的代码放在下面。

public static class OptionsBuilderExtensions
{
    public static OptionsBuilder<TOptions> ValidateOnStartupTime<TOptions>(this OptionsBuilder<TOptions> builder)
        where TOptions : class
    {
        builder.Services.AddTransient<IStartupFilter, OptionsValidateFilter<TOptions>>();
        return builder;
    }
    
    public class OptionsValidateFilter<TOptions> : IStartupFilter where TOptions : class
    {
        private readonly IOptions<TOptions> _options;

        public OptionsValidateFilter(IOptions<TOptions> options)
        {
            _options = options;
        }

        public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
        {
            _ = _options.Value; // Trigger for validating options.
            return next;
        }
    }
}

只需将ValidateOnStartup方法链接到OptionsBuilder<TOptions>

services.AddOptions<SampleOption>()
    .Bind(Configuration)
    .ValidateDataAnnotations()
    .ValidateOnStartupTime();

如果要为选项类创建自定义验证器。 结帐下面的文章。

https://docs.microsoft.com/ko-kr/aspnet/core/fundamentals/configuration/options?view=aspnetcore-5.0#ivalidateoptions-for-complex-validation

下面是一个通用的ConfigureAndValidate方法,用于立即验证和“快速失败”。

总结一下步骤:

  1. 调用serviceCollection.Configure获取您的选项
  2. serviceCollection.BuildServiceProvider().CreateScope()
  3. 使用scope.ServiceProvider.GetRequiredService<IOptions<T>>获取选项实例(记得取.Value
  4. 使用Validator.TryValidateObject验证它
public static class ConfigExtensions
{
    public static void ConfigureAndValidate<T>(this IServiceCollection serviceCollection, Action<T> configureOptions) where T : class, new()
    {
        // Inspired by https://blog.bredvid.no/validating-configuration-in-asp-net-core-e9825bd15f10
        serviceCollection.Configure(configureOptions);

        using (var scope = serviceCollection.BuildServiceProvider().CreateScope())
        {
            var options = scope.ServiceProvider.GetRequiredService<IOptions<T>>();
            var optionsValue = options.Value;
            var configErrors = ValidationErrors(optionsValue).ToArray();
            if (!configErrors.Any())
            {
                return;
            }

            var aggregatedErrors = string.Join(",", configErrors);
            var count = configErrors.Length;
            var configType = typeof(T).FullName;
            throw new ApplicationException($"{configType} configuration has {count} error(s): {aggregatedErrors}");
        }
    }

    private static IEnumerable<string> ValidationErrors(object obj)
    {
        var context = new ValidationContext(obj, serviceProvider: null, items: null);
        var results = new List<ValidationResult>();
        Validator.TryValidateObject(obj, context, results, true);
        foreach (var validationResult in results)
        {
            yield return validationResult.ErrorMessage;
        }
    }
}

这已在 .NET 6 中实现。现在您只需编写以下内容:

services.AddOptions<SampleOption>()
  .Bind(Configuration)
  .ValidateDataAnnotations()
  .ValidateOnStart(); // works in .NET 6

无需外部 NuGet 包或额外代码。

请参阅OptionsBuilderExtensions.ValidateOnStart

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM