7.1 Options模式
依赖注入不仅是支撑整个ASP.NET Core框架的基石,也是开发ASP.NET Core应用采用的基本编程模式,所以依赖注入十分重要。依赖注入使我们可以将依赖的功能定义成服务,最终以一种松耦合的形式注入消费该功能的组件或者服务中。除了可以采用依赖注入的形式消费承载某种功能的服务,还可以采用相同的方式消费承载配置数据的Options对象。
7.1.1 将配置绑定为Options对象
Options 模式是一种采用依赖注入来提供 Options 对象的编程方式,但这并不意味着我们会直接利用依赖注入框架来提供Options对象本身,因为利用依赖注入框架获取的是一个能够提供Options对象的 IOptions<TOptions>对象,泛型参数 TOptions表示的正是 Options对象的类型。下面的演示实例利用 IOptions<TOptions>服务来提供我们需要的 Options对象,该对象由一个承载配置数据的IConfiguration对象绑定而成。简单起见,我们依然沿用第6章定义的Profile作为基础的Options类型,下面先回顾相关类型的定义。
下面通过一个简单的控制台应用来演示Options编程模式。在演示程序中定义了上面这些类型之后,我们创建承载一个 Profile 对象的配置文件 profile.json。如下所示的代码片段就是这个JSON 文件的内容,它提供了构成一个完整 Profile 对象的所有数据。为了使该文件能够在编译后自动复制到输出目录,我们需要将“Copy to Output Directory”属性设置为“Copy Always”。
下面编写代码来演示如何采用 Options 模式获取由配置文件提供的数据绑定生成的 Profile对象。我们调用 AddJsonFile 扩展方法将针对 JSON 配置文件(profile.json)的配置源注册到创建的ConfigurationBuilder对象上,并利用它创建对应的IConfiguration对象。
下面创建一个ServiceCollection对象,在调用AddOptions扩展方法注册Options编程模式的核心服务后,可以将创建的 IConfiguration 对象作为参数调用 Configure<Profile>扩展方法。Configure<TOptions>扩展方法相当于将提供的IConfiguration对象与指定的TOptions类型做了一个映射,在需要提供对应的 TOptions对象时,IConfiguration对象承载的配置数据会被提取出来并绑定生成返回的TOptions对象。
在调用 IServiceCollection 的 BuildServiceProvider 扩展方法得到作为依赖注入容器的IServiceProvider 对象之后,可以直接调用其 GetRequiredService<T>扩展方法来提供IOptions<Profile>对象,该对象的Value属性返回的就是指定IConfiguration对象绑定生成的Profile对象。我们将这个 Profile对象承载的相关数据直接打印在控制台上,输出结果如图 7-1所示,由此可以看出,通过Options模式得到的Profile对象承载的数据完全来源于配置文件。(S701)
图7-1 绑定配置生成的Profile对象
7.1.2 提供具名的Options
针对同一个 Options 类型,IOptions<TOptions>服务在整个应用范围内只能提供一个单一的Options 对象,但是在很多情况下我们需要利用多个同类型的 Options 对象来承载不同的配置。就演示实例中用来表示个人信息的 Profile 类型来说,应用程序中可能会使用它来表示不同用户的信息,如张三、李四和王五。为了解决这个问题,我们可以在添加 IConfiguration 对象与Options 类型映射关系时赋予它们一个唯一标识,这个标识最终会被用来提取对应的 Options 对象。这种具名的Options对象由IOptionsSnapshot<TOptions>接口表示的服务提供。
同样,针对前面的演示实例,假设应用需要采用 Options 模式提取承载不同用户信息的Profile 对象,具体应该如何实现?由于采用 JSON 格式的配置文件来提供原始的用户信息,所以需要将针对多个用户的信息定义在 profile.json 文件中。我们通过如下形式提供了两个用户(foo和bar)的基本信息。
具名 Options 的注册和提取体现在如下所示的代码片段中。在调用 IServiceCollection 接口的Configure<TOptions>扩展方法时,我们将注册的映射关系命名为 foo和 bar,提供原始配置数据的IConfiguration对象也由原来的ConfigurationRoot对象变成它的两个子配置节。
为了使用指定的用户名来提取对应的 Profile 对象,可以利用作为依赖注入容器的IServiceProvider对象得到 IOptionsSnapshot<TOptions>服务,并将用户名作为参数调用其 Get方法得到对应的 Profile 对象。程序运行后,针对两个不同用户的基本信息将以图 7-2 所示的形式输出到控制台上。(S702)
图7-2 根据用户名提取对应的Profile对象
7.1.3 配置源的同步
通过第 6 章的介绍可知,配置模型不仅支持对配置源的监控,还可以在检测到更新之后及时加载新的配置数据,并通过一个 IChangeToken 对象对外发送通知。对于前面演示的两个实例来说,提供的Options对象都是由配置文件提供的数据绑定生成的,如果新的配置数据被重新加载之后能够提供与之匹配的 Options 对象,那么这将是最理想的编程模式,可以通过IOptionsMonitor<TOptions>服务来实现。
前面演示的第一个实例(S701)利用 JSON文件定义了一个单一 Profile对象的信息,下面对它做相应的修改来演示如何监控这个 JSON 文件,并在监测到文件改变之后及时提取新的配置信息生成新的 Profile 对象。如下面的代码片段所示,调用 AddJsonFile 扩展方法注册对应配置源时应将该方法的参数reloadOnChange设置为True,从而开启对对应配置文件的监控功能。
在得到作为依赖注入容器的 IServiceProvider 对象之后,可以利用它得到 IOptionsMonitor<TOptions>服务,该对象会接收到配置系统发出的关于配置被重新加载的通知,并在收到通知后重新生成Options对象。我们调用IOptionsMonitor<TOptions>对象的OnChange方法注册了一个类型为Action<TOptions>的委托对象,该委托对象会在接收到Options变化时自动执行,而作为输入的正是重新生成的Options对象。
由于注册的委托对象会将新 Profile 对象的相关属性打印在控制台上,所以程序启动后针对配置文件的任何修改都会导致新的数据被打印在控制台上。例如,我们先后修改了年龄(25)和性别(Female),新的数据将按照图7-3所示的形式反映在控制台上。(S703)
图7-3 及时提取新的Profile对象并应用到程序中(一)
具名 Options 同样可以采用类似的编程模式来实现配置的同步。在前面演示的提供具名Options 的第二个实例的基础上,我们对程序做了如下修改。与之前不同的是,在利用IServiceProvider对象得到IOptionsMonitor<TOptions>服务之后,可以调用其OnChange方法注册的回调是一个 Action<TOptions,String>对象,该委托对象的第二个参数表示的正是在注册IConfiguration对象与Options类型应用关系时指定的名称。
由于通过调用 OnChange方法注册的委托对象会将 Options的名称和承载的数据打印在控制台上,所以控制台上输出的内容总是与配置文件的内容同步。例如,在程序启动后,我们分别修改了用户 foo的年龄(25)和用户 bar的性别(Male),新的内容将以图 7-4所示的形式及时呈现在控制台上。(S704)
图7-4 及时提取新的Profile对象并应用到程序中(二)
7.1.4 直接初始化Options对象
前面演示的几个实例具有一个共同的特征,即都采用配置系统来提供绑定Options对象的原始数据,实际上,Options 框架具有一个完全独立的模型,可以称为 Options 模型。这个独立的Options 模型本身并不依赖于配置系统,让配置系统来提供配置数据仅仅是通过 Options 模型的一个扩展点实现的。在很多情况下,可能并不需要将应用的配置选项定义在配置文件中,在应用启动时直接初始化可能是一种更方便、快捷的方式。
我们依然沿用前面演示的应用场景,现在摒弃配置文件,转而采用编程的方式直接对用户信息进行初始化,所以需要对程序做如下改写。在调用 IServiceCollection 接口的Configure<Profile>扩展方法时,不需要再指定一个 IConfiguration 对象,而是利用一个Action<Profile>类型的委托对作为参数的 Profile 对象进行初始化。程序运行后会在控制台上产生图7-1所示的输出结果。(S705)
具名 Options同样可以采用类似的方式进行初始化。如果需要根据指定的名称对 Options进行初始化,那么调用方法时就需要指定一个 Action<TOptions,String>类型的委托对象,该委托对象的第二个参数表示Options的名称。在如下所示的代码片段中,我们通过类似的方式设置了两个用户(foo和 bar)的信息,然后利用 IOptionsSnapshot<TOptions>服务将它们分别提取出来。该程序运行后会在控制台上产生图7-2所示的输出结果。(S706)
在前面的演示中,我们利用依赖注入框架提供 IOptions<TOptions>服务、IOptionsSnapshot<TOptions>服务和 IOptionsMonitor<TOptions>服务,然后进一步利用它们来提供对应的 Options对象。既然作为依赖注入容器的 IServiceProvider对象能够提供这 3个对象,我们就能够将它们注入消费 Options 对象的类型中。所谓的 Options 模式就是通过注入这 3 个服务来提供对应Options对象的编程模式的。
7.1.5 根据依赖服务的Options设置
在很多情况下需要针对某个依赖的服务动态地初始化Options的设置,比较典型的就是根据当前的承载环境(开发、预发和产品)对 Options 做动态设置。第 6 章演示了一系列针对日期/时间输出格式的配置,下面沿用这个场景演示如何根据当前的承载环境设置对应的 Options。将DateTimeFormatOptions的定义进行简化,只保留如下所示的表示日期和时间格式的两个属性。
如下所示的代码片段是整个演示实例的完整定义。我们利用第 6 章介绍的配置系统来设置当前的承载环境,具体采用的是基于命令行参数的配置源。.NET Core的承载系统(详见第10章)通过IHostEnvironment接口表示承载环境,具体实现类型为HostingEnvironment。如下面的代码片段所示,我们利用获取的环境名称创建了一个 HostingEnvironment 对象,并针对IHostEnvironment接口采用Singleton生命周期做了相应的注册。
上面调用 IServiceCollection 接口的 AddOptions<DateTimeFormatOptions>扩展方法完成了针对Options 模型核心服务的注册和针对 DateTimeFormatOptions 的设置。该方法返回的是一个封装了IServiceCollection 集合的 OptionsBuilder<DateTimeFormatOptions>对象,可以调用其 Configure<IHostEnvironment>方法利用提供的 Action<DateTimeFormatOptions,IHostEnvironment>委托对象针对依赖的IHostEnvironment服务对DateTimeFormatOptions做相应的设置。
具体来说,我们针对开发环境和非开发环境设置了不同的日期与时间格式。如果采用命令行的方式启动这个应用程序,并利用命令行参数设置不同的环境名称,就可以在控制台上看到图7-5所示的针对DateTimeFormatOptions的不同设置。(S707)
图7-5 针对承载环境的Options设置
7.1.6 验证Options的有效性
由于配置选项是整个应用的全局设置,为了尽可能避免错误的设置造成的影响,最好能够对内容进行有效性验证。接下来我们将上面的程序做了如下改动,从而演示如何对设置的日期和时间格式做最后的有效性验证。
上述演示实例借助配置系统以命令行的形式提供了日期和时间格式化字符串。在创建了OptionsBuilder<DateTimeFormatOptions>对象并对 DateTimeFormatOptions做了相应设置之后,我们调用 Validate<DateTimeFormatOptions>方法利用提供的 Func<DateTimeFormatOptions,bool>委托对象对最终的设置进行验证。运行该程序并按照图 7-6 所示的方式指定不同的格式化字符串,系统会根据我们指定的规则来验证其有效性。(S708)
图7-6 验证Options的有效性