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

前言

本篇博客中,我将描述如何在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为例,这里我们需要验证WebhoolUrl和DisplayName属性是否绑定成功,并且我们还需要验证 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);
 }
}

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

最后一步,我们需要将SlackApiSettings 以IValidatable接口的形式注册到依赖注入容器中,这里我们同样可以使用前文的方法解除对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。 当程序启动时,就会抛出以下异常。

原文: Adding validation to strongly typed configuration objects in ASP.NET Core

作者: Andrew Lock

译文: Lamond Lu

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对我们的支持。

(0)

相关推荐

  • Json字符串与Object、List、Map的互转工具类

    package com.cq2022.zago.base.util; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.io.StringWriter; import java.util.List; import java.util.Map; import org.codehaus.jackson.JsonFactory; import org.codehau

  • C++关于构造函数可向父类或者本类传参的讲解

    前面我们学习了C++使用初始化列表的方式来初始化字段的方法: https://www.jb51.net/article/153032.htm 这一节的原理和前面的差不多. 在C++的构造函数中,子类继承父类,那么,在创建一个子类成员时,可以同时向父类或者子类的构造函数进行传参,实现方法如下: 写一个例子:mul_argc.c #include <iostream> #include <cstring> using namespace std ; //英雄联盟类 class Hero

  • 深入浅析Java中Static Class及静态内部类和非静态内部类的不同

    java中的类可以是static吗?答案是可以.在java中我们可以有静态实例变量.静态方法.静态块.类也可以是静态的. java允许我们在一个类里面定义静态类.比如内部类(nested class).把nested class封闭起来的类叫外部类.在java中,我们不能用static修饰顶级类(top level class).只有内部类可以为static.      静态内部类和非静态内部类之间到底有什么不同呢?下面是两者间主要的不同. (1)内部静态类不需要有指向外部类的引用.但非静态内部

  • C语言字符串另类用法的实现

    讲这个例子前,咱们先来看一个简单的程序: 字符串数组实现数字转字母: #include <stdio.h> #include <string.h> int main(void) { int num = 15 ; //26个字母 const char str[] = "abcdefghijklmnopqlstuvwxyz" ; //这个做法是将num这个十进制数通过取模转换为字母并输出,从程序看,即可知道是输出p char a = str[num%26] ; pr

  • C语言结构体数组同时赋值的另类用法

    说到C语言结构体数组的同时赋值,许多人一想就会想到用以下的这种方法,咱们来写一个例子: #include <stdio.h> struct student { int a; int b ; int c ; }; struct student array1[1000] ; int main(void) { int i ; for(i = 0 ; i < 1000 ; i++) { array[i].a = 1 ; array[i].b = 2 ; array[i].c = 3 ; } fo

  • 关于C++内部类的介绍与使用示例

    介绍 1.把一个类定义在另一个类的内部,称里面的类为内部类. 例如: class A { public: class B { public: int x; int y; }; }; 类B即为内部类. 2.内部类和外部类相互没有特权,即外部类无法自由访问内部类,内部类也无法自由访问外部类. a.他们不是朋友关系 b.他们不是父子关系 内部类的使用 #include <stdio.h> class A { public: class B { public: void test() { printf

  • JavaScript类的继承操作实例总结

    本文实例总结了JavaScript类的继承操作.分享给大家供大家参考,具体如下: 一.类式继承 首先要做的是创建构造函数.按惯例,其名称就是类名,首字母应该大写.在构造函数中,创建实例属性要用关键字this .类的方法则被添加到prototype对象中.要创建该类的实例,只需结合关键字new调用这构造函数即可. /* Class Person. */ function Person(name) { this.name = name; } Person.prototype.getName = fu

  • Jetty启动项目中引用json-lib相关类库报错ClassNotFound的解决方案

    今天项目用Jetty启动,在Tomcat下启动正常,放入Jetty容器启动,莫名其妙的报了个ClassNotFoundException: net.sf.json.JSONObject 具体如下: 项目中确实引入了json-lib的jar,开始还以为是版本的问题,后来经过一番资料查询发现,Jetty容器与Maven结合的时候,在jetty发布时,不加载json-lib相关类库,所以导致启动时,项目如果用了json-lib包,也会出现ClassNotFound. 解决方案: 右键项目Run As

  • 关于C++友元类的实现讲解

    C++中的友元既可以实现友元函数,也可以实现友元类,也就是说一个类也可以作为另外一个类的友元.当作为一个类的友元时,它的所有成员函数都是另一个类的友元函数,都可以访问另一个类的私有或者公有成员. 请看实例: #include <iostream> #include <cstring> using namespace std ; //声明教师类 class Techer ; //学生类 class Student { private: string name ; int age ;

  • C++类的分离式写法介绍示例

    介绍 类的分离式写法,使得代码更加规范,增强了阅读性. 分离式写法的规则: 1.类的变量:写在类的里面 2.成员函数:类中写函数的声明,函数的定义写在类体外. 3.写在类外函数定义时,类名前加限定(Object: :),其中,::理解为表示范围的符号. 代码演示 头文件:Object.h #ifndef _OBJECT_H_ #define _OBJECT_H_ class Student { private: char name[32]; int age; public: void SetNa

随机推荐