[ASP.NET Core 3框架揭秘] Options[1]: 配置选项的正确使用方式[上篇]
Artech 人气:0依赖注入不仅是支撑整个ASP.NET Core框架的基石,也是开发ASP.NET Core应用采用的基本编程模式,所以依赖注入十分重要。依赖注入使我们可以将依赖的功能定义成服务,最终以一种松耦合的形式注入消费该功能的组件或者服务中。除了采用依赖注入的形式消费承载某种功能的服务,还可以采用相同的方式消费承载配置数据的Options对象。
一、将配置绑定为Options对象
Options模式是一种采用依赖注入的方式来提供Options对象的编程方式,但这并不意味着我们会直接利用依赖注入框架来提供Options对象本身,因为利用依赖注入框架获取的是一个能够提供Options对象的IOptions<TOptions>对象,泛型参数TOptions表示的正是Options对象的类型。下面的演示实例利用IOptions<TOptions>服务来提供我们需要的Options对象,该对象由一个承载配置数据的IConfiguration对象绑定而成。简单起见,我们依然沿用《[ASP.NET Core 3框架揭秘] 配置[4]:将配置绑定为对象》定义的Profile作为基础的Options类型,下面先回顾相关类型的定义。
public class Profile : IEquatable<Profile> { public Gender Gender { get; set; } public int Age { get; set; } public ContactInfo ContactInfo { get; set; } public Profile() { } public Profile(Gender gender, int age, string emailAddress, string phoneNo) { Gender = gender; Age = age; ContactInfo = new ContactInfo { EmailAddress = emailAddress, PhoneNo = phoneNo }; } public bool Equals(Profile other) { return other == null? false : Gender == other.Gender &&Age == other.Age && ContactInfo.Equals(other.ContactInfo); } } public class ContactInfo : IEquatable<ContactInfo> { public string EmailAddress { get; set; } public string PhoneNo { get; set; } public bool Equals(ContactInfo other)=> other == null ? false : EmailAddress == other.EmailAddress && PhoneNo == other.PhoneNo; } public enum Gender { Male, Female }
下面通过一个简单的控制台应用来演示Options编程模式。在演示程序中定义了上面这些类型之后,我们创建承载一个Profile对象的配置文件profile.json。如下所示的代码片段就是这个JSON文件的内容,它提供了构成一个完整Profile对象的所有数据。为了使该文件能够在编译后自动复制到输出目录,我们需要将Copy to Output Directory属性设置为Copy Always。
{ "gender" : "Male", "age" : "18", "contactInfo": { "emailAddress": "foobar@outlook.com", "phoneNo" : "123456789" } }
下面编写代码来演示如何采用Options模式获取由配置文件提供的数据绑定生成的Profile对象。我们调用AddJsonFile扩展方法将针对JSON配置文件(profile.json)的配置源注册到创建的ConfigurationBuilder对象上,并利用它创建对应的IConfigurataion对象。
class Program { static void Main() { var configuration = new ConfigurationBuilder() .AddJsonFile("profile.json") .Build(); var profile = new ServiceCollection() .AddOptions() .Configure<Profile>(configuration) .BuildServiceProvider() .GetRequiredService<IOptions<Profile>>() .Value; Console.WriteLine($"Gender: {profile.Gender}"); Console.WriteLine($"Age: {profile.Age}"); Console.WriteLine($"Email Address: {profile.ContactInfo.EmailAddress}"); Console.WriteLine($"Phone No: {profile.ContactInfo.PhoneNo}"); } }
上面创建一个ServiceCollection对象,在调用AddOptions扩展方法注册Options编程模式的核心服务后,可以将创建的IConfiguration对象作为参数调用Configure<Profile>扩展方法。Configure<TOptions>扩展方法相当于将提供的IConfiguration对象与指定的TOptions类型做了一个映射,在需要提供对应TOptions对象时,IConfiguration对象承载的配置数据会被提取出来并绑定生成返回的TOptions对象。
在调用IServiceCollection的BuildServiceProvider扩展方法得到作为依赖注入容器的IServiceProvider对象之后,可以直接调用其GetRequiredService<T>扩展方法来提供IOptions<Profile>对象,该对象的Value属性返回的就是指定IConfiguration对象绑定生成的Profile对象。我们将这个Profile对象承载的相关数据直接打印在控制台上,输出结果如下图所示,由此可以看出,通过Options模式得到的Profile对象承载的数据完全来源于配置文件。
二、提供具名的Options
针对同一个Options类型,通过IOptions<TOptions>服务在整个应用范围内只能提供一个单一的Options对象,但是在很多情况下我们需要利用多个同类型的Options对象来承载不同的配置。就演示实例中用来表示个人信息的Profile类型来说,应用程序中可能会使用它来表示不同用户的信息,如张三、李四和王五。为了解决这个问题,我们可以在添加IConfiguration对象与Options类型映射关系时赋予它们一个唯一标识,这个标识最终会被用来提取对应的Options对象。这种具名的Options对象由IOptionsSnapshot<TOptions>接口表示的服务提供。
同样,针对前面的演示实例,假设的应用需要采用Options模式提取承载不同用户信息的Profile对象,具体应该如何实现?由于采用JSON格式的配置文件来提供原始的用户信息,所以需要将针对多个用户的信息定义在profile.json文件中。我们通过如下形式提供了两个用户(foo和bar)的基本信息。
{ "foo": { "gender": "Male", "age": "18", "contactInfo": { "emailAddress": "foo@outlook.com", "phoneNo": "123" } }, "bar": { "gender": "Female", "age": "25", "contactInfo": { "emailAddress": "bar@outlook.com", "phoneNo": "456" } } }
具名Options的注册和提取体现在如下所示的代码片段中。在调用IServiceCollection接口的Configure<TOptions>扩展方法时,我们将注册的映射关系命名为foo和bar,提供原始配置数据的IConfiguration对象也由原来的ConfigurationRoot对象变成它的两个子配置节。
class Program { static void Main() { var configuration = new ConfigurationBuilder() .AddJsonFile("profile.json") .Build(); var serviceProvider = new ServiceCollection() .AddOptions() .Configure<Profile>("foo", configuration.GetSection("foo")) .Configure<Profile>("bar", configuration.GetSection("bar")) .BuildServiceProvider(); var optionsAccessor = serviceProvider.GetRequiredService<IOptionsSnapshot<Profile>>(); Print(optionsAccessor.Get("foo")); Print(optionsAccessor.Get("bar")); static void Print(Profile profile) { Console.WriteLine($"Gender: {profile.Gender}"); Console.WriteLine($"Age: {profile.Age}"); Console.WriteLine($"Email Address: {profile.ContactInfo.EmailAddress}"); Console.WriteLine($"Phone No: {profile.ContactInfo.PhoneNo}\n"); } } }
为了使用指定的用户名来提取对应的Profile对象,可以利用作为依赖注入容器的IServiceProvider对象得到IOptionsSnapshot<TOptions>服务,并将用户名作为参数调用其Get方法得到对应的Profile对象。程序运行后,针对两个不同用户的基本信息将以下图所示的形式输出到控制台上。
三、配置源的同步
通过《配置数据与数据源的实时同步》的介绍可知,配置模型不仅支持对配置源的监控,还可以在检测到更新之后及时加载新的配置数据,并通过一个IChangeToken对象对外发送通知。对于前面演示的两个实例来说,提供的Options对象都是由配置文件提供的数据绑定生成的,如果新的配置数据被重新加载之后能够提供与之匹配的Options对象,那么这将是最理想的编程模式,可以通过IOptionsMonitor<TOptions>服务来实现。
前面演示的第一个实例利用JSON文件定义了一个单一Profile对象的信息,下面对它做相应的修改来演示如何监控这个JSON文件,并在监测到文件改变之后及时提取新的配置信息生成新的Profile对象。如下面的代码片段所示,调用AddJsonFile扩展方法注册对应配置源时应将该方法的参数reloadOnChange设置为True,从而开启对对应配置文件的监控功能。
class Program { static void Main() { var configuration = new ConfigurationBuilder() .AddJsonFile(path: "profile.json", optional: false, reloadOnChange: true) .Build(); new ServiceCollection() .AddOptions() .Configure<Profile>(configuration) .BuildServiceProvider() .GetRequiredService<IOptionsMonitor<Profile>>() .OnChange(profile => { Console.WriteLine($"Gender: {profile.Gender}"); Console.WriteLine($"Age: {profile.Age}"); Console.WriteLine($"Email Address: {profile.ContactInfo.EmailAddress}"); Console.WriteLine($"Phone No: {profile.ContactInfo.PhoneNo}\n"); }); Console.Read(); } }
在得到作为依赖注入容器的IServiceProvider对象之后,可以利用它得到IOptionsMonitor<TOptions>服务,该对象会接收到配置系统发出的关于配置被重新加载的通知,并在收到通知后重新生成Options对象。我们调用IOptionsMonitor<TOptions>对象的OnChange方法注册了一个类型为Action<TOptions>的委托对象,该委托对象会在接收到Options变化时自动执行,而作为输入的正是重新生成的Options对象。由于注册的委托对象会将新Profile对象的相关属性打印在控制台上,所以程序启动后针对配置文件的任何修改都会导致新的数据被打印在控制台上。例如,我们先后修改了年龄(25)和性别(Female),新的数据将按照下图所示的形式反映在控制台上。
具名Options同样可以采用类似的编程模式来实现配置的同步。在前面演示的提供具名Options的第二个实例的基础上,我们对程序做了如下修改。与之前不同的是,在利用IServiceProvider对象得到IOptionsMonitor<TOptions>服务之后,可以调用其OnChange方法注册的回调是一个Action<TOptions, String>对象,该委托对象的第二个参数表示的正是在注册IConfiguration对象与Options类型应用关系时指定的名称。
class Program { static void Main() { var configuration = new ConfigurationBuilder() .AddJsonFile(path: "profile.json", optional: false, reloadOnChange: true) .Build(); new ServiceCollection() .AddOptions() .Configure<Profile>("foo", configuration.GetSection("foo")) .Configure<Profile>("bar", configuration.GetSection("bar")) .BuildServiceProvider() .GetRequiredService<IOptionsMonitor<Profile>>() .OnChange((profile, name) => { Console.WriteLine($"Name: {name}"); Console.WriteLine($"Gender: {profile.Gender}"); Console.WriteLine($"Age: {profile.Age}"); Console.WriteLine($"Email Address: {profile.ContactInfo.EmailAddress}"); Console.WriteLine($"Phone No: {profile.ContactInfo.PhoneNo}\n"); }); Console.Read(); } }
由于通过调用OnChange方法注册的委托对象会将Options的名称和承载的数据打印在控制台上,所以控制台上输出的内容总是与配置文件的内容同步。例如,在程序启动后,我们分别修改了用户foo的年龄(25)和用户bar的性别(Male),新的内容将以图7-4所示的形式及时呈现在控制台上。
加载全部内容