如何在 ASP.NET Core 2.2 中使用 IValidateOptions 验证配置设置?

Posted

技术标签:

【中文标题】如何在 ASP.NET Core 2.2 中使用 IValidateOptions 验证配置设置?【英文标题】:How to validate configuration settings using IValidateOptions in ASP.NET Core 2.2? 【发布时间】:2019-11-29 19:43:20 【问题描述】:

Microsoft 的 ASP.NET Core 文档 briefly mentions 可以实现 IValidateOptions<TOptions> 以验证 appsettings.json 中的配置设置,但未提供完整示例。 IValidateOptions 打算如何使用?更具体地说:

你在哪里连接你的验证器类? 如何记录有用的消息来解释验证时出现的问题 失败了?

我实际上已经找到了解决方案。我正在发布我的代码,因为此时我在 Stack Overflow 上找不到任何提及 IValidateOptions 的内容。

【问题讨论】:

【参考方案1】:

我最终在commit where the options validation feature was added 中找到了如何完成此操作的示例。与 asp.net core 中的许多东西一样,答案是将您的验证器添加到 DI 容器中,它将自动使用。

通过这种方法,PolygonConfiguration 在验证后进入 DI 容器,并且可以注入到需要它的控制器中。我更喜欢将IOptions<PolygonConfiguration> 注入到我的控制器中。

验证代码似乎在第一次从容器请求PolygonConfiguration 实例时运行(即当控制器被实例化时)。在启动期间尽早验证可能会很好,但我现在对此感到满意。

这是我最终做的:

public class Startup

    public Startup(IConfiguration configuration, ILoggerFactory loggerFactory)
    
        Configuration = configuration;
        Logger = loggerFactory.CreateLogger<Startup>();
    

    public IConfiguration Configuration  get; 
    private ILogger<Startup> Logger  get; 

    public void ConfigureServices(IServiceCollection services)
    
        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

        //Bind configuration settings
        services.Configure<PolygonConfiguration>(Configuration.GetSection(nameof(PolygonConfiguration)));

        //Add validator
        services.AddSingleton<IValidateOptions<PolygonConfiguration>, PolygonConfigurationValidator>();

        //Validate configuration and add to DI container
        services.AddSingleton<PolygonConfiguration>(container =>
        
            try
            
                return container.GetService<IOptions<PolygonConfiguration>>().Value;
            
            catch (OptionsValidationException ex)
            
                foreach (var validationFailure in ex.Failures)
                    Logger.LogError($"appSettings section 'nameof(PolygonConfiguration)' failed validation. Reason: validationFailure");

                throw;
            
        );
    

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    
       ...
    


appSettings.json 包含一些有效和无效的值


  "PolygonConfiguration": 
    "SupportedPolygons": [
      
        "Description": "Triangle",
        "NumberOfSides": 3
      ,
      
        "Description": "Invalid",
        "NumberOfSides": -1
      ,
      
        "Description": "",
        "NumberOfSides": 6
      
    ]
  

验证器类本身

    public class PolygonConfigurationValidator : IValidateOptions<PolygonConfiguration>
    
        public ValidateOptionsResult Validate(string name, PolygonConfiguration options)
        
            if (options is null)
                return ValidateOptionsResult.Fail("Configuration object is null.");

            if (options.SupportedPolygons is null || options.SupportedPolygons.Count == 0)
                return ValidateOptionsResult.Fail($"nameof(PolygonConfiguration.SupportedPolygons) collection must contain at least one element.");

            foreach (var polygon in options.SupportedPolygons)
            
                if (string.IsNullOrWhiteSpace(polygon.Description))
                    return ValidateOptionsResult.Fail($"Property 'nameof(Polygon.Description)' cannot be blank.");

                if (polygon.NumberOfSides < 3)
                    return ValidateOptionsResult.Fail($"Property 'nameof(Polygon.NumberOfSides)' must be at least 3.");
            

            return ValidateOptionsResult.Success;
        
    

以及配置模型

    public class Polygon
    
        public string Description  get; set; 
        public int NumberOfSides  get; set; 
    

    public class PolygonConfiguration
    
        public List<Polygon> SupportedPolygons  get; set; 
    

【讨论】:

它在配置重新加载时有效吗?您是否可能会更新值并且不会发生验证?【参考方案2】:

一种方法是在您的配置类中添加一个特征IValidatable&lt;T&gt;。然后,您可以使用数据注释来定义应该验证的内容和不验证的内容。 我将提供一个示例,说明如何在您的解决方案中添加一个在一般情况下需要注意的辅助项目。

这里有我们要验证的类: Configs/JwtConfig.cs

using System.ComponentModel.DataAnnotations;
using SettingValidation.Traits;

namespace Configs

    public class JwtConfig : IValidatable<JwtConfig>
    
        [Required, StringLength(256, MinimumLength = 32)]
        public string Key  get; set; 
        [Required]
        public string Issuer  get; set;  = string.Empty;
        [Required]
        public string Audience  get; set;  = "*";
        [Range(1, 30)]
        public int ExpireDays  get; set;  = 30;
    


这是添加验证功能的“特征接口”(在 c# 8 中,可以将其更改为具有默认方法的接口) SettingValidation/Traits/IValidatable.cs

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Microsoft.Extensions.Logging;

namespace SettingValidation.Traits

    public interface IValidatable
    
    

    public interface IValidatable<T> : IValidatable
    

    

    public static class IValidatableTrait
    
        public static void Validate(this IValidatable @this, ILogger logger)
        
            var validation = new List<ValidationResult>();
            if (Validator.TryValidateObject(@this, new ValidationContext(@this), validation, validateAllProperties: true))
            
                logger.LogInformation($"@this Correctly validated.");
            
            else
            
                logger.LogError($"@this Failed validation.Environment.NewLinevalidation.Aggregate(new System.Text.StringBuilder(), (sb, vr) => sb.AppendLine(vr.ErrorMessage))");
                throw new ValidationException();
            
        
    


一旦你有了这个,你需要添加一个启动过滤器: SettingValidation/Filters/SettingValidationStartupFilter.cs

using System.Collections.Generic;
using Microsoft.Extensions.Logging;
using SettingValidation.Traits;

namespace SettingValidation.Filters

    public class SettingValidationStartupFilter
    
        public SettingValidationStartupFilter(IEnumerable<IValidatable> validatables, ILogger<SettingValidationStartupFilter> logger)
        
            foreach (var validatable in validatables)
            
                validatable.Validate(logger);
            
        
    


添加扩展方法是惯例:

SettingValidation/Extensions/IServiceCollectionExtensions.cs

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using SettingValidation.Filters;
using SettingValidation.Traits;

namespace SettingValidation.Extensions

    public static class IServiceCollectionExtensions
    

        public static IServiceCollection UseConfigurationValidation(this IServiceCollection services)
        
            services.AddSingleton<SettingValidationStartupFilter>();
            using (var scope = services.BuildServiceProvider().CreateScope())
            
                // Do not remove this call.
                // ReSharper disable once UnusedVariable
                var validatorFilter = scope.ServiceProvider.GetRequiredService<SettingValidationStartupFilter>();
            
            return services;
        

        //
        // Summary:
        //     Registers a configuration instance which TOptions will bind against.
        //
        // Parameters:
        //   services:
        //     The Microsoft.Extensions.DependencyInjection.IServiceCollection to add the services
        //     to.
        //
        //   config:
        //     The configuration being bound.
        //
        // Type parameters:
        //   TOptions:
        //     The type of options being configured.
        //
        // Returns:
        //     The Microsoft.Extensions.DependencyInjection.IServiceCollection so that additional
        //     calls can be chained.
        public static IServiceCollection ConfigureAndValidate<T>(this IServiceCollection services, IConfiguration config)
            where T : class, IValidatable<T>, new()
        
            services.Configure<T>(config);
            services.AddSingleton<IValidatable>(r => r.GetRequiredService<IOptions<T>>().Value);
            return services;
        
    


最后启用启动过滤器的使用 Startup.cs

public class Startup

    public void ConfigureServices(IServiceCollection services)
    
        ...
        services.ConfigureAndValidate<JwtConfig>(Configuration.GetSection("Jwt"));
        services.UseConfigurationValidation();
        ...
    

我记得这段代码来自我现在无法找到的互联网上的一些博客文章,也许它和你找到的一样,即使你不使用这个解决方案,尝试将你所做的重构到另一个项目中,因此它可以在您拥有的其他 ASP.NET Core 解决方案中重复使用。

【讨论】:

【参考方案3】:

现在可能为时已晚,但为了其他偶然发现此问题的人的利益...

在文档部分的底部附近(链接到问题中),此行出现

正在考虑在未来的版本中进行即时验证(启动时快速失败)。

在搜索更多有关这方面的信息时,我遇到了this github issue,它提供了一个 IStartupFilter 和一个 IOptions 的扩展方法(我在下面重复了,以防问题消失)...

此解决方案可确保在应用程序“运行”之前验证选项。

public static class EagerValidationExtensions 
    public static OptionsBuilder<TOptions> ValidateEagerly<TOptions>(this OptionsBuilder<TOptions> optionsBuilder)
        where TOptions : class, new()
    
        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.GetRequiredService(typeof(IOptions<>).MakeGenericType(typeof(T)));
            if (options != null)
            
                var optionsValue = ((IOptions<object>)options).Value;
            

            next(builder);
        ;
    

然后我有一个从 ConfigureServices 中调用的扩展方法,看起来像这样

services
  .AddOptions<SomeOptions>()
  .Configure(options=> options.SomeProperty = "abcd" )
  .Validate(x=>
  
      // do FluentValidation here
  )
  .ValidateEagerly();

【讨论】:

注:此注释为aspnetcore-2.2版本,而不是.NET 5版本的aspnetcore-3【参考方案4】:

只需构建一个库以将 FluentValidation 与 Microsoft.Extensions.Options 集成。

https://github.com/iron9light/FluentValidation.Extensions

nuget 在这里:https://www.nuget.org/packages/IL.FluentValidation.Extensions.Options/

示例:

public class MyOptionsValidator : AbstractValidator<MyOptions> 
    // ...


using IL.FluentValidation.Extensions.Options;

// Registration
services.AddOptions<MyOptions>("optionalOptionsName")
    .Configure(o =>  )
    .Validate<MyOptions, MyOptionsValidator>(); // ❗ Register validator type

// Consumption
var monitor = services.BuildServiceProvider()
    .GetService<IOptionsMonitor<MyOptions>>();

try

    var options = monitor.Get("optionalOptionsName");

catch (OptionsValidationException ex)


【讨论】:

【参考方案5】:

正在考虑在未来的版本中进行即时验证(启动时快速失败)。

从 .NET 6 开始,ValidateOnStart() 可以实现这一点

用法:

services.AddOptions<ComplexOptions>()
  .Configure(o => o.Boolean = false)
  .Validate(o => o.Boolean, "Boolean must be true.")
  .ValidateOnStart();

背景信息:Pull Request: Add Eager Options Validation: ValidateOnStart API

【讨论】:

以上是关于如何在 ASP.NET Core 2.2 中使用 IValidateOptions 验证配置设置?的主要内容,如果未能解决你的问题,请参考以下文章

如何在启用 ASP.NET Core 2.2 EndpointRouting 的情况下使用 RouteDataRequestCultureProvider?

如何使用 Asp.Net Core 2.2 / IdentityServer4 / SP.NET Core Identity 手动散列密码

Asp.Net Core 2.2 - 如何在使用两个 AuthorizationSchemes、JWT 和 cookie 时返回当前用户 ID

如何在asp.net core razor pages 2.2中设置日期格式和文化

如何在 asp net core 2.2 中间件中多次读取请求正文?

这是如何使用 Entity Framework Core 和 ASP.NET Core MVC 2.2+ 和 3.0 创建数据传输对象 (DTO)