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

26

微软的ASP.NET Core文档 简单提到 可以实现IValidateOptions<TOptions> 来验证来自appsettings.json的配置设置,但未提供完整的示例。那么 IValidateOptions 应该如何使用呢?更具体地:

  • 在哪里连接您的验证器类?
  • 如果验证失败,如何记录有用的消息来解释问题?

我已经找到了一个解决方案。由于目前在Stack Overflow上没有任何关于IValidateOptions的提及,所以我将发布我的代码。

5个回答

33

我最终找到了一个示例,展示了如何在添加选项验证功能的提交中完成此操作。与 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; }
    }

它能在配置重新加载时工作吗?您是否可能会更新值而不进行验证? - Siarhei Kavaleuski

19

太棒了 Julian!非常感谢。只是为了澄清,1)向用于绑定配置的类的属性添加数据注释2)创建配置services.Configure<EmailOptions>(config.GetSection(nameof(EmailOptions)));3)使用数据注释验证配置services.AddOptions<EmailOptions>().ValidateDataAnnotations().ValidateOnStart(); - Gerardo Verrone

10
可能现在已经太晚了,但为了其他遇到这个问题的人的利益...
在文档部分的底部(链接在问题中),出现了下面这一行:
“热切的验证(在启动时快速失败)正在考虑在未来的版本中实现。”
在进一步搜索有关此信息时,我发现 此github问题,其中提供了一个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。 - Julian

6

为了将FluentValidation与Microsoft.Extensions.Options进行集成,只需构建一个库。

GitHub代码库在这里: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)
{
}

3

一种方法是在您的配置类中添加特性IValidatable<T>。然后,您可以使用数据注释来定义应该验证什么以及不应该验证什么。

我将提供一个示例,演示如何在解决方案中添加一个辅助项目,以便在一般情况下进行处理。

这里有一个要验证的类: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;
    }
}

这是“trait interface”,它添加了验证功能(在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解决方案中可以重复使用。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接