簡體   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