.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创建一个,否则就读缓存中的选项。

IOptionsIOptionsSnapshot的实现类都是OptionsManager,只是生命周期不同。

OptionsFactory

那么OptionsFactory又是如何创建Options的呢?我们看一下他的构造函数,构造函数将所有ConfigurePostConfigure的初始化委托都通过构造函数保存在内部变量中

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函数中,会调用IConfigurationBind函数

由于IOptionsSnapshot生命周期是Scope,在配置文件变动后新的Scope中会获取最新的Options

ValidateOptions

OptionsBuilder还包含了一个Validate函数,该函数要求传入一个Func<TOptions,bool>的委托,会注入一个单例的ValidateOptions对象。

OptionsFactory构建Options的时候会验证Options的有效性,验证失败会抛出OptionsValidationException异常

对于ValidateOptionsPostConfigureOptions都是构建Options实例时需要用到的主要模块,不过使用和内部实现都较为简单,应用场景也不是很多,本文就不对这两个类多做介绍了

结论

Configure扩展函数中会首先调用AddOptions函数

IOptions,IOptionsSnapshot,IOptionsMonitor都是在AddOptions函数中注入的

Configure配置的选项配置委托最终会保存到ConfigureNamedOptionsNamedConfigureFromConfigurationOptions

IOptionsIOptionsSnapshot的实现类为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的更多相关文章

  1. (13)ASP.NET Core 中的选项模式(Options)

    1.前言 选项(Options)模式是对配置(Configuration)的功能的延伸.在12章(ASP.NET Core中的配置二)Configuration中有介绍过该功能(绑定到实体类.绑定至对 ...

  2. ASP.NET Core 学习笔记 第五篇 ASP.NET Core 中的选项

    前言 还记得上一篇文章中所说的配置吗?本篇文章算是上一篇的延续吧.在 .NET Core 中读取配置文件大多数会为配置选项绑定一个POCO(Plain Old CLR Object)对象,并通过依赖注 ...

  3. asp.net core 3.0 选项模式1:使用

    本篇只是从应用角度来说明asp.net core的选项模式,下一篇会从源码来分析 1.以前的方式 以前我们使用web.config/app.config时是这样使用配置的 var count = Co ...

  4. .NET Core 2.x中使用Named Options处理多个强类型配置实例

    来源: Using multiple instances of strongly-typed settings with named options in .NET Core 2.x 作者: Andr ...

  5. [ASP.NET Core 3框架揭秘] Options[1]: 配置选项的正确使用方式[上篇]

    依赖注入不仅是支撑整个ASP.NET Core框架的基石,也是开发ASP.NET Core应用采用的基本编程模式,所以依赖注入十分重要.依赖注入使我们可以将依赖的功能定义成服务,最终以一种松耦合的形式 ...

  6. [ASP.NET Core 3框架揭秘] Options[2]: 配置选项的正确使用方式[下篇]

    四.直接初始化Options对象 前面演示的几个实例具有一个共同的特征,即都采用配置系统来提供绑定Options对象的原始数据,实际上,Options框架具有一个完全独立的模型,可以称为Options ...

  7. 理解ASP.NET Core - 选项(Options)

    注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或点击此处查看全文目录 Options绑定 上期我们已经聊过了配置(IConfiguration),今天我们来聊一聊O ...

  8. 基于SqlSugar的开发框架循序渐进介绍(7)-- 在文件上传模块中采用选项模式【Options】处理常规上传和FTP文件上传

    在基于SqlSugar的开发框架的服务层中处理文件上传的时候,我们一般有两种处理方式,一种是常规的把文件存储在本地文件系统中,一种是通过FTP方式存储到指定的FTP服务器上.这种处理应该由程序进行配置 ...

  9. Asp.Net Core中服务的生命周期选项区别和用法

    在做一个小的Demo中,在一个界面上两次调用视图组件,并且在视图组件中都调用了数据库查询,结果发现,一直报错,将两个视图组件的调用分离,单独进行,却又是正常的,寻找一番,发现是配置依赖注入服务时,对于 ...

随机推荐

  1. (java4)什么是计算机

    (java4)什么是计算机 computer : 全称电子计算机,俗称电脑 能够按照程序运行.自动.高速处理海量数据的现代化智能电子设备 由硬件和软件组成 常见的由台式计算机,笔记本计算机,大型计算机 ...

  2. tomcat与springmvc 结合 之---第19篇(下,补充) springmvc 加载.xml文件的bean标签的过程

    writedby 张艳涛,上一篇写了springmvc对<mvc:annoXXXX/>标签的解析过程,其实是遗漏重要的细节,因为理解的不深入吧 今天接着解析<bean>标签 & ...

  3. 版本号是通过import合并而来的,不是继承来的

  4. Discuz 7.x/6.x 全局变量防御绕过导致代码执行

    地址 http://192.168.49.2:8080/viewthread.php?tid=13&extra=page%3D1 安装成功后,找一个已存在的帖子,向其发送数据包,并在Cooki ...

  5. 单片机学习(一)项目的建立和vscode代码编辑环境的设置

    目录 Keil项目的建立 使用vscode进行开发 工欲善其事必先利其器,因此我们先搭建一个比较舒服的开发环境. Keil项目的建立 打开Keil软件点击Project/New uVision Pro ...

  6. 如何将fidd上抓的包移到jmete中

    1.fiddler的安装配置就不说了, 网上有很多资源, 不会太难 2.使用fiddler抓包, 相信进来看这篇文章的博友都已经会使用fiddler抓包 3.打开jmeter, 添加>测试计划& ...

  7. java.lang.instrument.Instrumentation

    java.lang.instrument.Instrumentation 看完文档之后,我们发现这么两个接口:redefineClasses和retransformClasses.一个是重新定义cla ...

  8. python3 Redis利用脚本

    ### Redis weakpassword # 获取password def passwd_dict(dict): with open(str(password_dict), 'r', encodi ...

  9. 附件携马之CS免杀shellcode过国内主流杀软

    0x01 写在前面 其实去年已经写过类似的文章,但是久没用了,难免有些生疏.所谓温故而知新,因此再详细的记录一下,一方面可以给各位看官做个分享,另一方面等到用时也不至于出现临阵磨枪的尴尬场面. 0x0 ...

  10. Blazor Server 应用程序中进行 HTTP 请求

    翻译自 Waqas Anwar 2021年5月4日的文章 <Making HTTP Requests in Blazor Server Apps> [1] Blazor Server 应用 ...