四、直接初始化Options对象
前面演示的几个实例具有一个共同的特征,即都采用配置系统来提供绑定Options对象的原始数据,实际上,Options框架具有一个完全独立的模型,可以称为Options模型。这个独立的Options模型本身并不依赖于配置系统,让配置系统来提供配置数据仅仅是通过Options模型的一个扩展点实现的。在很多情况下,可能并不需要将应用的配置选项定义在配置文件中,在应用启动时直接初始化可能是一种更方便快捷的方式。
class Program
{
static void Main()
{
var profile = new ServiceCollection()
.AddOptions()
.Configure<Profile>(it =>
{
it.Gender = Gender.Male;
it.Age = 18;
it.ContactInfo = new ContactInfo
{
PhoneNo = "123456789",
EmailAddress = "foobar@outlook.com"
};
})
.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}\n");
}
}
我们依然沿用前面演示的应用场景,现在摒弃配置文件,转而采用编程的方式直接对用户信息进行初始化,所以需要对程序做如上改写。在调用IServiceCollection接口的Configure<Profile>扩展方法时,不需要再指定一个IConfiguration对象,而是利用一个Action<Profile>类型的委托对作为参数的Profile对象进行初始化。程序运行后会在控制台上产生下图所示的输出结果。
具名Options同样可以采用类似的方式进行初始化。如果需要根据指定的名称对Options进行初始化,那么调用方法时就需要指定一个Action<TOptions,String>类型的委托对象,该委托对象的第二个参数表示Options的名称。在如下所示的代码片段中,我们通过类似的方式设置了两个用户(foo和bar)的信息,然后利用IOptionsSnapshot<TOptions>服务将它们分别提取出来。
class Program
{
static void Main()
{
var optionsAccessor = new ServiceCollection()
.AddOptions()
.Configure<Profile>("foo", it =>
{
it.Gender = Gender.Male;
it.Age = 18;
it.ContactInfo = new ContactInfo
{
PhoneNo = "123",
EmailAddress = "foo@outlook.com"
};
})
.Configure<Profile>("bar", it =>
{
it.Gender = Gender.Female;
it.Age = 25;
it.ContactInfo = new ContactInfo
{
PhoneNo = "456",
EmailAddress = "bar@outlook.com"
};
})
.BuildServiceProvider()
.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");
};
}
}
该程序运行后会在控制台上产生下图所示的输出结果。在前面的演示中,我们利用依赖注入框架提供IOptions<TOptions>服务、IOptionsSnapshot<TOptions>服务和IOptionsMonitor<TOptions>服务,然后进一步利用它们来提供对应的Options对象。既然作为依赖注入容器的IServiceProvider对象能够提供这3个对象,我们就能够将它们注入消费Options对象的类型中。所谓的Options模式就是通过注入这3个服务来提供对应Options对象的编程模式。
五、根据依赖服务的Options设置
在很多情况下需要针对某个依赖的服务动态地初始化Options的设置,比较典型的就是根据当前的承载环境(开发、预发和产品)对Options做动态设置。《上篇》演示了一系列针对时间日期输出格式的配置,下面沿用这个场景演示如何根据当前的承载环境设置对应的Options。将DateTimeFormatOptions的定义进行简化,只保留如下所示的表示日期和时间格式的两个属性。
public class DateTimeFormatOptions
{
public string DatePattern { get; set; }
public string TimePattern { get; set; }
public override string ToString() => $"Date: {DatePattern}; Time: {TimePattern}";
}
如下所示的代码片段是整个演示实例的完整定义。我们利用第6章介绍的配置系统来设置当前的承载环境,具体采用的是基于命令行参数的配置源。.NET Core的承载系统通过IHostEnvironment接口表示承载环境,具体实现类型为HostingEnvironment。如下面的代码片段所示,我们利用获取的环境名称创建了一个HostingEnvironment对象,并针对IHostEnvironment接口采用Singleton生命周期做了相应的注册。
class Program
{
public static void Main(string[] args)
{
var environment = new ConfigurationBuilder()
.AddCommandLine(args)
.Build()["env"]; var services = new ServiceCollection();
services
.AddSingleton<IHostEnvironment>(new HostingEnvironment { EnvironmentName = environment })
.AddOptions<DateTimeFormatOptions>().Configure<IHostEnvironment>( (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<IOptions<DateTimeFormatOptions>>().Value;
Console.WriteLine(options);
}
}
上面调用IServiceCollection接口的AddOptions<DateTimeFormatOptions>扩展方法完成了针对Options模型核心服务的注册和针对DateTimeFormatOptions的设置。该方法返回的是一个封装了IServiceCollection集合的OptionsBuilder<DateTimeFormatOptions>对象,可以调用其Configure<IHostEnvironment>方法利用提供的Action<DateTimeFormatOptions, IHostEnvironment>委托对象针对依赖的IHostEnvironment服务对DateTimeFormatOptions做相应的设置。具体来说,我们针对开发环境和非开发环境设置了不同的日期时间格式。如果采用命令行的方式启动这个应用程序,并利用命令行参数设置不同的环境名称,就可以在控制台上看到下图所示的针对DateTimeFormatOptions的不同设置。
六、验证Options的有效性
由于配置选项是整个应用的全局设置,为了尽可能避免错误的设置造成的影响,最好能够对内容进行有效性验证。接下来我们将上面的程序做了如下改动,从而演示如何对设置的日期和时间格式做最后的有效性验证。
class Program
{
public static void Main(string[] args)
{
var config = new ConfigurationBuilder()
.AddCommandLine(args)
.Build();
var datePattern = config["date"];
var timePattern = config["time"]; var services = new ServiceCollection();
services.AddOptions<DateTimeFormatOptions>()
.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<IOptions<DateTimeFormatOptions>>().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<DateTimeFormatOptions>对象并对DateTimeFormatOptions做了相应设置之后,我们调用Validate<DateTimeFormatOptions>方法利用提供的Func<DateTimeFormatOptions,bool>委托对象对最终的设置进行验证。运行该程序并按照下图所示的方式指定不同的格式化字符串,系统会根据我们指定的规则来验证其有效性。
[ASP.NET Core 3框架揭秘] Options[1]: 配置选项的正确使用方式[上篇]
[ASP.NET Core 3框架揭秘] Options[2]: 配置选项的正确使用方式[下篇]
[ASP.NET Core 3框架揭秘] Options[3]: Options模型[上篇]
[ASP.NET Core 3框架揭秘] Options[4]: Options模型[下篇]
[ASP.NET Core 3框架揭秘] Options[5]: 依赖注入
[ASP.NET Core 3框架揭秘] Options[6]: 扩展与定制
[ASP.NET Core 3框架揭秘] Options[7]: 与配置系统的整合