.Net之选项配置校验

前言

之前文章介绍了关于选项配置读取的问题并且也讲解了一些关于模型绑定与验证的内容,但是最近我突然发现了一个好东西可以将两者搭配一起使用,那就是对绑定的配置进行校验,关于如何使用,请随我一起看一下

操作

首先我们创建一个新的API项目.

.Net之选项配置校验

修改appsettings.json增加rabbitmq的配置信息

  "RabbitMQ": {    "Hosts": "195.168.1.10",    "Port": 5672,    "UserName": "admin",    "Password": "123456789",    "VirtualHost": "myQueue"  }

并增加一个类用来绑定rabbitmq的配置

public class RabbitMQConfig{    public const string RabbitMQ = "RabbitMQ";    public string Hosts { get; set; }    public int Port { get; set; }    public string UserName { get; set; }    public string Password { get; set; }    public string VirtualHost { get; set; }}

然后我们就在program中映射配置信息

var builder = WebApplication.CreateBuilder(args);builder.Services.AddControllers();builder.Services.AddEndpointsApiExplorer();builder.Services.AddSwaggerGen();// 配置builder.Services.Configure<RabbitMQConfig>(builder.Configuration.GetSection(RabbitMQConfig.RabbitMQ));var app = builder.Build();// Configure the HTTP request pipeline.if (app.Environment.IsDevelopment()){    app.UseSwagger();    app.UseSwaggerUI();}app.UseAuthorization();app.MapControllers();app.Run();

然后在需要的地方进行读取,下面演示控制器中读取

[ApiController][Route("[controller]")]public class HomeController : ControllerBase{    private readonly RabbitMQConfig _rabbitMQConfig;    public HomeController(IOptions<RabbitMQConfig> options)    {        _rabbitMQConfig = options.Value;    }    [HttpGet]    public int GetConfig()    {        return _rabbitMQConfig.Port;    }}

上面就是一个完整的将配置映射并读取的方案,那么准备是准备好了,下面开始关于校验的内容。

配置在编写的时候并不会自己校验格式,只是一个JSON文件的文本内容,比如上面的配置在生成环境或者其他环境的时候将port的内容无意间配置为了汉字等,那么在你程序走这里逻辑的时候会提示下面的错误信息

Failed to convert configuration value at 'RabbitMQ:Port' to type 'System.Int32'.”

所以我们需要实现的就是让其在项目启动的时候就进行校验,校验不通过则抛出异常。

基础校验

当一些明显的错误,比如上面的将汉字等映射到int类型这种问题,可以通过简单的修改注入配置的方法就可以实现在项目启动的时候就抛出异常

builder.Services.AddOptions<RabbitMQConfig>()    .Bind(builder.Configuration.GetSection(RabbitMQConfig.RabbitMQ))    .ValidateOnStart();

当你启动项目的时候,程序会直接抛出上面的异常来阻止项目启动。当然也可以进行以下自定义的校验规则,例如可以这么编写校验规则

builder.Services.AddOptions<RabbitMQConfig>()    .Bind(builder.Configuration.GetSection(RabbitMQConfig.RabbitMQ))    .Validate(t =>    {        // host 用户名等校验        if (t.Port <= 0 || t.Port > 65535)        {            return false;        }        return true;    })    .ValidateOnStart();

当我们将port端口设置为负数,那么在项目启动的时候会提示:Microsoft.Extensions.Options.OptionsValidationException:“A validation error has occurred.”

通过特性校验

在模型绑定和校验中我们使用类的特性来进行入参的校验以及错误信息的输出,这里我们可以同样使用特性来进行配置的校验,例如

public class RabbitMQConfig{    public const string RabbitMQ = "RabbitMQ";    [Required]    public string Hosts { get; set; }    public int Port { get; set; }    [Required]    public string UserName { get; set; }    [Required]    [MinLength(6, ErrorMessage = "密码长度不能小于6位")]    public string Password { get; set; }    [Required]    public string VirtualHost { get; set; }}

当然我们绑定的配置也要做一些修改操作

builder.Services.AddOptions<RabbitMQConfig>()    .Bind(builder.Configuration.GetSection(RabbitMQConfig.RabbitMQ))    .ValidateDataAnnotations()    .ValidateOnStart();

当我们将密码设置长度小于6位的时候,那么在项目启动的时候提示错误信息:DataAnnotation validation failed for 'RabbitMQConfig' members: 'Password' with the error: '密码长度不能小于6位'.”

FluentValidation

如果在你的项目中使用的是FluentValidation,那么还可以使用它来进行校验,首先我们需要先引入nuget包

<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.5.1" />

然后再注入服务

builder.Services.AddValidatorsFromAssemblyContaining<Program>(ServiceLifetime.Singleton);

关于如何编写可以参考ValidateDataAnnotations的写法,里面大概这个样子

public static OptionsBuilder<TOptions> ValidateDataAnnotations<TOptions>(  this OptionsBuilder<TOptions> optionsBuilder)  where TOptions : class{  optionsBuilder.Services.AddSingleton<IValidateOptions<TOptions>>((IValidateOptions<TOptions>) new DataAnnotationValidateOptions<TOptions>(optionsBuilder.Name));  return optionsBuilder;}public class DataAnnotationValidateOptions<TOptions> : IValidateOptions<TOptions> where TOptions : class{  /// <summary>Constructor.</summary>  /// <param name="name">The name of the option.</param>  public DataAnnotationValidateOptions(string name) => this.Name = name;  /// <summary>The options name.</summary>  public string Name { get; }  /// <summary>  /// Validates a specific named options instance (or all when <paramref name="name" /> is null).  /// </summary>  /// <param name="name">The name of the options instance being validated.</param>  /// <param name="options">The options instance.</param>  /// <returns>The <see cref="T:Microsoft.Extensions.Options.ValidateOptionsResult" /> result.</returns>  public ValidateOptionsResult Validate(string name, TOptions options)  {    if (this.Name != null && !(name == this.Name))      return ValidateOptionsResult.Skip;    List<ValidationResult> validationResultList = new List<ValidationResult>();    if (Validator.TryValidateObject((object) options, new ValidationContext((object) options, (IServiceProvider) null, (IDictionary<object, object>) null), (ICollection<ValidationResult>) validationResultList, true))      return ValidateOptionsResult.Success;    List<string> failures = new List<string>();    foreach (ValidationResult validationResult in validationResultList)      failures.Add("DataAnnotation validation failed for members: '" + string.Join(",", validationResult.MemberNames) + "' with the error: '" + validationResult.ErrorMessage + "'.");    return ValidateOptionsResult.Fail((IEnumerable<string>) failures);  }}

所以我们可以模仿着添加OptionsBuilderDataAnnotationsExtensions文件,并编写下面的代码

public static class OptionsBuilderDataAnnotationsExtensions{    public static OptionsBuilder<TOptions> ValidtaeFluently<TOptions>(this OptionsBuilder<TOptions> optionsBuilder) where TOptions : class    {        optionsBuilder.Services.AddSingleton<IValidateOptions<TOptions>>(t => new FluentValidationOptions<TOptions>(optionsBuilder.Name, t.GetRequiredService<IValidator<TOptions>>()));        return optionsBuilder;    }}public class FluentValidationOptions<TOptions> : IValidateOptions<TOptions> where TOptions : class{    private readonly IValidator<TOptions> _validator;    public string? Name { get; }    public FluentValidationOptions(string? name, IValidator<TOptions> validator)    {        Name = name;        _validator = validator;    }    public ValidateOptionsResult Validate(string name, TOptions options)    {        if (Name != null && Name != name)        {            return ValidateOptionsResult.Skip;        }        ArgumentNullException.ThrowIfNull(options);        var validtaionResult = _validator.Validate(options);        if (validtaionResult.IsValid)        {            return ValidateOptionsResult.Success;        }        var error = validtaionResult.Errors.Select(t => $"属性{t.PropertyName}验证失败,错误信息:{t.ErrorMessage};");        return ValidateOptionsResult.Fail(error);    }}

最后我们修改关于配置绑定的地方

builder.Services.AddOptions<RabbitMQConfig>()    .Bind(builder.Configuration.GetSection(RabbitMQConfig.RabbitMQ))    .ValidtaeFluently()    .ValidateOnStart();

上面一个通用的使用FluentValidation校验配置的方案已经写好了,那么我们编写这次关于rabbtimq的配置校验

public class RabbitMQConfig{    public const string RabbitMQ = "RabbitMQ";    public string Hosts { get; set; }    public int Port { get; set; }    public string UserName { get; set; }    public string Password { get; set; }    public string VirtualHost { get; set; }}public class RabbitMQConfigValidator : AbstractValidator<RabbitMQConfig>{    public RabbitMQConfigValidator()    {        RuleFor(x => x.Password)            .MinimumLength(6)            .WithMessage("密码长度不能小于6位");    }}

上面的校验只是举例了密码长度的限制,当部署的时候,如果我们设置的密码长度小于6位,那么就会抛出异常信息:属性Password验证失败,错误信息:密码长度不能小于6位;”