【问题标题】:Validation of ASP.NET Core options during startup在启动期间验证 ASP.NET Core 选项
【发布时间】:2018-08-05 08:26:03
【问题描述】:

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

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

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

但是在启动期间运行验证更明智 - 如果配置验证失败,我希望应用程序立即失败。 docs imply that 是它的工作原理,但事实并非如此。

那我做对了吗?如果是这样,而且这是设计使然,那么我该如何更改我正在做的事情以使其按我想要的方式工作?

(另外,PostConfigurePostConfigureAll 有什么区别?这种情况下没有区别,那么我应该什么时候使用呢?)

【问题讨论】:

  • 这是一篇反对注入 IOptions 并建议解决方法的文章。 simpleinjector.readthedocs.io/en/latest/…
  • @Nkosi 对此事的看法很有趣,将重新考虑,谢谢。
  • @Nkosi:我非常不同意链接的文档,因为首先IOptions&lt;T&gt; 可以以与使用services.Configure&lt;T&gt;(instanceOfT) 时相同的方式配置,其次IOptionsSnapshot&lt;T&gt; 还提供重新加载设置,而应用程序运行。第三,当有人更改其中的值而请求已经在进行中时,单例设置可能会出现很大问题。这样,请求可能以选项的一个值开始,并可能以不同的值结束。对于IOptions&lt;T&gt;,这不会发生(仅在请求中),因为它有一个作用域的生命周期
  • 如果真的想验证配置,可以在Configure 方法中创建一个范围提供程序,在那里解析并检查/验证配置(即通过解析选项并访问/验证其参数) .当此处发生错误时,应用程序将无法启动
  • @Tseng 同意。我只是提出不同的论点来权衡可用的利弊。配置建议看起来非常好,与提供的答案相似。我关心的一个问题是耦合到框架问题,但这也可以抽象出另一层。

标签: c# validation asp.net-core


【解决方案1】:

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

配置后操作通常用于验证“如果有问题,则抛出异常”,而是“如果有问题,恢复正常的默认设置并使其正常工作”

例如,身份验证堆栈中有一个配置后步骤,可确保始终为远程身份验证处理程序设置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&lt;MyOptions&gt; 添加为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&lt;T&gt;,因此您可能不需要它。


对于PostConfigurePostConfigureAll,它们都注册了IPostConfigure&lt;TOptions&gt;。区别只是前者仅匹配单个 named 选项(默认情况下未命名选项 - 如果您不关心选项名称),而 PostConfigureAll 将针对所有名称运行。

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

【讨论】:

  • 你一开始说不可能,然后继续给我一个非常详细的答案和一个很好的解决方案! :-) 您的OptionsValidator 想法正是我所需要的。谢谢!
  • 您确实提出了一个有趣的观点——如果配置在运行时发生变化怎么办?然后验证应该再次运行。当配置源更改时,您是否知道会触发一个钩子?您说的是自定义选项监视器,但这可能有点矫枉过正,不是吗?我在 docs 网站上查看了类似于 PostConfigure 的内容,在配置更改时会出现,但没有找到任何东西 - 你知道类似的事情吗?
  • 通常,您可能只在任何地方使用IOptions&lt;T&gt;。仅当您使用 IOptionsSnapshot&lt;T&gt; 或较低级别的 IOptionsMonitor&lt;T&gt; 时,您才能获得重新加载配置的支持。如果您确实选择使用配置后操作来实施验证,那么当使用更改的配置重新配置选项时,当然也会运行。但这必然会在运行时运行,而不是在启动时运行。 – 不过在实践中,我没有看到太多用于更改配置的用例,所以也许你甚至不需要考虑这一点。
  • 哦,我说过这是不可能的,因为选项的东西不支持这个(开箱即用)。但这当然并不意味着我们将不得不忍受这种限制,因此我就如何解决它提出了一些想法:)
【解决方案2】:

在一个 ASP.NET Core 2.2 项目中,我按照以下步骤进行 eager 验证...

给定一个像这样的 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);

ConfigureAndValidatehere 的扩展方法。

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 扩展方法。它利用了来自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);
        };
    }
}

这使我们可以向CredCycleOptions 添加数据注释,并在应用开始时立即获得良好的错误反馈,使其成为理想的解决方案。

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

【讨论】:

    【解决方案3】:

    自 2018 年以来,dotnet/runtime issue 已对此进行了讨论。

    在 .NET 6 中,ValidateOnStart 扩展方法已添加到 Microsoft.Extensions.Hosting

    你可以这样使用它:

    services.AddOptions<MyOptions>()
        .ValidateDataAnnotations()
        .ValidateOnStart();                 // Support eager validation
    

    但是,ValidateDataAnnotations 仍然不验证嵌套属性。

    这个NuGet package 提供了一个ConfigureAndValidate&lt;TOptions&gt; 扩展方法,它在启动时验证选项。

    它基于 Microsoft.Extensions.Options.DataAnnotations。但与微软的包不同,它甚至可以验证嵌套属性。

    它与 .NET Standard 2.0、.NET Core 3.1、.NET 5 和 .NET 6 兼容。

    Documentation & source code (GitHub)

    TL;DR

    1. 创建您的选项类
    2. 使用数据注释装饰您的选项
    3. 在您的IServiceCollection 上致电ConfigureAndValidate&lt;T&gt;(Action&lt;T&gt; configureOptions)

    ConfigureAndValidate 将配置您的选项(调用基本的Configure 方法),但还将检查构建的配置是否符合数据注释,否则一旦启动应用程序就会抛出 OptionsValidationException(带有详细信息)。运行时不会出现配置错误!

    使用

    ServiceCollection 扩展

    services.ConfigureAndValidate<TOptions>(configureOptions)
    

    是语法糖

    services
        .AddOptions<TOptions>()
            .Configure(configureOptions) // Microsoft
            .ValidateDataAnnotationsRecursively() // based on Microsoft's ValidateDataAnnotations, but supports nested properties
            .ValidateOnStart() // or ValidateEagerly() in previous versions
            .Services
    

    OptionsBuilder 扩展

    ValidateDataAnnotationsRecursively

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

    ValidateOnStart(或以前版本中的 ValidateEagerly)

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

    自定义验证

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

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

    Microsoft options validation documentation

    【讨论】:

      【解决方案4】:

      使用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&lt;TOptions&gt;

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

      如果您想为选项类创建自定义验证器,请查看 this article

      【讨论】:

        【解决方案5】:

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

        总结一下步骤:

        1. 致电serviceCollection.Configure 了解您的选择
        2. serviceCollection.BuildServiceProvider().CreateScope()
        3. 使用scope.ServiceProvider.GetRequiredService&lt;IOptions&lt;T&gt;&gt; 获取选项实例(记得使用.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;
                }
            }
        }
        

        【讨论】:

          【解决方案6】:

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

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

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

          OptionsBuilderExtensions.ValidateOnStart&lt;TOptions&gt;

          【讨论】:

            猜你喜欢
            • 2019-09-15
            • 2018-11-13
            • 2018-01-15
            • 1970-01-01
            • 2013-12-23
            • 1970-01-01
            • 2018-06-23
            相关资源
            最近更新 更多