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

目录
  • 由代码开始
    • 定义一个用户配置选项
    • 定义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内容请搜索www.887551.com以前的文章或继续浏览下面的相关文章希望大家以后多多支持www.887551.com!

(0)
上一篇 2022年3月23日
下一篇 2022年3月23日

相关推荐