简体   繁体   English

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

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

Core2 has a hook for validating options read from appsettings.json : Core2 有一个用于验证从appsettings.json读取的选项的钩子:

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

This validation code triggers on first use of MyConfig , and every time after that.此验证代码在第一次使用MyConfig触发,之后每次都触发。 So I get multiple runtime errors.所以我收到多个运行时错误。

However it is more sensible to run validation during startup - if config validation fails I want the app to fail immediately.然而,在启动期间运行验证更明智- 如果配置验证失败,我希望应用程序立即失败。 The docs imply that is how it works, but that is not what happens. 文档暗示这就是它的工作方式,但事实并非如此。

So am I doing it right?那我做对了吗? If so and this is by design, then how can I change what I'm doing so it works the way I want?如果是这样并且这是设计使然,那么我该如何改变我正在做的事情,让它按照我想要的方式工作?

(Also, what is the difference between PostConfigure and PostConfigureAll ? There is no difference in this case, so when should I use either one?) (另外, PostConfigurePostConfigureAll有什么区别?在这种情况下没有区别,所以我什么时候应该使用任何一个?)

There is no real way to run a configuration validation during startup.在启动期间没有真正的方法来运行配置验证。 As you already noticed, post configure actions run, just like normal configure actions, lazily when the options object is being requested.正如您已经注意到的,post configure 动作会像普通的 configure 动作一样在请求选项对象时延迟运行。 This completely by design, and allows for many important features, for example reloading configuration during run-time or also options cache invalidation.这完全是设计使然,并允许许多重要功能,例如在运行时重新加载配置或选项缓存失效。

What the post configuration action is usually being used for is not a validation in terms of “if there's something wrong, then throw an exception” , but rather “if there's something wrong, fall back to sane defaults and make it work” .后配置操作通常用于验证“如果有问题,则抛出异常” ,而是“如果有问题,回退到合理的默认值并使其工作”

For example, there's a post configuration step in the authentication stack, that makes sure that there's always a SignInScheme set for remote authentication handlers:例如,身份验证堆栈中有一个后配置步骤,可确保始终为远程身份验证处理程序设置一个SignInScheme

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

As you can see, this will not fail but rather just provides multiple fallbacks.如您所见,这不会失败,而只是提供了多个回退。

In this sense, it's also important to remember that options and configuration are actually two separate things.从这个意义上说,记住选项和配置实际上是两个独立的东西也很重要。 It's just that the configuration is a commonly used source for configuring options.只是配置是配置选项的常用来源。 So one might argue that it is not actually the job of the options to validate that the configuration is correct.因此,有人可能会争辩说,验证配置是否正确实际上并不是选项的工作。

As such it might make more sense to actually check the configuration in the Startup, before configuring the options.因此,在配置选项之前实际检查启动中的配置可能更有意义。 Something like this:像这样的东西:

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

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

services.Configure<MyOptions>(myOptionsConfiguration);

Of course this easily becomes very excessive, and will likely force you to bind/parse many properties manually.当然,这很容易变得非常过分,并且可能会迫使您手动绑定/解析许多属性。 It will also ignore the configuration chaining that the options pattern supports (ie configuring a single options object with multiple sources/actions).它还将忽略选项模式支持的配置链接(即配置具有多个源/操作的单个选项对象)。

So what you could do here is keep your post configuration action for validation, and simply trigger the validation during startup by actually requesting the options object.因此,您在这里可以做的是保留您的后期配置操作以进行验证,并在启动期间通过实际请求选项对象简单地触发验证。 For example, you could simply add IOptions<MyOptions> as a dependency to the Startup.Configure method:例如,您可以简单地将IOptions<MyOptions>添加为Startup.Configure方法的依赖项:

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

    // …
}

If you have multiple of these options, you could even move this into a separate type:如果您有多个这些选项,您甚至可以将其移动到单独的类型中:

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

At that time, you could also move the logic from the post configuration action into that OptionsValidator .那时,您还可以将逻辑从配置后操作移到OptionsValidator So you could trigger the validation explicitly as part of the application startup:因此,您可以在应用程序启动过程中显式触发验证:

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

    // …
}

As you can see, there's no single answer for this.如您所见,对此没有唯一的答案。 You should think about your requirements and see what makes the most sense for your case.你应该考虑你的要求,看看什么对你的情况最有意义。 And of course, this whole validation only makes sense for certain configurations.当然,整个验证仅对某些配置有意义。 In particular, you will have difficulties when working configurations that will change during run-time (you could make this work with a custom options monitor, but it's probably not worth the hassle).特别是,当工作配置在运行时发生变化时,您会遇到困难(您可以使用自定义选项监视器使其工作,但这可能不值得麻烦)。 But as most own applications usually just use cached IOptions<T> , you likely don't need that.但是由于大多数自己的应用程序通常只使用缓存的IOptions<T> ,您可能不需要它。


As for PostConfigure and PostConfigureAll , they both register an IPostConfigure<TOptions> .至于PostConfigurePostConfigureAll ,它们都注册了IPostConfigure<TOptions> The difference is simply that the former will only match a single named option (by default the unnamed option—if you don't care about option names), while PostConfigureAll will run for all names.区别仅在于前者只会匹配单个命名选项(默认情况下未命名选项——如果您不关心选项名称),而PostConfigureAll将针对所有名称运行。

Named options are for example used for the authentication stack, where each authentication method is identified by its scheme name.例如,命名选项用于身份验证堆栈,其中每个身份验证方法都由其方案名称标识。 So you could for example add multiple OAuth handlers and use PostConfigure("oauth-a", …) to configure one and PostConfigure("oauth-b", …) to configure the other, or use PostConfigureAll(…) to configure them both.因此,您可以例如添加多个 OAuth 处理程序并使用PostConfigure("oauth-a", …)配置一个,使用PostConfigure("oauth-b", …)配置另一个,或使用PostConfigureAll(…)配置它们.

On an ASP.NET Core 2.2 project I got this working doing eager validation by following these steps...在 ASP.NET Core 2.2 项目中,我按照以下步骤进行了急切验证...

Given an Options class like this one:给定一个像这样的 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; }
}

In Startup.cs add these lines to ConfigureServices method:Startup.cs将这些行添加到ConfigureServices方法:

services.AddOptions();

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

ConfigureAndValidate is an extension method from here . 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;
    }
}

I plumbed ValidateEargerly extension method right inside ConfigureAndValidate .我在ConfigureAndValidate探测了ValidateEargerly扩展方法。 It makes use of this other class from here :它从这里使用另一个类:

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);
        };
    }
}

This allows us to add data annotations to the CredCycleOptions and get nice error feedback right at the moment the app starts making it an ideal solution.这允许我们向CredCycleOptions添加数据注释,并在应用程序开始时CredCycleOptions获得很好的错误反馈,使其成为理想的解决方案。

在此处输入图片说明

If an option is missing or have a wrong value, we don't want users to catch these errors at runtime.如果某个选项缺失或值错误,我们不希望用户在运行时捕获这些错误。 That would be a bad experience.那将是一次糟糕的经历。

This NuGet package provides a ConfigureAndValidate<TOptions> extension method which validates options at startup using an IStartupFilter .NuGet 包提供了一个ConfigureAndValidate<TOptions>扩展方法,该方法在启动时使用IStartupFilter验证选项。

It is based on based on Microsoft.Extensions.Options.DataAnnotations.它基于 Microsoft.Extensions.Options.DataAnnotations。 But unlike Microsoft's package, it can even validate nested properties.但与 Microsoft 的包不同的是,它甚至可以验证嵌套属性。 It is compatible with .NET Core 3.1 and .NET 5.它与 .NET Core 3.1 和 .NET 5 兼容。

Documentation & source code (GitHub) 文档和源代码 (GitHub)

TL;DR TL; 博士

  1. Create your options class(es)创建您的选项类
  2. Decorate your options with data annotations用数据注释装饰您的选项
  3. Call ConfigureAndValidate<T>(Action<T> configureOptions) on your IServiceCollection在您的IServiceCollection上调用ConfigureAndValidate<T>(Action<T> configureOptions)

ConfigureAndValidate will configure your options (calling the base Configure method), but will also check that the built configuration respects the data annotations, otherwise an OptionsValidationException (with details) is thrown as soon as the application is started. ConfigureAndValidate将配置您的选项(调用基本的Configure方法),但还将检查构建的配置是否尊重数据注释,否则在应用程序启动后立即抛出 OptionsValidationException(包含详细信息)。 No misconfiguration surprise at runtime!运行时没有错误配置惊喜!

Use

ServiceCollection extension服务集合扩展

services.ConfigureAndValidate<TOptions>(configureOptions)

Is syntactic sugar for语法糖是为了

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

OptionsBuilder extensions选项构建器扩展

ValidateDataAnnotationsRecursively以递归方式验证数据注释

This method register this options instance for validation of its DataAnnotations at the first dependency injection.此方法注册此选项实例以在第一次依赖注入时验证其 DataAnnotations。 Nested objects are supported.支持嵌套对象。

ValidateEagerly立即验证

This method validates this options instance at application startup rather than at the first dependency injection.此方法在应用程序启动时而不是在第一次依赖注入时验证此选项实例。

Custom validation自定义验证

You can combine with your own option validations:您可以结合自己的选项验证:

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

Microsoft options validation documentation Microsoft 选项验证文档

There are easy way to validating with using IStartupFilter and IValidateOptions .有使用IStartupFilterIValidateOptions进行验证的简单方法。

You can just put below code your ASP.NET Core project.您可以将 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;
        }
    }
}

And just chain the ValidateOnStartup method on OptionsBuilder<TOptions> .只需将ValidateOnStartup方法链接到OptionsBuilder<TOptions>

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

If you want to create custom validator for options class.如果要为选项类创建自定义验证器。 checkout below article.结帐下面的文章。

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

Below is a generic ConfigureAndValidate method to validate immediately and "fail fast".下面是一个通用的ConfigureAndValidate方法,用于立即验证和“快速失败”。

To summarize the steps:总结一下步骤:

  1. Call serviceCollection.Configure for your options调用serviceCollection.Configure获取您的选项
  2. Do serviceCollection.BuildServiceProvider().CreateScope()serviceCollection.BuildServiceProvider().CreateScope()
  3. Get the options instance with scope.ServiceProvider.GetRequiredService<IOptions<T>> (remember to take the .Value )使用scope.ServiceProvider.GetRequiredService<IOptions<T>>获取选项实例(记得取.Value
  4. Validate it using Validator.TryValidateObject使用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;
        }
    }
}

This has been implemented in .NET 6. Now you can just write the following:这已在 .NET 6 中实现。现在您只需编写以下内容:

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

No need for external NuGet Packages or extra code.无需外部 NuGet 包或额外代码。

See OptionsBuilderExtensions.ValidateOnStart请参阅OptionsBuilderExtensions.ValidateOnStart

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

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