Microsoft 的 ASP.NET Core 文档 简要提到 您可以实现
IValidateOptions<TOptions>
以验证来自 appsettings.json 的配置设置,但没有提供完整的示例。 IValidateOptions
的用途是什么?更具体地说:
其实我已经找到了解决办法。我正在发布我的代码,因为此时我在 Stack Overflow 上找不到任何关于
IValidateOptions
的提及。
我最终在添加了选项验证功能的提交中找到了一个如何完成此操作的示例。与 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; }
}
急切验证(启动时快速失败)正在考虑用于未来版本。
ValidateOnStart()
用法:
services.AddOptions<ComplexOptions>()
.Configure(o => o.Boolean = false)
.Validate(o => o.Boolean, "Boolean must be true.")
.ValidateOnStart();
现在可能为时已晚,但为了其他任何偶然发现此问题的人的利益......
在文档部分的底部附近(链接到问题中),出现此行
急切验证(启动时快速失败)正在考虑用于未来版本。
在搜索更多关于此的信息时,我遇到了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();
只需构建一个用于将 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)
{
}
一种方法可能是向您的配置类添加特征
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;
}
}
这是添加验证功能的“特征接口”(在 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 解决方案中重用。