.Net Core 中的选项Options
.NetCore的配置选项建议结合在一起学习,不了解.NetCore 配置Configuration的同学可以看下我的上一篇文章 [.Net Core配置Configuration源码研究]
由代码开始
定义一个用户配置选项
public class UserOptions
{
private string instanceId;
private static int index = 0;
public UserOptions()
{
instanceId = (++index).ToString("00");
Console.WriteLine($"Create UserOptions Instance:{instanceId}");
}
public string Name { get; set; }
public int Age { get; set; }
public override string ToString() => $"Name:{Name} Age:{Age} Instance:{instanceId} ";
}
public class UserOptions2
{
public string Name { get; set; }
public int Age { get; set; }
public override string ToString() => $" Name:{Name} Age:{Age}";
}
定义json配置文件:myconfig.json
{
"UserOption": {
"Name": "ConfigName-zhangsan",
"Age": 666
}
}
创建ServiceCollection
services = new ServiceCollection();
var configBuilder = new ConfigurationBuilder().AddInMemoryCollection().AddJsonFile("myconfig.json", true, true);
var iconfiguration = configBuilder.Build();
services.AddSingleton<IConfiguration>(iconfiguration);
示例代码
services.Configure<UserOptions>(x => { x.Name = "张三"; x.Age = new Random().Next(1, 10000); });
services.AddOptions<UserOptions2>().Configure<IConfiguration>((x, config) => { x.Name = config["UserOption:Name"]; x.Age = 100; }); ;
services.PostConfigure<UserOptions>(x => { x.Name = x.Name + "Post"; x.Age = x.Age; });
services.Configure<UserOptions>("default", x => { x.Name = "Default-张三"; x.Age = new Random().Next(1, 10000); });
services.Configure<UserOptions>("config", configuration.GetSection("UserOption"));
using (var provider = services.BuildServiceProvider())
{
using (var scope1 = provider.CreateScope())
{
PrintOptions(scope1, "Scope1");
}
//修改配置文件
Console.WriteLine(string.Empty);
Console.WriteLine("修改配置文件");
var filePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "myconfig.json");
File.WriteAllText(filePath, "{\"UserOption\": { \"Name\": \"ConfigName-lisi\", \"Age\": 777}}");
//配置文件的change回调事件需要一定时间执行
Thread.Sleep(300);
Console.WriteLine(string.Empty);
using (var scope2 = provider.CreateScope())
{
PrintOptions(scope2, "Scope2");
}
Console.WriteLine(string.Empty);
using (var scope3 = provider.CreateScope())
{
PrintOptions(scope3, "Scope3");
}
}
static void PrintOptions(IServiceScope scope, string scopeName)
{
var options1 = scope.ServiceProvider.GetService<IOptions<UserOptions>>();
Console.WriteLine($"手动注入读取,IOptions,{scopeName}-----{ options1.Value}");
var options2 = scope.ServiceProvider.GetService<IOptionsSnapshot<UserOptions>>();
Console.WriteLine($"配置文件读取,IOptionsSnapshot,{scopeName}-----{ options2.Value}");
var options3 = scope.ServiceProvider.GetService<IOptionsSnapshot<UserOptions>>();
Console.WriteLine($"配置文件根据名称读取,IOptionsSnapshot,{scopeName}-----{ options3.Get("config")}");
var options4 = scope.ServiceProvider.GetService<IOptionsMonitor<UserOptions>>();
Console.WriteLine($"配置文件读取,IOptionsMonitor,{scopeName}-----{ options4.CurrentValue}");
var options5 = scope.ServiceProvider.GetService<IOptionsMonitor<UserOptions>>();
Console.WriteLine($"配置文件根据名称读取,IOptionsMonitor,{scopeName}-----{options5.Get("config")}");
var options6 = scope.ServiceProvider.GetService<IOptions<UserOptions2>>();
Console.WriteLine($"Options2-----{options6.Value}");
}
代码运行结果
Create UserOptions Instance:01
手动注入读取,IOptions,Scope1----- Name:张三Post Age:6575 Instance:01
Create UserOptions Instance:02
配置文件读取,IOptionsSnapshot,Scope1----- Name:张三Post Age:835 Instance:02
Create UserOptions Instance:03
配置文件根据名称读取,IOptionsSnapshot,Scope1----- Name:ConfigName-zhangsan Age:666 Instance:03
Create UserOptions Instance:04
配置文件读取,IOptionsMonitor,Scope1----- Name:张三Post Age:1669 Instance:04
Create UserOptions Instance:05
配置文件根据名称读取,IOptionsMonitor,Scope1----- Name:ConfigName-zhangsan Age:666 Instance:05
Options2----- Name:ConfigName-zhangsan Age:100
修改配置文件
Create UserOptions Instance:06
手动注入读取,IOptions,Scope2----- Name:张三Post Age:6575 Instance:01
Create UserOptions Instance:07
配置文件读取,IOptionsSnapshot,Scope2----- Name:张三Post Age:5460 Instance:07
Create UserOptions Instance:08
配置文件根据名称读取,IOptionsSnapshot,Scope2----- Name:ConfigName-lisi Age:777 Instance:08
配置文件读取,IOptionsMonitor,Scope2----- Name:张三Post Age:1669 Instance:04
配置文件根据名称读取,IOptionsMonitor,Scope2----- Name:ConfigName-lisi Age:777 Instance:06
Options2----- Name:ConfigName-zhangsan Age:100
手动注入读取,IOptions,Scope3----- Name:张三Post Age:6575 Instance:01
Create UserOptions Instance:09
配置文件读取,IOptionsSnapshot,Scope3----- Name:张三Post Age:5038 Instance:09
Create UserOptions Instance:10
配置文件根据名称读取,IOptionsSnapshot,Scope3----- Name:ConfigName-lisi Age:777 Instance:10
配置文件读取,IOptionsMonitor,Scope3----- Name:张三Post Age:1669 Instance:04
配置文件根据名称读取,IOptionsMonitor,Scope3----- Name:ConfigName-lisi Age:777 Instance:06
Options2----- Name:ConfigName-zhangsan Age:100
通过运行代码得到的结论
- Options可通过手动初始化配置项配置(可在配置时读取依赖注入的对象)、或通过IConfiguration绑定配置
- PostConfiger可在Configer基础上继续配置
- 可通过IOptionsSnapshot或IOptionsMonitor根据配置名称读取配置项,未指定名称读取第一个注入的配置
- IOptions和IOptionsMonitor生命周期为Singleton,IOptionsSnapshot生命周期为Scope
- IOptionsMonitor可监听到配置文件变动去动态更新配置项
问题
- IOptions,IOptionsSnapshot,IOptionsMonitor 如何/何时注入、初始化
- Options指定名称时内部是如何设置的
- Options如何绑定的IConfiguration
- IOptionsMonitor是如何同步配置文件变动的
配合源码解决疑惑
Configure注入
public static IServiceCollection Configure<TOptions>(this IServiceCollection services, Action<TOptions> configureOptions) where TOptions : class
{
return services.Configure(Microsoft.Extensions.Options.Options.DefaultName, configureOptions);
}
public static IServiceCollection Configure<TOptions>(this IServiceCollection services, string name, Action<TOptions> configureOptions) where TOptions : class
{
services.AddOptions();
services.AddSingleton((IConfigureOptions<TOptions>)new ConfigureNamedOptions<TOptions>(name, configureOptions));
return services;
}
public static IServiceCollection AddOptions(this IServiceCollection services)
{
services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptions<>), typeof(OptionsManager<>)));
services.TryAdd(ServiceDescriptor.Scoped(typeof(IOptionsSnapshot<>), typeof(OptionsManager<>)));
services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptionsMonitor<>), typeof(OptionsMonitor<>)));
services.TryAdd(ServiceDescriptor.Transient(typeof(IOptionsFactory<>), typeof(OptionsFactory<>)));
services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptionsMonitorCache<>), typeof(OptionsCache<>)));
return services;
}
通过上面的源码可以发现,Options相关类是在AddOptions中注入的,具体的配置项在Configure中注入。
如果不指定Configure的Name,也会有个默认的Name=Microsoft.Extensions.Options.Options.DefaultName
那么我们具体的配置项存到哪里去了呢,在ConfigureNamedOptions这个类中,在Configer函数调用时,只是把相关的配置委托存了起来:
public ConfigureNamedOptions(string name, Action<TOptions> action)
{
Name = name;
Action = action;
}
OptionsManager
private readonly ConcurrentDictionary<string, Lazy<TOptions>> _cache = new ConcurrentDictionary<string, Lazy<TOptions>>(StringComparer.Ordinal);
public TOptions Value => Get(Options.DefaultName);
public virtual TOptions Get(string name)
{
name = name ?? Options.DefaultName;
return _cache.GetOrAdd(name, () => _factory.Create(name));
}
OptionsManager实现相对较简单,在查询时需要执行Name,如果为空就用默认的Name,如果缓存没有,就用Factory创建一个,否则就读缓存中的选项。
IOptions和IOptionsSnapshot的实现类都是OptionsManager,只是生命周期不同。
OptionsFactory
那么OptionsFactory又是如何创建Options的呢?我们看一下他的构造函数,构造函数将所有Configure和PostConfigure的初始化委托都通过构造函数保存在内部变量中
public OptionsFactory(IEnumerable<IConfigureOptions<TOptions>> setups, IEnumerable<IPostConfigureOptions<TOptions>> postConfigures)
{
_setups = setups;
_postConfigures = postConfigures;
}
接下来看Create(有删改,与本次研究无关的代码没有贴出来):
public TOptions Create(string name)
{
//首先创建对应Options的实例
TOptions val = Activator.CreateInstance<TOptions>();
//循环所有的配置项,依次执行,如果对同一个Options配置了多次,最后一次的赋值生效
foreach (IConfigureOptions<TOptions> setup in _setups)
{
var configureNamedOptions = setup as IConfigureNamedOptions<TOptions>;
if (configureNamedOptions != null)
{
//Configure中会判断传入Name的值与本身的Name值是否相同,不同则不执行Action
//这解释了我们一开始的示例中,注入了三个UserOptions,但是在IOptionsSnapshot.Value中获取到的是第一个没有名字的
//因为Value会调用OptionsManager.Get(Options.DefaultName),进而调用Factory的Create(Options.DefaultName)
configureNamedOptions.Configure(name, val);
}
else if (name == Options.DefaultName)
{
setup.Configure(val);
}
}
//PostConfigure没啥可多说了,名字判断逻辑与Configure一样
foreach (var postConfigure in _postConfigures)
{
postConfigure.PostConfigure(name, val);
}
return val;
}
NamedConfigureFromConfigurationOptions
IConfiguration配置Options的方式略有不同
对应Configure扩展方法最终调用的代码在Microsoft.Extensions.DependencyInjection.OptionsConfigurationServiceCollectionExtensions这个类中
public static IServiceCollection Configure<TOptions>(this IServiceCollection services, string name, IConfiguration config, Action<BinderOptions> configureBinder) where TOptions : class
{
services.AddOptions();
services.AddSingleton((IOptionsChangeTokenSource<TOptions>)new ConfigurationChangeTokenSource<TOptions>(name, config));
return services.AddSingleton((IConfigureOptions<TOptions>)new NamedConfigureFromConfigurationOptions<TOptions>(name, config, configureBinder));
}
扩展方法里又注入了一个IOptionsChangeTokenSource,这个类的作用是提供一个配置文件变动监听的Token
同时将IConfigureOptions实现类注册成了NamedConfigureFromConfigurationOptions
NamedConfigureFromConfigurationOptions继承了ConfigureNamedOptions,在构造函数中用IConfiguration.Bind实现了生成Options的委托
public NamedConfigureFromConfigurationOptions(string name, IConfiguration config, Action<BinderOptions> configureBinder)
: base(name, (Action<TOptions>)delegate(TOptions options)
{
config.Bind(options, configureBinder);
})
所以在Factory的Create函数中,会调用IConfiguration的Bind函数
由于IOptionsSnapshot生命周期是Scope,在配置文件变动后新的Scope中会获取最新的Options
ValidateOptions
OptionsBuilder还包含了一个Validate函数,该函数要求传入一个Func<TOptions,bool>的委托,会注入一个单例的ValidateOptions对象。
在OptionsFactory构建Options的时候会验证Options的有效性,验证失败会抛出OptionsValidationException异常
对于ValidateOptions和PostConfigureOptions都是构建Options实例时需要用到的主要模块,不过使用和内部实现都较为简单,应用场景也不是很多,本文就不对这两个类多做介绍了
结论
在Configure扩展函数中会首先调用AddOptions函数
IOptions,IOptionsSnapshot,IOptionsMonitor都是在AddOptions函数中注入的
Configure配置的选项配置委托最终会保存到ConfigureNamedOptions或NamedConfigureFromConfigurationOptions
IOptions和IOptionsSnapshot的实现类为OptionsManager
OptionsManager通过OptionsFactory创建Options的实例,并会以Name作为键存到字典中缓存实例
OptionsFactory会通过反射创建Options的实例,并调用ConfigureNamedOptions中的委托给实例赋值
现在只剩下最后一个问题了,OptionsMonitor是如何动态更新选项的呢?
其实前面的讲解中已经提到了一个关键的接口IOptionsChangeTokenSource,这个接口提供一个IChangeToken,通过ChangeToken监听这个Token就可以监听到文件的变动,我们来看下OptionsMonitor是否是这样做的吧!
//构造函数
public OptionsMonitor(IOptionsFactory<TOptions> factory, IEnumerable<IOptionsChangeTokenSource<TOptions>> sources, IOptionsMonitorCache<TOptions> cache)
{
_factory = factory;
_sources = sources;
_cache = cache;
//循环属于TOptions的所有IChangeToken
foreach (IOptionsChangeTokenSource<TOptions> source in _sources)
{
ChangeToken.OnChange(() => source.GetChangeToken(), delegate(string name)
{
//清除缓存
name = name ?? Options.DefaultName;
_cache.TryRemove(name);
}, source.Name);
}
}
public virtual TOptions Get(string name)
{
name = name ?? Options.DefaultName;
return _cache.GetOrAdd(name, () => _factory.Create(name));
}
果然是这样的吧!
.Net Core 中的选项Options的更多相关文章
- (13)ASP.NET Core 中的选项模式(Options)
1.前言 选项(Options)模式是对配置(Configuration)的功能的延伸.在12章(ASP.NET Core中的配置二)Configuration中有介绍过该功能(绑定到实体类.绑定至对 ...
- ASP.NET Core 学习笔记 第五篇 ASP.NET Core 中的选项
前言 还记得上一篇文章中所说的配置吗?本篇文章算是上一篇的延续吧.在 .NET Core 中读取配置文件大多数会为配置选项绑定一个POCO(Plain Old CLR Object)对象,并通过依赖注 ...
- asp.net core 3.0 选项模式1:使用
本篇只是从应用角度来说明asp.net core的选项模式,下一篇会从源码来分析 1.以前的方式 以前我们使用web.config/app.config时是这样使用配置的 var count = Co ...
- .NET Core 2.x中使用Named Options处理多个强类型配置实例
来源: Using multiple instances of strongly-typed settings with named options in .NET Core 2.x 作者: Andr ...
- [ASP.NET Core 3框架揭秘] Options[1]: 配置选项的正确使用方式[上篇]
依赖注入不仅是支撑整个ASP.NET Core框架的基石,也是开发ASP.NET Core应用采用的基本编程模式,所以依赖注入十分重要.依赖注入使我们可以将依赖的功能定义成服务,最终以一种松耦合的形式 ...
- [ASP.NET Core 3框架揭秘] Options[2]: 配置选项的正确使用方式[下篇]
四.直接初始化Options对象 前面演示的几个实例具有一个共同的特征,即都采用配置系统来提供绑定Options对象的原始数据,实际上,Options框架具有一个完全独立的模型,可以称为Options ...
- 理解ASP.NET Core - 选项(Options)
注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或点击此处查看全文目录 Options绑定 上期我们已经聊过了配置(IConfiguration),今天我们来聊一聊O ...
- 基于SqlSugar的开发框架循序渐进介绍(7)-- 在文件上传模块中采用选项模式【Options】处理常规上传和FTP文件上传
在基于SqlSugar的开发框架的服务层中处理文件上传的时候,我们一般有两种处理方式,一种是常规的把文件存储在本地文件系统中,一种是通过FTP方式存储到指定的FTP服务器上.这种处理应该由程序进行配置 ...
- Asp.Net Core中服务的生命周期选项区别和用法
在做一个小的Demo中,在一个界面上两次调用视图组件,并且在视图组件中都调用了数据库查询,结果发现,一直报错,将两个视图组件的调用分离,单独进行,却又是正常的,寻找一番,发现是配置依赖注入服务时,对于 ...
随机推荐
- (java4)什么是计算机
(java4)什么是计算机 computer : 全称电子计算机,俗称电脑 能够按照程序运行.自动.高速处理海量数据的现代化智能电子设备 由硬件和软件组成 常见的由台式计算机,笔记本计算机,大型计算机 ...
- tomcat与springmvc 结合 之---第19篇(下,补充) springmvc 加载.xml文件的bean标签的过程
writedby 张艳涛,上一篇写了springmvc对<mvc:annoXXXX/>标签的解析过程,其实是遗漏重要的细节,因为理解的不深入吧 今天接着解析<bean>标签 & ...
- 版本号是通过import合并而来的,不是继承来的
- Discuz 7.x/6.x 全局变量防御绕过导致代码执行
地址 http://192.168.49.2:8080/viewthread.php?tid=13&extra=page%3D1 安装成功后,找一个已存在的帖子,向其发送数据包,并在Cooki ...
- 单片机学习(一)项目的建立和vscode代码编辑环境的设置
目录 Keil项目的建立 使用vscode进行开发 工欲善其事必先利其器,因此我们先搭建一个比较舒服的开发环境. Keil项目的建立 打开Keil软件点击Project/New uVision Pro ...
- 如何将fidd上抓的包移到jmete中
1.fiddler的安装配置就不说了, 网上有很多资源, 不会太难 2.使用fiddler抓包, 相信进来看这篇文章的博友都已经会使用fiddler抓包 3.打开jmeter, 添加>测试计划& ...
- java.lang.instrument.Instrumentation
java.lang.instrument.Instrumentation 看完文档之后,我们发现这么两个接口:redefineClasses和retransformClasses.一个是重新定义cla ...
- python3 Redis利用脚本
### Redis weakpassword # 获取password def passwd_dict(dict): with open(str(password_dict), 'r', encodi ...
- 附件携马之CS免杀shellcode过国内主流杀软
0x01 写在前面 其实去年已经写过类似的文章,但是久没用了,难免有些生疏.所谓温故而知新,因此再详细的记录一下,一方面可以给各位看官做个分享,另一方面等到用时也不至于出现临阵磨枪的尴尬场面. 0x0 ...
- Blazor Server 应用程序中进行 HTTP 请求
翻译自 Waqas Anwar 2021年5月4日的文章 <Making HTTP Requests in Blazor Server Apps> [1] Blazor Server 应用 ...