• 如何为ASP.NET Core的强类型配置对象添加验证


    原文: Adding validation to strongly typed configuration objects in ASP.NET Core
    作者: Andrew Lock
    译文: Lamond Lu

    本篇博客中,我将描述如何在ASP.NET Core程序启动时,确保强类型配置对象正确的绑定成功。通过使用IStartupFilter接口对象,你可以更早的验证你的配置对象是否绑定了正确的值,并不需要等待程序启动之后的某个时间点再验证。

    这里我将简单描述一下ASP.NET Core的配置系统,以及如何使用强类型配置。我将主要描述一下如何去除对IOptions接口的依赖,然后我会描述一下强类型配置对象绑定不正确的问题。最后,我将给出一个在程序启动时验证强类型配置对象的方案。

    ASP.NET Core中的强类型配置

    ASP.NET Core的配置系统非常的灵活,它允许你从多种数据源中读取配置信息,例如Json文件,YAML文件,环境变量,Azure Key Vault等。官方推荐方案是使用强类型配置来获取IConfiguration接口对象的值。

    强类型配置使用POCO对象来呈现你的程序配置的一个子集,这与IConfiguration接口对象存储的原始键值对不同。例如,现在你正在你的程序中集成Slack, 并且使用Web hooks向频道中发送消息,你需要配置Web hook的URL, 以及一些其他的配置。

    public class SlackApiSettings
    {
        public string WebhookUrl { get; set; }
        public string DisplayName { get; set; }
        public bool ShouldNotify { get; set; }
    }
    

    你可以在Startup类中使用扩展方法Configure,将强类型配置对象和你程序配置绑定起来。

    public class Startup
    {
        public Startup(IConfiguration configuration) 
        {
            Configuration = configuration;
        }
    
        public IConfiguration Configuration { get; }
    
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
            services.Configure<SlackApiSettings>(Configuration.GetSection("SlackApi")); 
        }
    
        public void Configure(IApplicationBuilder app)
        {
            app.UseMvc();
        }
    }
    

    当你需要读取配置的时候,你只需要在你当前方法所在类的构造函数中注入一个IOptions接口对象,即可使用这个对象的Value属性,获取到配置的值, 这里ASP.NET Core配置系统自动帮你完成了强类型对象和配置之间的绑定。

    public class TestController : Controller
    {
        private readonly SlackApiSettings _slackApiSettings;
        public TestController(IOptions<SlackApiSettings> options)
        {
            _slackApiSettings = options.Value
        }
    
        public object Get()
        {
            return _slackApiSettings;
        }
    }
    

    解除对IOptions接口的依赖

    可能有些人和我一样,不太喜欢让自己创建的类依赖于IOptions接口,我们只希望自己创建的类仅依赖于配置对象。这里你可以使用如下所述的方法来解除对IOptions接口的依赖。这里我们可以在依赖注入容器中显式的注册一个SlackApiSetting配置对象,并将解析它的方法委托给一个IOptions对象

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc();
    
        services.Configure<SlackApiSettings>(Configuration.GetSection("SlackApi")); 
    
        services.AddSingleton(resolver => 
            resolver.GetRequiredService<IOptions<SlackApiSettings>>().Value);
    }
    

    现在你可以在不引用Microsoft.Extensions.Options程序集的情况下,注入了一个“原始”的配置对象了。

    public class TestController : Controller
    {
        private readonly SlackApiSettings _slackApiSettings;
    
        public TestController(SlackApiSettings settings)
        {
            _slackApiSettings = settings;
        }
    
        public object Get()
        {
            return _slackApiSettings;
        }
    }
    

    这个解决方案通常都非常有效, 但是如果配置出现问题,例如在JSON文件中出现了错误拼写,这里会发生什么事情呢?

    如果绑定失败,程序会发生什么事情?

    我们绑定强类型配置对象的时候有以下几种错误的可能。

    节点名称拼写错误

    当你绑定配置的时候,你需要显式的指定绑定的配置节点名称,如果你当前使用的appsetting.json作为配置文件,json文件中的key即是配置的节点名称。例如下面代码中的"Logging"和“SlackApi”

    {
     "Logging": {
        "LogLevel": {
          "Default": "Warning"
        }
      },
      "AllowedHosts": "*",
      "SlackApi": {
        "WebhookUrl": "http://example.com/test/url",
        "DisplayName": "My fancy bot",
        "ShouldNotify": true
      }
    }
    

    为了绑定"SlackApi"节点的值到强类型配置对象SlackApiSetting, 你需要调用一下代码

    services.Configure<SlackApiSettings>(Configuration.GetSection("SlackApi")); 
    

    这时候,假设我们将appsettings.json中的"SlackApi"错误的拼写为"SackApi"。现在我们去调用前面例子中的TestController中的GET方法,会得到一下结果

    {
        "webhookUrl":null,
        "displayName":null,
        "shouldNotify":false
    }
    

    所有的key都是绑定了他们的默认值,但是没有发生任何错误,这意味着他们绑定到了一个空的配置节点上。这看起来非常糟糕,因为你的代码并没有验证webhookUrl是否是一个合法的Url。

    属性名拼写错误

    相似的,有时候拼写的节点名称正确,但是属性名称可能拼写错误。例如, 我们将appSettings.json文件中的"WebhookUrl"错误的拼写为"Url"。这时我们调用前面例子中的TestController中的GET方法,会得到以下结果

    {
    	"webhookUrl":null,
    	"displayName":"My fancy bot",
    	"shouldNotify":true
    }
    

    强类型配置类的属性缺少SET访问器

    我经常发现一些初级程序员会遇到这个问题,针对属性,他们只提供了GET访问器,而缺少SET访问器,在这种情况下强类型配置对象是不会正确绑定的。

    public class SlackApiSettings
    {
        public string WebhookUrl { get; }
        public string DisplayName { get; }
        public bool ShouldNotify { get; }
    }
    

    现在我们去调用前面例子中的TestController中的GET方法,会得到以下结果

    {
        "webhookUrl":null,
        "displayName":null,
        "shouldNotify":false
    }
    

    不兼容的类型值

    最后一种情况就是将一个不兼容的类型值,绑定到属性上。在配置文件中,所有的配置都是以文本形式保存的,但是绑定器需要将他们转换成.NET中支持的基础类型。例如ShouldNotify属性是一个布尔类型的值,我们只能将"True", "False"字符串绑定到这个值上,但是如果你在配置文件中,设置该属性的值为"THE VALUE", 当程序访问TestController时,程序就会报错

    使用IStartupFilter创建一个配置验证

    为了解决这个问题,我将使用IStartupFilter创建一个在应用启动时运行的简单验证步骤,以确保你的设置正确无误。

    IStartupFilter接口允许你通过向依赖注入容器添加服务来间接控制中间件管道。 ASP.NET Core框架使用它来执行诸如“将IIS中间件添加到应用程序的中间件管道的开头, 或添加诊断中间件之类”的操作。

    虽然IStartupFilter经常用来向管道中添加中间件,但是我们也可以不这么做。相反的,我们可以在程序启动时(服务配置完成之后,处理请求之前),使用它来执行一些简单的代码。

    这里首先我们创建一个简单的接口,强类型配置类可以通过实现这个接口来完成一些必要的验证。

    public interface IValidatable
    {
        void Validate();
    }
    

    下一步,我们创建一个SettingValidationStartupFilter类, 它实现了IStartupFilter接口

    public class SettingValidationStartupFilter : IStartupFilter
    {
        readonly IEnumerable<IValidatable> _validatableObjects;
        public SettingValidationStartupFilter(IEnumerable<IValidatable> validatableObjects)
        {
            _validatableObjects = validatableObjects;
        }
    
        public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
        {
            foreach (var validatableObject in _validatableObjects)
            {
                validatableObject.Validate();
            }
    
            return next;
        }
    }
    

    在构造函数中,我们从依赖注入容器中取出了所有实现IValidatable接口的强类型配置对象,并在Configure方法中依次调用他们的Validate方法。

    SettingValidationStartupFilter并没有修改任何中间件管道, Configure方法中直接返回了next对象。但是如果某个强类型配置类的验证失败,在程序启动时,就会抛出异常,从而阻止了程序。

    接下来我们需要在Startup类中注册我们创建的服务SettingValidationStartupFilter

    public void ConfigureServices(IServiceCollection services)  
    {
        services.AddTransient<IStartupFilter, SettingValidationStartupFilter>()
        // 其他配置
    }
    

    最后你需要让你的配置类实现IValidatable接口, 我们以SlackApiSettings为例,这里我们需要验证WebhoolUrlDisplayName属性是否绑定成功,并且我们还需要验证 WebhoolUrl是否是一个合法的Url。

    public class SlackApiSettings : IValidatable
    {
        public string WebhookUrl { get; set; }
        public string DisplayName { get; set; }
        public bool ShouldNotify { get; set; }
    
        public void Validate()
        {
            if (string.IsNullOrEmpty(WebhookUrl))
            {
                throw new Exception("SlackApiSettings.WebhookUrl must not be null or empty");
            }
    
            if (string.IsNullOrEmpty(DisplayName))
            {
                throw new Exception("SlackApiSettings.WebhookUrl must not be null or empty");
            }
    
            // 如果不是合法的Url,就会抛出异常
            var uri = new Uri(WebhookUrl);
        }
    }
    

    当然我们还可以使用DataAnnotationsAttribute来实现上述验证。

    public class SlackApiSettings : IValidatable
    {
        [Required, Url]
        public string WebhookUrl { get; set; }
        [Required]
        public string DisplayName { get; set; }
        public bool ShouldNotify { get; set; }
    
        public void Validate()
        {
            Validator.ValidateObject(this, 
            	new ValidationContext(this), 
            	validateAllProperties: true);
        }
    }
    

    无论你使用哪一种方式,如果绑定出现问题,程序启动时都会抛出异常。

    最后一步,我们需要将SlackApiSettingsIValidatable接口的形式注册到依赖注入容器中,这里我们同样可以使用前文的方法解除对IOptions接口的依赖。

    public void ConfigureServices(IServiceCollection services)  
    {
        services.AddMvc();
    
        services.AddTransient<IStartupFilter, SettingValidationStartupFilter>()
    
        services.Configure<SlackApiSettings>(Configuration.GetSection("SlackApi")); 
    
        services.AddSingleton(resolver => 
            resolver.GetRequiredService<IOptions<SlackApiSettings>>().Value);
    
        services.AddSingleton<IValidatable>(resolver => 
            resolver.GetRequiredService<IOptions<SlackApiSettings>>().Value);
    }
    

    测试结果

    我们可以任选之前列举的一个错误方式来进行测试,例如,我们将WebhookUrl错误的拼写为Url。 当程序启动时,就会抛出以下异常。

  • 相关阅读:
    学习asp.net完整步骤
    UltraEdit中Matlab语法高亮显示的操作方法
    C#基础完成和深入
    75道程序员面试逻辑思维题
    模拟退火算法解决函数优化问题
    模拟退火算法解决TSP问题
    C#入门
    简单感知器模型解决简单真值表问题
    jQuery 1.3.2 :visible选择器问题
    NUnit2.0详细使用方法
  • 原文地址:https://www.cnblogs.com/lwqlun/p/10084047.html
Copyright © 2020-2023  润新知