1. 选项
前面讲完了.NET Core 下的配置系统,我们可以通过 IConfiguration 服务从各种来源的配置中读取到配置信息,但是每次要用的时候都通过 Iconfiguration 读取配置文件会比较不方便,而且效率低。.NET Core 体系下提供了一个选项系统,该功能用于实现以强类型的方式对程序配置信息进行访问,并且可以将选项类注入到依赖注入容器中进行管理和使用。
在进行配置信息的强类型选项绑定的时候,需要一个相应的选项类,该类推荐按 {Object}Options
命名规则进行命名,有以下特点:
- 必须非抽象类
- 必须包含公共无参的构造函数
- 类中需要与配置项进行绑定的属性必须拥有 public 的 get、set 访问器,并且属性的命名必须与配置键一直,不区分大小写
- 要确保配置项能够转换到其绑定的属性类型
- 该类的字段不会被绑定
2. 选项配置方式
2.1 手动绑定
IConfiguration 服务通过 ConfigurationBinder 类扩展了 Get 和 Bind 两个方法,这两个方法可以将配置信息绑定到选项类上。这种方式在之前的 ASP.NET Core – 配置系统之配置读取 一章有讲到过,这里再做一下演示:
首先在配置文件中添加以下节点:
"Blog": {
"Title": "ASP.NET Core Options",
"Content": "This is a blog about Options System in ASP.NET Core Framework.",
"CreateTime": "2022-12-06"
}
之后定义一个选项类:
public class BlogOptions
{
public const string Blog = "Blog";
public string Title { get; set; }
public string Content { get; set; }
public DateTime CreateTime { get; set; }
}
然后,在任何可以获取到 IConfiguration 服务的地方都可以通过 IConfiguration 服务进行绑定:
using OptionsSample.Options;
using System.Text.Json;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
var blog1 = new BlogOptions();
app.Configuration.GetSection(BlogOptions.Blog).Bind(blog1);
var blog2 = app.Configuration.GetSection(BlogOptions.Blog).Get<BlogOptions>();
Console.WriteLine(JsonSerializer.Serialize(blog1));
Console.WriteLine(JsonSerializer.Serialize(blog2));
app.Run();
这种方式依旧有些不方便,虽然也有好处,能够监测到配置的修改,在应用运行中同步改变(如果相应的配置处理程序支持变更重载的化),但每次都要指定相应的节点,每次都要实时构建新的选项对象,并且选项系统也能做到配置更改时更新选项。
2.2 依赖注入配置
2.2.1 配置文件节点转换选项
除了手动绑定的方式外,我们还可以在应用启动的时候读取相应的配置节点,配置成Options并同时注册到依赖注入容器中,由依赖注入容器管理其生命周期,并且多次使用。我们可以像注册其他的依赖注入关系一样,在入口文件中通过 IServiceCollection 的 Configure 扩展方法进行配置。
// 通过配置文件读取某一配置节点
//builder.Services.Configure<BlogOptions>(builder.Configuration.GetSection(BlogOptions.Blog));
这里通过获取特定的配置节点,并将其配置为选项,之后就可以在任何能够进行依赖注入的地方使用注入的 BlogOptions 选项类了。
var blogOption = app.Services.GetRequiredService<IOptions<BlogOptions>>().Value;
Console.WriteLine(JsonSerializer.Serialize(blogOption));
这里使用到的 Configure 方法是 OptionsConfigurationServiceCollectionExtensions 类中的扩展方法,选项系统中有好几个同名的 Configure 方法,但是这些是并不是同一个方法的重载,下面会详细讲到。
2.2.2 硬编码配置选项
除了从配置文件中读取配置节点转换为选项之外,我们也可以直接在代码中硬编码指定选项内容,并注入到容器之中。
//硬编码的方式设置配置信息,也可以在这里读取数据库信息
builder.Services.Configure<BlogOptions>(option =>
{
option.Title = "test";
option.Content = "test hard code option";
option.CreateTime = DateTime.Now;
});
这种情况用得不多,在这种情况下我们可以进行额外的一些逻辑,例如从数据库中获取一些信息。这里的 Configure 方法是 OptionsServiceCollectionExtensions 扩展类中的方法。值得注意的是,在同时使用上面两个方法配置同一个选项类的情况下,硬编码配置的方式优先。
2.2.3 使用DI服务配置选项
在某些场景下,选项的配置需要比较复杂的逻辑,会依赖容器中的其他服务,我们还可以使用以下方式:
builder.Services.AddOptions<BlogOptions>()
// 这里可以通过 Configure 方法指定需要的服务, IServiceProvider 只是一个示例
.Configure<IServiceProvider>((option, service) => // 接收的的第一个参数选项类对象,后面依次是所注入的服务
{
// 通过注入的服务执行相应的逻辑
option.Title = "test DI Configure";
option.Content = "test DI Configure";
option.CreateTime = DateTime.Now;
});
这里的 Configure 方法,与上面的不一样,不再是 IServiceCollection 的扩展方法,而是 OptionsBuilder 类中的方法,AddOptions 扩展方法在 OptionsServiceCollectionExtensions 中,返回一个 OptionsBuilder 对象。该方法有多个重载,最多支持5个服务来配置选项:
当使用这种方式和上面的硬编码的方式同时配置同一个选项类的情况下,哪部分代码后执行,最后选项类的内容就以哪部分为准。
2.2.4 命名选项
在一些情况下,应用中是存在多份配置结构相同,但具体配置值不同的配置信息的,例如以下的情况:
"FirstBlog": {
"Title": "ASP.NET Core Options",
"Content": "This is a blog about Options System in ASP.NET Core Framework.",
"CreateTime": "2022-12-06"
},
"SecondBlog": {
"Title": "ASP.NET Core Configuration",
"Content": "This is a blog about Configuration System in ASP.NET Core Framework.",
"CreateTime": "2022-12-08"
}
这种情况下,两个配置节点其实可以用同一个选项类接收,它们的结构是一样的。而命名选项就是为了应对这种情况,选项系统中允许为当前配置的选项命名,而选项系统区分同一个类型的不同选项也是根据名字进行区分的。事实上,.NET Core 中所有 Options 都是命名选项,当没有显式指定名字时(如上面讲到的默认配置方式),使用的名字默认是Options.DefaultName,即string.Empty。
builder.Services.Configure<BlogOptions>("First", builder.Configuration.GetSection("FirstBlog"));
builder.Services.Configure<BlogOptions>("Second", builder.Configuration.GetSection("SecondBlog"));
这里要说明的是,上面我们从依赖注入容器中解析 IOptions 接口,从而获取我们需要的选项值,但是使用命名选项的情况下是无法通过 IOptions 接口解析相应的选项的,必须通过 IOptionsMonitor 或者 IOptionsSnapshot 接口来解析。这三个接口的区别下面会重点讲。
var blog = app.Services.GetRequiredService<IOptions<BlogOptions>>().Value;
Console.WriteLine(JsonSerializer.Serialize(blog));
var firstBlog = app.Services.GetRequiredService<IOptionsMonitor<BlogOptions>>().Get("First");
Console.WriteLine(JsonSerializer.Serialize(firstBlog));
var secondBlog = app.Services.GetRequiredService<IOptionsMonitor<BlogOptions>>().Get("Second");
Console.WriteLine(JsonSerializer.Serialize(secondBlog));
2.2.4 后期配置
后期配置是指当我们通过前面的方法对一个选项类进行配置之后,可能还会因为其他业务逻辑需要对其中的配置信息进行修改,这时候我们可以通过后期配置对选项系统中已配置的选项内容进行替换。后期配置在所有的OptionsServiceCollectionExtensions.Configure 执行完成之后执行,即配置系统不管代码顺序,会先完成所有选项的配置,再执行后期配置。
builder.Services.Configure<BlogOptions>("First", builder.Configuration.GetSection("FirstBlog"));
builder.Services.PostConfigure<BlogOptions>("First", options =>
{
options.Title = "Post Config";
});
var firstBlog = app.Services.GetRequiredService<IOptionsMonitor<BlogOptions>>().Get("First");
Console.WriteLine(JsonSerializer.Serialize(firstBlog));
除了 PostConfigure 方法外,还有 PostConfigureAll 方法,前者对单一选项类单一命名的选项对象进行后期配置,如果没有指定名称则是默认名称,后者会对统一选项类下的不同命名选项统一进行配置。
builder.Services.Configure<BlogOptions>("First", builder.Configuration.GetSection("FirstBlog"));
builder.Services.Configure<BlogOptions>("Second", builder.Configuration.GetSection("SecondBlog"));
builder.Services.PostConfigureAll<BlogOptions>(options =>
{
options.Title = "Post Config";
});
var firstBlog = app.Services.GetRequiredService<IOptionsMonitor<BlogOptions>>().Get("First");
Console.WriteLine(JsonSerializer.Serialize(firstBlog));
var secondBlog = app.Services.GetRequiredService<IOptionsMonitor<BlogOptions>>().Get("Second");
Console.WriteLine(JsonSerializer.Serialize(secondBlog));
通过 IOptionsMonitor 或者 IOptionsSnapshot 接口解析选项,在配置来源更改的情况下,相应的选项内容也会随之变化(两者的适用场景不同), 后期配置逻辑在选项的每一次更改之后都会执行。
参考文章:
ASP.NET Core 中的选项模式 | Microsoft Learn
选项模式 – .NET | Microsoft Learn
面向 .NET 库创建者的选项模式指南 – .NET | Microsoft Learn
理解ASP.NET Core – 选项(Options)
ASP.NET Core 系列: