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

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

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

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

ASP.NET Core中的强类型配置

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

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

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

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

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对象asp.net

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。 当程序启动时,就会抛出如下异常。

相关文章
相关标签/搜索