微软的ASP.NET Core文档 简单提到 可以实现IValidateOptions<TOptions>
来验证来自appsettings.json的配置设置,但未提供完整的示例。那么 IValidateOptions
应该如何使用呢?更具体地:
- 在哪里连接您的验证器类?
- 如果验证失败,如何记录有用的消息来解释问题?
我已经找到了一个解决方案。由于目前在Stack Overflow上没有任何关于IValidateOptions
的提及,所以我将发布我的代码。
微软的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; }
}
热切的验证(在启动时快速失败)正在考虑在将来的版本中实现。
自 .NET 6 开始,可以使用ValidateOnStart()
实现此功能。
用法:
services.AddOptions<ComplexOptions>()
.Configure(o => o.Boolean = false)
.Validate(o => o.Boolean, "Boolean must be true.")
.ValidateOnStart();
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进行集成,只需构建一个库。
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)
{
}
一种方法是在您的配置类中添加特性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解决方案中可以重复使用。