ASP.NET Core 6框架揭秘实例演示[10]:Options基本编程模式
依赖注入使我们可以将依赖的功能定义成服务,最终以一种松耦合的形式注入消费该功能的组件或者服务中。除了可以采用依赖注入的形式消费承载某种功能的服务,还可以采用相同的方式消费承载配置数据的Options对象,这篇文章演示几种典型的编程模式。(本篇提供的实例已经汇总到《》)
[601]将配置绑定为Options对象(源代码)
[602]具名Options的注册和提取(源代码)
[603]Options与配置源的实时同步(匿名Options)(源代码)
[604]Options与配置源的实时同步(具名Options)(源代码)
[605]用代码方式初始化Options(匿名Options)(源代码)
[606]用代码方式初始化Options(具名Options)(源代码)
[607]针对依赖服务的Options设置(源代码)
[608]验证Options的有效性(源代码)
[601]将配置绑定为Options对象
Options模式采用依赖注入的方式提供Options对象,但是由依赖注入容提供的是一个IOptions
public class Profile { public Gender Gender { get; set; } public int Age { get; set; } public ContactInfo? ContactInfo { get; set; } } public class ContactInfo { public string? EmailAddress { get; set; } public string? PhoneNo { get; set; } } public enum Gender { Male, Female }
我们在项目根目录下创建一个名为profile.json的配置文件,并在启动定义了如下的内容。为了使该文件能够在编译后自动复制到输出目录,我们需要将“Copy to Output Directory”属性设置为“Copy Always”。
{ "gender": "Male", "age": "18", "contactInfo": { "emailAddress" : "foobar@outlook.com", "phoneNo": "123456789" } }
在如下演示的程序中。我们调用AddJsonFile扩展方法将针对JSON配置文件(profile.json)的配置源注册到创建的ConfigurationBuilder对象上,并最终将IConfiguration对象构建出来。我们接下来创建了一个ServiceCollection对象,通过调用它的AddOptions扩展方法注册Options模式的核心服务。我们然后将创建的IConfiguration对象作为参数调用了ServiceCollection对象Configure
using App; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; var configuration = new ConfigurationManager(); configuration.AddJsonFile("profile.json"); var profile = new ServiceCollection() .AddOptions() .Configure(configuration) .BuildServiceProvider() .GetRequiredService >().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}");
在成功构建出作为依赖注入容器的IServiceProvider对象后,我们调用其GetRequiredService
图1 绑定配置生成的Profile对象
[602]具名Options的注册和提取
IOptions
{ "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
using App; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; var configuration = new ConfigurationManager(); configuration.AddJsonFile("profile.json"); var serviceProvider = new ServiceCollection() .AddOptions() .Configure("foo", configuration.GetSection("foo")) .Configure ("bar", configuration.GetSection("bar")) .BuildServiceProvider(); var optionsAccessor = serviceProvider.GetRequiredService >(); 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"); }
我们调用IServiceProvider对象的GetRequiredService
图2 根据用户名提取对应的Profile对象
[603]Options与配置源的实时同步(匿名Options)
前面演示的第一个实例利用JSON文件定义了一个单一Profile对象的信息,我们现在对它做相应的修改来演示如何监控这个JSON文件,并在文件更新之后加载新的内容来生成对Profile对象进行绑定的IConfiuration对象。如下面的代码片段所示,我们在调用AddJsonFile扩展方法注册对应配置源时应将该方法的参数reloadOnChange设置为True,从而开启对对应配置文件的监控功能。
using App; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; var configuration = new ConfigurationManager(); configuration.AddJsonFile( path : "profile.json", optional : false, reloadOnChange : true); new ServiceCollection() .AddOptions() .Configure(configuration) .BuildServiceProvider() .GetRequiredService >() .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();
我们利用作为依赖注入容器得到IOptionsMonitor
图3 及时提取新的Profile对象并应用到程序中(匿名Options)
[604]Options与配置源的实时同步(具名Options)
具名Options同样可以采用类似的编程模式来。我们在前面演示程序的基础上做了如下修改。如代码片段所示,在得到IOptionsMonitor
using App; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; var configuration = new ConfigurationManager(); configuration.AddJsonFile( path : "profile.json", optional : false, reloadOnChange : true); new ServiceCollection() .AddOptions() .Configure("foo", configuration.GetSection("foo")) .Configure ("bar", configuration.GetSection("bar")) .BuildServiceProvider() .GetRequiredService >() .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();
改动后的程序启动之后,针对配置文件所作的任何更新都会体现在控制台上。比如我们分别修改了用户foo的年龄(25)和用户bar的性别(Male),新的内容将以图4所示的形式及时呈现在控制台上。
图4 及时提取新的Profile对象并应用到程序中(具名Options)
[605]用代码方式初始化Options(匿名Options)
前面演示的几个实例具有一个共同的特征,那就是都采用承载配置的IConfiguration对象来绑定Options对象。实际上Options是一个完全独立于配置系统的框架,利用配置绑定的形式来对Options对象进行初始化仅仅是该框架提供的一个小小的扩展而已。我们现在摒弃配置文件,转而采用编程的方式直接对Options进行初始化。如下面的代码片段所示,在调用IServiceCollection接口的Configure
using App; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; var profile = new ServiceCollection() .AddOptions() .Configure(it => { it.Gender = Gender.Male; it.Age = 18; it.ContactInfo = new ContactInfo { PhoneNo = "123456789", EmailAddress = "foobar@outlook.com" }; }) .BuildServiceProvider() .GetRequiredService >() .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}\n");
[606]用代码方式初始化Options(具名Options)
具名Options同样可以采用类似的编程方式。如果需要根据指定的名称对Options进行初始化,那么调用方法时就需要指定一个Action
using App; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; var optionsAccessor = new ServiceCollection() .AddOptions() .Configure("foo", it => { it.Gender = Gender.Male; it.Age = 18; it.ContactInfo = new ContactInfo { PhoneNo = "123", EmailAddress = "foo@outlook.com" }; }) .Configure ("bar", it => { it.Gender = Gender.Female; it.Age = 25; it.ContactInfo = new ContactInfo { PhoneNo = "456", EmailAddress = "bar@outlook.com" }; }) .BuildServiceProvider() .GetRequiredService >(); 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"); };
[607]针对依赖服务的Options设置
在很多情况下我们需要针对某个依赖的服务动态地初始化Options的设置,比较典型的就是根据当前的承载环境(开发、预发和产品)对Options做动态设置。我们在第5章“配置选项(上)”中演示了一系列针对日期/时间输出格式的配置,下面沿用这个场景演示如何根据当前的承载环境设置对应的Options。我们将DateTimeFormatOptions的定义进行简化,只保留如下所示的表示日期和时间格式的两个属性。
public class DateTimeFormatOptions { public string DatePattern { get; set; } public string TimePattern { get; set; } public override string ToString() => $"Date: {DatePattern}; Time: {TimePattern}"; }
我们利用配置来提供当前的承载环境,具体采用的是基于命令行参数的配置源。 .NET的服务承载系统通过IHostEnvironment接口表示承载环境,具体实现类型为HostingEnvironment(该类型定义在“Microsoft.Extensions.Hosting”NuGet包中,我们需要添加针对这个包的引用)。如下面的演示程序所示,我们创建了一个ServiceCollection对象,并添加了针对IHostEnvironment接口的服务注册,具体提供的是一个根据环境名称创建的HostingEnvironment对象。
using App; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting.Internal; using Microsoft.Extensions.Options; var environment = new ConfigurationBuilder() .AddCommandLine(args) .Build()["env"]; var services = new ServiceCollection(); services .AddSingleton( new HostingEnvironment { EnvironmentName = environment }) .AddOptions ().Configure ( (options, env) => { if (env.IsDevelopment()) { options.DatePattern = "dddd, MMMM d, yyyy"; options.TimePattern = "M/d/yyyy"; } else { options.DatePattern = "M/d/yyyy"; options.TimePattern = "h:mm tt"; } }); var options = services .BuildServiceProvider() .GetRequiredService >() .Value; Console.WriteLine(options);
我们调用了ServiceCollection对象的AddOptions
图5 针对承载环境的Options设置
[608]验证Options的有效性
配置选项是整个应用的全局设置,如果对它进行了错误的设置可能会造成很严重的后果,所以最好能够在使用之前进行有效性验证。接下来我们将上面的程序做了如下改动,从而演示如何对设置的日期和时间格式进行验证。
using App; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using System.Globalization; var config = new ConfigurationBuilder() .AddCommandLine(args) .Build(); var datePattern = config["date"]; var timePattern = config["time"]; var services = new ServiceCollection(); services.AddOptions() .Configure(options => { options.DatePattern = datePattern; options.TimePattern = timePattern; }) .Validate(options => Validate(options.DatePattern) && Validate(options.TimePattern), "Invalid Date or Time pattern."); try { var options = services .BuildServiceProvider() .GetRequiredService >().Value; Console.WriteLine(options); } catch (OptionsValidationException ex) { Console.WriteLine(ex.Message); } static bool Validate(string format) { var time = new DateTime(1981, 8, 24, 2, 2, 2); var formatted = time.ToString(format); return DateTimeOffset.TryParseExact(formatted, format,null, DateTimeStyles.None, out var value) && (value.Date == time.Date || value.TimeOfDay == time.TimeOfDay); }
上述演示实例借助配置系统以命令行的形式提供了日期和时间格式化字符串。在创建了OptionsBuilder
图6 验证Options的有效性