.Net Core 中选项Options的具体实现

2022-07-21,,,

目录
  • 由代码开始
    • 定义一个用户配置选项
    • 定义json配置文件:myconfig.json
    • 创建servicecollection
    • 示例代码
    • 代码运行结果
    • 通过运行代码得到的结论
    • 问题
  • 配合源码解决疑惑
    • configure注入
    • optionsmanager
    • optionsfactory
    • namedconfigurefromconfigurationoptions
    • validateoptions
    • 结论

.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的具体实现的文章就介绍到这了,更多相关.net core options内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!

《.Net Core 中选项Options的具体实现.doc》

下载本文的Word格式文档,以方便收藏与打印。