C# 启动期间验证ASP.NET核心选项
Core2有一个钩子,用于验证从C# 启动期间验证ASP.NET核心选项,c#,validation,asp.net-core,C#,Validation,Asp.net Core,Core2有一个钩子,用于验证从appsettings.json读取的选项: services.PostConfigure<MyConfig>(options => { // do some validation // maybe throw exception if appsettings.json has invalid data }); services.PostConfigure(选项=>{ //做一些验证 //如果appsettings.json包含无效数据
appsettings.json
读取的选项:
services.PostConfigure<MyConfig>(options => {
// do some validation
// maybe throw exception if appsettings.json has invalid data
});
services.PostConfigure(选项=>{
//做一些验证
//如果appsettings.json包含无效数据,则可能引发异常
});
此验证代码在首次使用MyConfig时触发,此后每次都会触发。所以我得到了多个运行时错误
不过,在启动期间运行验证更为明智——如果配置验证失败,我希望应用程序立即失败。问题在于它是如何工作的,但事实并非如此
那么我做对了吗?如果是这样,而且这是出于设计,那么我如何才能改变我正在做的事情,使其按照我想要的方式工作
(另外,
PostConfigure
和PostConfigureAll
之间有什么区别?在这种情况下没有区别,所以什么时候应该使用这两种方法?在启动过程中没有真正的方法运行配置验证。正如您已经注意到的,配置后操作与正常配置操作一样,在请求options对象时会延迟运行。这完全取决于设计,并允许许多重要功能,例如在运行时重新加载配置或选项缓存失效
配置后操作通常用于验证的不是“如果出现错误,则抛出异常”,而是“如果出现错误,则返回正常默认值并使其工作”
例如,在身份验证堆栈中有一个post配置步骤,确保始终为远程身份验证处理程序设置了signnscheme
:
options.SignInScheme = options.SignInScheme ?? _authOptions.DefaultSignInScheme ?? _authOptions.DefaultScheme;
正如您所看到的,这不会失败,而是提供了多个回退
从这个意义上讲,记住选项和配置实际上是两个独立的东西也是很重要的。只是配置是配置选项的常用来源。因此,有人可能会说,验证配置是否正确实际上不是选项的工作
因此,在配置选项之前,在启动时实际检查配置可能更有意义。大概是这样的:
var myOptionsConfiguration = Configuration.GetSection("MyOptions");
if (string.IsNullOrEmpty(myOptionsConfiguration["Url"]))
throw new Exception("MyOptions:Url is a required configuration");
services.Configure<MyOptions>(myOptionsConfiguration);
如果您有多个选项,甚至可以将其移动到单独的类型:
public class OptionsValidator
{
public OptionsValidator(IOptions<MyOptions> myOptions, IOptions<OtherOptions> otherOptions)
{ }
}
正如你所看到的,这个问题没有单一的答案。您应该考虑您的需求,看看什么对您的案例最有意义。当然,整个验证只对某些配置有意义。特别是,在运行时更改配置时,您将遇到困难(您可以使用自定义选项监视器来实现这一点,但可能不值得这么麻烦)。但是,由于大多数自己的应用程序通常只使用缓存的IOptions
,您可能不需要这样做
至于
PostConfigure
和PostConfigureAll
,它们都注册了IPostConfigure
。区别在于前者只匹配一个命名选项(如果您不关心选项名称,则默认为未命名选项),而PostConfigureAll
将对所有名称运行
例如,命名选项用于身份验证堆栈,其中每个身份验证方法由其方案名称标识。例如,您可以添加多个OAuth处理程序,并使用
PostConfigure(“OAuth-a”,…)
配置一个,使用PostConfigure(“OAuth-b”,…)
配置另一个,或者使用PostConfigureAll(…)
配置它们。下面是一个通用的配置和验证方法,用于立即验证和“快速失败”
要总结这些步骤,请执行以下操作:
调用serviceCollection。为您的选项配置
执行serviceCollection.BuildServiceProvider().CreateScope()
使用scope.ServiceProvider.GetRequiredService
获取选项实例(请记住获取的.Value
)
使用Validator.TryValidateObject验证它
公共静态类配置扩展
{
public static void ConfigureAndValidate(此IServiceCollection服务集合,操作配置选项),其中T:class,new()
{
//灵感来自https://blog.bredvid.no/validating-configuration-in-asp-net-core-e9825bd15f10
serviceCollection.Configure(配置选项);
使用(var scope=servicecolection.BuildServiceProvider().CreateScope())
{
var options=scope.ServiceProvider.GetRequiredService();
var optionsValue=options.Value;
var configErrors=ValidationErrors(optionsValue).ToArray();
如果(!configErrors.Any())
{
返回;
}
var aggregatedErrors=string.Join(“,”,configErrors);
var count=configErrors.Length;
var configType=typeof(T).FullName;
抛出新的ApplicationException($“{configType}配置有{count}错误:{aggregatedErrors}”);
}
}
私有静态IEnumerable验证错误(对象obj)
{
var context=newvalidationcontext(obj,serviceProvider:null,items:null);
var results=新列表();
TryValidateObject(obj,上下文,结果,true);
foreach(结果中的var validationResult)
{
产生返回validationResult.ErrorMessage;
}
}
}
在一个ASP.NET Core 2.2项目中,我通过以下步骤进行了急切的验证
给定这样一个选项类:
public class CredCycleOptions
{
[Range(1753, int.MaxValue, ErrorMessage = "Please enter a valid integer Number.")]
public int VerifiedMinYear { get; set; }
[Range(1753, int.MaxValue, ErrorMessage = "Please enter a valid integer Number.")]
public int SignedMinYear { get; set; }
[Range(1753, int.MaxValue, ErrorMessage = "Please enter a valid integer Number.")]
public int SentMinYear { get; set; }
[Range(1753, int.MaxValue, ErrorMessage = "Please enter a valid integer Number.")]
public int ConfirmedMinYear { get; set; }
}
在Startup.cs
中,将这些行添加到ConfigureServices
方法:
public void Configure(IApplicationBuilder app, IOptions<MyOptions> myOptions)
{
// all configuration and post configuration actions automatically run
// …
}
services.AddOptions();
// This will validate Eagerly...
services.ConfigureAndValidate<CredCycleOptions>("CredCycle", Configuration);
我在ConfigureAndValidate
内部找到了validateargery
扩展方法。它利用了以下其他类别:
公共类启动选项验证:
public class CredCycleOptions
{
[Range(1753, int.MaxValue, ErrorMessage = "Please enter a valid integer Number.")]
public int VerifiedMinYear { get; set; }
[Range(1753, int.MaxValue, ErrorMessage = "Please enter a valid integer Number.")]
public int SignedMinYear { get; set; }
[Range(1753, int.MaxValue, ErrorMessage = "Please enter a valid integer Number.")]
public int SentMinYear { get; set; }
[Range(1753, int.MaxValue, ErrorMessage = "Please enter a valid integer Number.")]
public int ConfirmedMinYear { get; set; }
}
services.AddOptions();
// This will validate Eagerly...
services.ConfigureAndValidate<CredCycleOptions>("CredCycle", Configuration);
public static class OptionsExtensions
{
private static void ValidateByDataAnnotation(object instance, string sectionName)
{
var validationResults = new List<ValidationResult>();
var context = new ValidationContext(instance);
var valid = Validator.TryValidateObject(instance, context, validationResults);
if (valid)
return;
var msg = string.Join("\n", validationResults.Select(r => r.ErrorMessage));
throw new Exception($"Invalid configuration for section '{sectionName}':\n{msg}");
}
public static OptionsBuilder<TOptions> ValidateByDataAnnotation<TOptions>(
this OptionsBuilder<TOptions> builder,
string sectionName)
where TOptions : class
{
return builder.PostConfigure(x => ValidateByDataAnnotation(x, sectionName));
}
public static IServiceCollection ConfigureAndValidate<TOptions>(
this IServiceCollection services,
string sectionName,
IConfiguration configuration)
where TOptions : class
{
var section = configuration.GetSection(sectionName);
services
.AddOptions<TOptions>()
.Bind(section)
.ValidateByDataAnnotation(sectionName)
.ValidateEagerly();
return services;
}
public static OptionsBuilder<TOptions> ValidateEagerly<TOptions>(this OptionsBuilder<TOptions> optionsBuilder) where TOptions : class
{
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.GetService(typeof(IOptions<>).MakeGenericType(typeof(T)));
if (options != null)
{
// Retrieve the value to trigger validation
var optionsValue = ((IOptions<object>)options).Value;
}
next(builder);
};
}
}