C# 启动期间验证ASP.NET核心选项

C# 启动期间验证ASP.NET核心选项,c#,validation,asp.net-core,C#,Validation,Asp.net Core,Core2有一个钩子,用于验证从appsettings.json读取的选项: services.PostConfigure<MyConfig>(options => { // do some validation // maybe throw exception if appsettings.json has invalid data }); services.PostConfigure(选项=>{ //做一些验证 //如果appsettings.json包含无效数据

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

services.PostConfigure<MyConfig>(options => {
  // do some validation
  // maybe throw exception if appsettings.json has invalid data
});
services.PostConfigure(选项=>{
//做一些验证
//如果appsettings.json包含无效数据,则可能引发异常
});
此验证代码在首次使用MyConfig时触发,此后每次都会触发。所以我得到了多个运行时错误

不过,在启动期间运行验证更为明智——如果配置验证失败,我希望应用程序立即失败。问题在于它是如何工作的,但事实并非如此

那么我做对了吗?如果是这样,而且这是出于设计,那么我如何才能改变我正在做的事情,使其按照我想要的方式工作


(另外,
PostConfigure
PostConfigureAll
之间有什么区别?在这种情况下没有区别,所以什么时候应该使用这两种方法?

在启动过程中没有真正的方法运行配置验证。正如您已经注意到的,配置后操作与正常配置操作一样,在请求options对象时会延迟运行。这完全取决于设计,并允许许多重要功能,例如在运行时重新加载配置或选项缓存失效

配置后操作通常用于验证的不是“如果出现错误,则抛出异常”,而是“如果出现错误,则返回正常默认值并使其工作”

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

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);
如果您有多个选项,甚至可以将其移动到单独的类型:

public class OptionsValidator
{
    public OptionsValidator(IOptions<MyOptions> myOptions, IOptions<OtherOptions> otherOptions)
    { }
}
正如你所看到的,这个问题没有单一的答案。您应该考虑您的需求,看看什么对您的案例最有意义。当然,整个验证只对某些配置有意义。特别是,在运行时更改配置时,您将遇到困难(您可以使用自定义选项监视器来实现这一点,但可能不值得这么麻烦)。但是,由于大多数自己的应用程序通常只使用缓存的
IOptions
,您可能不需要这样做


至于
PostConfigure
PostConfigureAll
,它们都注册了
IPostConfigure
。区别在于前者只匹配一个命名选项(如果您不关心选项名称,则默认为未命名选项),而
PostConfigureAll
将对所有名称运行


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

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

要总结这些步骤,请执行以下操作:

  • 调用
    serviceCollection。为您的选项配置
  • 执行
    serviceCollection.BuildServiceProvider().CreateScope()
  • 使用
    scope.ServiceProvider.GetRequiredService
    获取选项实例(请记住获取
    的.Value
  • 使用
    Validator.TryValidateObject验证它
  • 公共静态类配置扩展
    {
    public static void ConfigureAndValidate(此IServiceCollection服务集合,操作配置选项),其中T:class,new()
    {
    //灵感来自https://blog.bredvid.no/validating-configuration-in-asp-net-core-e9825bd15f10
    serviceCollection.Configure(配置选项);
    使用(var scope=servicecolection.BuildServiceProvider().CreateScope())
    {
    var options=scope.ServiceProvider.GetRequiredService();
    var optionsValue=options.Value;
    var configErrors=ValidationErrors(optionsValue).ToArray();
    如果(!configErrors.Any())
    {
    返回;
    }
    var aggregatedErrors=string.Join(“,”,configErrors);
    var count=configErrors.Length;
    var configType=typeof(T).FullName;
    抛出新的ApplicationException($“{configType}配置有{count}错误:{aggregatedErrors}”);
    }
    }
    私有静态IEnumerable验证错误(对象obj)
    {
    var context=newvalidationcontext(obj,serviceProvider:null,items:null);
    var results=新列表();
    TryValidateObject(obj,上下文,结果,true);
    foreach(结果中的var validationResult)
    {
    产生返回validationResult.ErrorMessage;
    }
    }
    }
    
    在一个ASP.NET Core 2.2项目中,我通过以下步骤进行了急切的验证

    给定这样一个选项类:

    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
    方法:

    public void Configure(IApplicationBuilder app, IOptions<MyOptions> myOptions)
    {
        // all configuration and post configuration actions automatically run
    
        // …
    }
    
    services.AddOptions();
    
    // This will validate Eagerly...
    services.ConfigureAndValidate<CredCycleOptions>("CredCycle", Configuration);
    
    我在
    ConfigureAndValidate
    内部找到了
    validateargery
    扩展方法。它利用了以下其他类别:

    公共类启动选项验证:
    
    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; }
    }
    
    services.AddOptions();
    
    // This will validate Eagerly...
    services.ConfigureAndValidate<CredCycleOptions>("CredCycle", Configuration);
    
    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;
        }
    }
    
    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);
            };
        }
    }