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

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

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

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

【问题讨论】:

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


    【解决方案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.NewLine}{validation.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

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2018-08-15
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2020-03-20
          • 2020-07-23
          相关资源
          最近更新 更多