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

时间:2018-08-05 08:26:03

标签: c# asp.net-core .net-core asp.net-core-2.1 .net-core-2.1

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

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

此验证代码在首次使用MyConfig时以及之后的每次触发时触发。所以我遇到多个运行时错误。

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

所以我做对了吗?如果是这样,并且这是设计使然,那么如何更改正在执行的操作,使其按我想要的方式工作?

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

5 个答案:

答案 0 :(得分:5)

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

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

例如,身份验证堆栈中有一个后配置步骤,可确保始终为远程身份验证处理程序设置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);

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

因此,您在这里可以做的是保留配置后的操作以进行验证,并通过实际请求options对象简单地在启动过程中触发验证。例如,您可以简单地将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>。区别仅在于前者仅匹配单个 named 选项(默认情况下为unnamed选项-如果您不关心选项名称),而PostConfigureAll将针对所有名称运行

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

答案 1 :(得分:3)

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

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

我在ValidateEargerly内部使用了ConfigureAndValidate扩展方法。它利用了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上添加数据注释,并在应用程序启动时立即获得不错的错误反馈。

enter image description here

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

答案 2 :(得分:2)

NuGet package提供了一种// Component A function App() { const apiURL = "https://services9.arcgis.com/M4Su6cnNT6vqupbh/arcgis/rest/services/COVID19_Data/FeatureServer/0/query?where=1%3D1&outFields=*&outSR=4326&f=json"; const apiData = fetch(apiURL) .catch(err=>console.log(err)) .then(data=>console.log(data)); return ( <div> <h1>Hello</h1> <DataVisualize data={apiData}/> </div> ); } // Component B function DataVisualize(props){ return <div>{props.data}</div>; } 扩展方法,可在启动时使用ConfigureAndValidate<TOptions>验证选项。

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

Documentation & source code (GitHub)

安德鲁·洛克(Andrew Lock)解释options validation with IStartupFilter

答案 3 :(得分:1)

下面是一种通用的ConfigureAndValidate方法,可以立即进行验证并“快速失败”。

总结步骤:

  1. 致电serviceCollection.Configure进行选择
  2. 执行serviceCollection.BuildServiceProvider().CreateScope()
  3. 使用scope.ServiceProvider.GetRequiredService<IOptions<T>>获取选项实例(请记住使用.Value
  4. 使用Validator.TryValidateObject
  5. 进行验证
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;
        }
    }
}

答案 4 :(得分:1)

使用 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