ASP.NET Core 3.0 : 二十四. 配置的Options模式

上一章讲到了配置的用法及内部处理机制,对于配置,asp.net core还提供了一种options模式。(asp.net core 系列目录)

一、options的使用

上一章有个配置的绑定的例子,可以将配置绑定到一个theme实例中。也就是在使用对应配置的时候,需要进行一次绑定操作。而options模式提供了更直接的方式,并且可以通过依赖注入的方式提供配置的读取。下文中称每一条options配置为option。

1.简单的不为option命名的方式

依然采用这个例子,在appsettings.json中存在这样的配置:

{
  "theme": {
    "name": "blue",
    "color": "#0921dc"
  }
}

修改一下valuecontroller,代码如下:

public class valuescontroller : controller
{
    private ioptions<theme> _options = null;
    public valuescontroller(ioptions<theme> options)
    {
        _options = options;
    }

    public contentresult getoptions()
    {
        return new contentresult() { content = $"options:{ _options.value.name}" };
    }
}

依然需要在startup文件中做注册:

    public void configureservices(iservicecollection services)
    {
        services.configure<theme>(configuration.getsection("theme"));

        services.addcontrollerswithviews();  //3.0中启用的新方法
    }

 

请求这个action,获取到的结果为:

options:blue

这样就可以在需要使用该配置的时候通过依赖注入的方式使用了。但有个疑问,这里将“theme”类型绑定了这样的配置,但如果有多个这样的配置呢?就如同下面这样的配置的时候:

  "themes": [
    {
      "name": "blue",
      "color": "#0921dc"
    },
    {
      "name": "red",
      "color": "#ff4500"
    }
  ]

在这样的情况下,存在多个theme的配置,这样对于之前这种依赖注入的方式就不行了。这时系统提供了将注入的options进行命名的方法。

2.为option命名的方式

首先需要在startup文件中注册的时候对其命名,添加如下两条注册代码:

services.configure<theme>("themeblue", configuration.getsection("themes:0"));
services.configure<theme>("themered" , configuration.getsection("themes:1"));

修改valuecontroller代码,添加ioptionsmonitor<theme>和ioptionssnapshot<theme>两种新的注入方式如下:

        private ioptions<theme> _options = null;
        private ioptionsmonitor<theme> _optionsmonitor = null;
        private ioptionssnapshot<theme> _optionssnapshot = null;
        public valuescontroller(ioptions<theme> options, ioptionsmonitor<theme> optionsmonitor, ioptionssnapshot<theme> optionssnapshot)
        {
            _options = options;
            _optionsmonitor = optionsmonitor;
            _optionssnapshot = optionssnapshot;
        }

        public contentresult getoptions()
        {
            return new contentresult() { content = $"options:{_options.value.name}," +
                $"optionssnapshot:{ _optionssnapshot.get("themeblue").name }," +
                $"optionsmonitor:{_optionsmonitor.get("themered").name}" };
        }

请求这个action,获取到的结果为:

options:blue,optionssnapshot:red,optionsmonitor:gray

新增的两种注入方式通过options的名称获取到了对应的options。为什么是两种呢?它们有什么区别?不知道有没有读者想到上一章配置的重新加载功能。在配置注册的时候,有个reloadonchange选项,如果它被设置为true的,当对应的数据源发生改变的时候,会进行重新加载。而options怎么能少了这样的特性呢。

3.option的自动更新与生命周期

为了验证这三种options的读取方式的特性,修改theme类,添加一个guid字段,并在构造方法中对其赋值,代码如下:

public class theme
{
    public theme()
    {
        guid = guid.newguid();
    }
    public guid guid { get; set; }
    public string name { get; set; }
    public string color { get; set; }
}

修改上例中的名为getoptions的action的代码如下:

public contentresult getoptions()
{
    return new contentresult()
    {
        content = $"options:{_options.value.name}|{_options.value.guid}," +
        $"optionssnapshot:{ _optionssnapshot.get("themeblue").name }|{_optionssnapshot.get("themeblue").guid}," +
        $"optionsmonitor:{_optionsmonitor.get("themered").name}|{_optionsmonitor.get("themered").guid}"
    };
}

请求这个action,返回结果如下:

options:blue|ad328f15-254f-4505-a79f-4f27db4a393e,optionssnapshot:red|dba5f550-29ca-4779-9a02-781dd17f595a,optionsmonitor:gray|a799fa41-9444-45dd-b51b-fcd15049f98f

刷新页面,返回结果为:

options:blue|ad328f15-254f-4505-a79f-4f27db4a393e,optionssnapshot:red|a2350cb3-c156-4f71-bb2d-25890fe08bec,optionsmonitor:gray|a799fa41-9444-45dd-b51b-fcd15049f98f

可见ioptions和ioptionsmonitor两种方式获取到的name值和guid值均未改变,而通过ioptionssnapshot方式获取到的name值未改变,但guid值发生了改变,每次刷新页面均会改变。这类似前面讲依赖注入时做测试的例子,现在猜测guid未改变的ioptions和ioptionsmonitor两种方式是采用了singleton模式,而guid发生改变的ioptionssnapshot方式是采用了scoped或transient模式。如果在这个action中多次采用ioptionssnapshot读取_optionssnapshot.get("themeblue").guid的值,会发现同一次请求的值是相同的,不同请求之间的值是不同的,也就是ioptionssnapshot方式使采用了scoped模式(此验证示例比较简单,请读者自行修改代码验证)。

在这样的情况下,修改三种获取方式对应的配置项的name值,例如分别修改为“blue1”、“red1”和“gray1”,再次多次刷新页面查看返回值,会发现如下情况:

ioptions方式:name和guid的值始终未变。name值仍为blue。

ioptionssnapshot方式:name值变为red1,guid值单次请求内相同,每次刷新之间不同。

ioptionsmonitor方式:只有修改配置值后第一次刷新的时候将name值变为了gray1,guid未改变。之后多次刷新,这两个值均未做改变。

总结:ioptions和ioptionsmonitor两种方式采用了singleton模式,但区别在于ioptionsmonitor会监控对应数据源的变化,如果发生了变化则更新实例的配置值,但不会重新提供新的实例。ioptionssnapshot方式采用了scoped模式每次请求采用同一个实例,在下一次请求的时候获取到的是一个新的实例,所以如果数据源发生了改变,会读取到新的值。先大概记一下这一的情况,在下文剖析ioptions的内部处理机制的时候就会明白为什么会这样。

4.数据更新提醒

ioptionsmonitor方式还提供了一个onchange方法,当数据源发生改变的时候会触发它,所以如果想在这时候做点什么,可以利用这个方法实现。示例代码:

_optionsmonitor.onchange((theme,name)=> { console.writeline(theme.name +"-"+ name); });

5.不采用configuration配置作为数据源的方式

上面的例子都是采用了读取配置的方式,实际上options模式和上一章的configuration配置方式使分开的,读取配置只不过是options模式的一种实现方式,例如可以不使用configuration中的数据,直接通过如下代码注册:

services.configure<theme>("themeblack", theme => {
    theme.color = "#000000";
    theme.name = "black";
}); 

6.configureall方法

系统提供了一个configureall方法,可以将所有对应的实例统一设置。例如如下代码:

services.configureall<theme>(theme => {
     theme.color = "#000000";
     theme.name = "black2";
});

此时无论通过什么名称去获取theme的实例,包括不存在对应设置的名称,获取到的值都是本次通过configureall设置的值。

7.postconfigure和postconfigureall方法

这两个方法和configure、configureall方法类似,只是它们会在configure、configureall之后执行。

8.多个configure、configureall、postconfigure和postconfigureall的执行顺序

可以这样理解,每个configure都是去修改一个名为其设置的名称的变量,以如下代码为例:

services.configure<theme>("themeblack", theme => {
    theme.color = "#000000";
    theme.name = "black";
}); 

这条设置就是去修改(注意是修改而不是替换)一个名为"themeblack"的theme类型的变量,如果该变量不存在,则创建一个theme实例并赋值。这样就生成了一些变量名为“空字符串、“themeblue”、“themeblack”的变量(只是举例,忽略空字符串作为变量名不合法的顾虑)”。

依次按照代码的顺序执行,这时候如果后面的代码中出现同名的configure,则修改对应名称的变量的值。如果是configureall方法,则修改所有类型为theme的变量的值。

而postconfigure和postconfigureall则在configure和configureall之后执行,即使configure的代码写在了postconfigure之后也是一样。

至于为什么会是这样的规则,下一节会详细介绍。

二、内部处理机制解析

1. 系统启动阶段,依赖注入

上一节的例子中涉及到了三个接口ioptions、ioptionssnapshot和ioptionsmonitor,那么就从这三个接口说起。既然options模式是通过这三个接口的泛型方式注入提供服务的,那么在这之前系统就需要将它们对应的实现注入到依赖注入容器中。这发生在系统启动阶段创建ihost的时候,这时候hostbuilder的build方法中调用了一个services.addoptions()方法,这个方法定义在optionsservicecollectionextensions中,代码如下:

public static class optionsservicecollectionextensions
    {
        public static iservicecollection addoptions(this iservicecollection services)
        {
            if (services == null)
            {
                throw new argumentnullexception(nameof(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;
        }

        public static iservicecollection configure<toptions>(this iservicecollection services, action<toptions> configureoptions) where toptions : class
            => services.configure(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 configureall<toptions>(this iservicecollection services, action<toptions> configureoptions) where toptions : class
            => services.configure(name: null, configureoptions: configureoptions);
//省略部分代码
    }

可见这个addoptions方法的作用就是进行服务注入,ioptions<>、ioptionssnapshot<>对应的实现是optionsmanager<>,只是分别采用了singleton和scoped两种生命周期模式,ioptionsmonitor<>对应的实现是optionsmonitor<>,同样为singleton模式,这也验证了上一节例子中的猜想。除了上面提到的三个接口外,还有ioptionsfactory<>和ioptionsmonitorcache<>两个接口,这也是options模式中非常重要的两个组成部分,接下来的内容中会用到。

另外的两个configure方法就是上一节例子中用到的将具体的theme注册到options中的方法了。二者的区别就是是否为配置的option命名,而第一个configure方法就未命名的方法,通过上面的代码可知它实际上是传入了一个默认的options.options.defaultname作为名称,这个默认值是一个空字符串,也就是说,未命名的option相当于是被命名为空字符串。最终都是按照已命名的方式也就是第二个configure方法进行处理。还有一个configureall方法,它是传入了一个null作为option的名称,也是交由第二个configure处理。

在第二个configure方法中仍调用了一次addoptions方法,然后才是将具体的类型进行注入。addoptions方法中采用的都是tryadd方法进行注入,已被注入的不会被再次注入。接下来注册了一个iconfigureoptions<toptions>接口,对应的实现是configurenamedoptions<toptions>(name, configureoptions),它的代码如下:

public class configurenamedoptions<toptions> : iconfigurenamedoptions<toptions> where toptions : class
{
    public configurenamedoptions(string name, action<toptions> action)
    {
        name = name;
        action = action;
}

    public string name { get; }
    public action<toptions> action { get; }

    public virtual void configure(string name, toptions options)
    {
        if (options == null)
        {
            throw new argumentnullexception(nameof(options));
        }

        // null name is used to configure all named options.
        if (name == null || name == name)
        {
            action?.invoke(options);
        }
    }

    public void configure(toptions options) => configure(options.defaultname, options);
}

它在构造方法中存储了配置的名称(name)和创建方法(action),它的两个configure方法用于在获取options的值的时候执行对应的action来创建实例(例如示例中的theme)。在此时不会被执行。所以在此会出现3中类型的configurenamedoptions,分别是name值为具体值的、name值为为空字符串的和name值为null的。这分别对应了第一节的例子中的为option命名的configure方法、不为option命名的configure方法、以及configureall方法。

此处用到的optionsservicecollectionextensions和configurenamedoptions对应的是通过代码直接注册option的方式,例如第一节例子中的如下方式:

services.configure<theme>("themeblack", theme => { new theme { color = "#000000", name = "black" }; });

如果是以configuration作为数据源的方式,例如如下代码

services.configure<theme>("themeblue", configuration.getsection("themes:0"));

用到的是optionsservicecollectionextensions和configurenamedoptions这两个类的子类,分别为optionsconfigurationservicecollectionextensions和namedconfigurefromconfigurationoptions两个类,通过它们的名字也可以知道是专门用于采用configuration作为数据源用的,代码类似,只是多了一条关于ioptionschangetokensource的依赖注入,作用是将configuration的关于数据源变化的监听和options的关联起来,当数据源发生改变的时候可以及时更新options中的值,主要的configure方法代码如下:

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));
}

同样还有postconfigure和postconfigureall方法,和configure、configureall方法类似,只不过注入的类型为ipostconfigureoptions<toptions>。

2. options值的获取

option值的获取也就是从依赖注入容器中获取相应实现的过程。通过依赖注入阶段,已经知道了ioptions<>和ioptionssnapshot<>对应的实现是optionsmanager<>,就以optionsmanager<>为例看一下依赖注入后的服务提供过程。optionsmanager<>代码如下:

public class optionsmanager<toptions> : ioptions<toptions>, ioptionssnapshot<toptions> where toptions : class, new()
{
    private readonly ioptionsfactory<toptions> _factory;
private readonly optionscache<toptions> _cache = new optionscache<toptions>();

    public optionsmanager(ioptionsfactory<toptions> factory)
    {
        _factory = factory;
    }

    public toptions value
    {
        get
        {
            return get(options.defaultname);
        }
    }

    public virtual toptions get(string name)
    {
        name = name ?? options.defaultname;
        return _cache.getoradd(name, () => _factory.create(name));
    }
}

它有ioptionsfactory<toptions>和optionscache<toptions>两个重要的成员。如果直接获取value值,实际上是调用的另一个get(string name)方法,传入了空字符串作为name值。所以最终值的获取还是在缓存中读取,这里的代码是_cache.getoradd(name, () => _factory.create(name)),即如果缓存中存在对应的值,则返回,如果不存在,则由_factory去创建。optionsfactory<toptions>的代码如下:

public class optionsfactory<toptions> : ioptionsfactory<toptions> where toptions : class, new()
{
    private readonly ienumerable<iconfigureoptions<toptions>> _setups;
    private readonly ienumerable<ipostconfigureoptions<toptions>> _postconfigures;
    private readonly ienumerable<ivalidateoptions<toptions>> _validations;

    public optionsfactory(ienumerable<iconfigureoptions<toptions>> setups, ienumerable<ipostconfigureoptions<toptions>> postconfigures) : this(setups, postconfigures, validations: null)
    { }

    public optionsfactory(ienumerable<iconfigureoptions<toptions>> setups, ienumerable<ipostconfigureoptions<toptions>> postconfigures, ienumerable<ivalidateoptions<toptions>> validations)
    {
        _setups = setups;
        _postconfigures = postconfigures;
        _validations = validations;
}

    public toptions create(string name)
    {
        var options = new toptions();
        foreach (var setup in _setups)
        {
            if (setup is iconfigurenamedoptions<toptions> namedsetup)
            {
                namedsetup.configure(name, options);
            }
            else if (name == options.defaultname)
            {
                setup.configure(options);
            }
        }
        foreach (var post in _postconfigures)
        {
            post.postconfigure(name, options);
        }

        if (_validations != null)
        {
            var failures = new list<string>();
            foreach (var validate in _validations)
            {
                var result = validate.validate(name, options);
                if (result.failed)
                {
                    failures.addrange(result.failures);
                }
            }
            if (failures.count > 0)
            {
                throw new optionsvalidationexception(name, typeof(toptions), failures);
            }
        }

        return options;
    }
}

主要看它的toptions create(string name)方法。这里会遍历它的_setups集合,这个集合类型为ienumerable<iconfigureoptions<toptions>>,在讲options模式的依赖注入的时候已经知道,每一个configure、configureall实际上就是向依赖注入容器中注册了一个iconfigureoptions<toptions>,只是名称可能不同。而postconfigure和postconfigureall方法注册的是ipostconfigureoptions<toptions>类型,对应的就是_postconfigures集合。

首先会遍历_setups集合,调用iconfigureoptions<toptions>的configure方法,这个方法的主要代码就是:

 if (name == null || name == name)
 {
      action?.invoke(options);
 }

如果name值为null,即对应的是configureall方法,则执行该action。或者name值和需要读取的值相同,则执行该action。

_setups集合遍历之后,同样的机制遍历_postconfigures集合。这就是上一节关于configure、configureall、postconfigure和postconfigureall的执行顺序的验证。

最终返回对应的实例并写入缓存。这就是ioptions和ioptionssnapshot两种模式的处理机制,接下来看一下ioptionsmonitor模式,它对应的实现是optionsmonitor。代码如下:

public class optionsmonitor<toptions> : ioptionsmonitor<toptions> where toptions : class, new()
{
    private readonly ioptionsmonitorcache<toptions> _cache;
    private readonly ioptionsfactory<toptions> _factory;
    private readonly ienumerable<ioptionschangetokensource<toptions>> _sources;
    internal event action<toptions, string> _onchange;

    public optionsmonitor(ioptionsfactory<toptions> factory, ienumerable<ioptionschangetokensource<toptions>> sources, ioptionsmonitorcache<toptions> cache)
    {
        _factory = factory;
        _sources = sources;
        _cache = cache;

        foreach (var source in _sources)
        {
                var registration = changetoken.onchange(
                      () => source.getchangetoken(),
                      (name) => invokechanged(name),
                      source.name);

                _registrations.add(registration);        
}
    }

    private void invokechanged(string name)
    {
        name = name ?? options.defaultname;
        _cache.tryremove(name);
        var options = get(name);
        if (_onchange != null)
        {
            _onchange.invoke(options, name);
        }
    }

    public toptions currentvalue
    {
        get => get(options.defaultname);
    }

    public virtual toptions get(string name)
    {
        name = name ?? options.defaultname;
        return _cache.getoradd(name, () => _factory.create(name));
    }

    public idisposable onchange(action<toptions, string> listener)
    {
        var disposable = new changetrackerdisposable(this, listener);
        _onchange += disposable.onchange;
        return disposable;
    }

    internal class changetrackerdisposable : idisposable
    {
        private readonly action<toptions, string> _listener;
        private readonly optionsmonitor<toptions> _monitor;

        public changetrackerdisposable(optionsmonitor<toptions> monitor, action<toptions, string> listener)
        {
            _listener = listener;
            _monitor = monitor;
        }

        public void onchange(toptions options, string name) => _listener.invoke(options, name);

        public void dispose() => _monitor._onchange -= onchange;
    }
}

大部分功能和optionsmanager类似,只是由于它是采用了singleton模式,所以它是采用监听数据源改变并更新的模式。当通过configuration作为数据源注册option的时候,多了一条ioptionschangetokensource的依赖注入。当数据源发生改变的时候更新数据并触发onchange(action<toptions, string> listener),在第一节的数据更新提醒中有相关的例子。

 

本文链接:https://2i3i.com/aspnetcore_24.html ,转载请注明来源地址。
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇