AspNetCore 配置--Options

AspNetCore Options 源码解读

  • Option 监控原理
  • OptionsMonitorOptionsManager

Option

Option 监控原理

  1. ChangeToken.OnChange

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    public static class ChangeToken
    {
    /// <summary>
    /// Registers the <paramref name="changeTokenConsumer"/> action to be called whenever the token produced changes.
    /// </summary>
    /// <param name="changeTokenProducer">Produces the change token.</param>
    /// <param name="changeTokenConsumer">Action called when the token changes.</param>
    /// <returns></returns>
    public static IDisposable OnChange(Func<IChangeToken> changeTokenProducer, Action changeTokenConsumer)
    {
    if (changeTokenProducer == null)
    {
    throw new ArgumentNullException(nameof(changeTokenProducer));
    }
    if (changeTokenConsumer == null)
    {
    throw new ArgumentNullException(nameof(changeTokenConsumer));
    }

    Action<object> callback = null;
    callback = (s) =>
    {
    // The order here is important. We need to take the token and then apply our changes BEFORE
    // registering. This prevents us from possible having two change updates to process concurrently.
    //
    // If the token changes after we take the token, then we'll process the update immediately upon
    // registering the callback.
    var t = changeTokenProducer();
    try
    {
    changeTokenConsumer();
    }
    finally // We always want to ensure the callback is registered
    {
    t.RegisterChangeCallback(callback, null);
    }
    };

    return changeTokenProducer().RegisterChangeCallback(callback, null);
    }
  2. 以下每个类都会有几个相同属性或方法:

    • _reloadToken : ConfigurationReloadToken 属性

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      public class ConfigurationReloadToken : IChangeToken
      {
      private CancellationTokenSource _cts = new CancellationTokenSource();

      /// <summary>
      /// Indicates if this token will proactively raise callbacks. Callbacks are still guaranteed to be invoked, eventually.
      /// </summary>
      public bool ActiveChangeCallbacks => true;

      /// <summary>
      /// Gets a value that indicates if a change has occurred.
      /// </summary>
      public bool HasChanged => _cts.IsCancellationRequested;

      /// <summary>
      /// Registers for a callback that will be invoked when the entry has changed. <see cref="Microsoft.Extensions.Primitives.IChangeToken.HasChanged"/>
      /// MUST be set before the callback is invoked.
      /// </summary>
      /// <param name="callback">The callback to invoke.</param>
      /// <param name="state">State to be passed into the callback.</param>
      /// <returns></returns>
      public IDisposable RegisterChangeCallback(Action<object> callback, object state) => _cts.Token.Register(callback, state);

      /// <summary>
      /// Used to trigger the change token when a reload occurs.
      /// </summary>
      public void OnReload() => _cts.Cancel();
      }
    • GetReloadToken() : IChangeToken 方法

      1
      2
      3
      4
      5
      public IChangeToken GetReloadToken()
      {
      //永远返回最新的 _reloadToken ,只要调用 OnReload() 就会更新
      return _reloadToken;
      }
    • OnReload() : void 方法

      1
      2
      3
      4
      5
      6
      7
      8
      protected void OnReload()
      {
      //生成新的ConfigurationReloadToken,赋值到 _reloadToken,并将原来的 _reloadToken 赋值给 previousToken
      var previousToken = Interlocked.Exchange(ref _reloadToken, new ConfigurationReloadToken());

      //调用原来的 ConfigurationReloadToken 中 OnReload,会触发已经注册过回调函数
      previousToken.OnReload();
      }
  3. FileConfigurationProvider

    1
    2
    3
    4
    5
    6
    ChangeToken.OnChange(
    () => Source.FileProvider.Watch(Source.Path),
    () => {
    Thread.Sleep(Source.ReloadDelay);
    Load(reload: true);
    });

    FileConfigurationProvider 初始化时会在 ChangeTokenchangeTokenProducer 传一个委托 () => Source.FileProvider.Watch(Source.Path) ,这个委托会中会实现一个 FileSystemWatcher 类,这个监控类有 OnChanged/OnCreated/OnDeleted/OnError/OnRenamed 这些事件。

    e.g. OnChanged 事件,在文件修改的时候触发,其中 CancelToken(<matchtoken>) 是执行取消的。取消同时就会触发已经注册的回调,回调做两件事(看 ChangeToken.Onchange 中的 callback):其一,执行 changeTokenConsumer 委托。其二,获取最新的 reloadToken ,给它注册上取消回调 。这里取消的回调就是:

    1
    2
    3
    4
    () => {
    Thread.Sleep(Source.ReloadDelay);
    Load(reload: true);
    })
  4. ConfigurationRoot

    1
    2
    3
    4
    5
    foreach (var p in providers)
    {
    p.Load();
    ChangeToken.OnChange(() => p.GetReloadToken(), () => RaiseChanged());
    }

    ConfigurationRoot 初始化的时候,会给所有 ConfigurationProviderreloadToken 注册一个 callback,这个 callback 做两件事:其一, 执行 changeTokenConsumer 委托。其二,获取最新的 reloadToken ,给它注册上取消回调 。

    这里 changeTokenConsumer 委托是 () => RaiseChanged() , changeTokenProducer 委托是 () => p.GetReloadToken()

    FileConfigurationProvider 这一步时,文件更改最终会消费其 changeTokenConsumer ,其中有个方法 Load(reload: true),此方法会调用 FileConfigurationProvider 中的 OnReload() 方法。

    前面我们说过,OnReload() 方法会生成一个新的 reloadToken ,同时调用之前的 reloadTokenOnReload ,从而触发已经注册过回调函数。

    FileConfigurationProvider 的取消回调函数哪里来的?? ConfigurationRoot 的初始化构造函数会注入 changeTokenConsumer() => RaiseChanged(), 这个委托会调用 OnReload ,从而激活下游。

以上是整个监控链的构造过程,修改文件是否修改Options还有一个重要因素 reloadOnChange,此属性来自 ConfigurationSource

1
2
3
4
var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.Build();

OptionsMonitorOptionsManager

构造函数中通过以下代码,于上游的 ConfigurationRoot 建立联系。

1
2
3
4
ChangeToken.OnChange<string>(
() => source.GetChangeToken(),
(name) => InvokeChanged(name),
source.Name);

changeTokenProducer : () => source.GetChangeToken() ,获取到的是 IConfigurationIChangeToken。换句话说,IConfiguration 上的取消回调函数来自于这里的 changeTokenConsumer 委托:(name) => InvokeChanged(name)

OptionsManager<TOptions> 是以 Singleton 生命周期注入的,每次实例化都会 new 一个 OptionsCache

OptionsManager<TOptions> 的取值是通过 Get(name) 方法, Get 是通过 cacheGetorAdd(name,()=>_factory.Create(name)

_factory 即 OptionsFactory, Create 方法的实现是获取所有 IConfigureOptions<TOptions>IPostConfigureOptions<TOptions> ,执行其中的 Configure 方法 , 从而执行所有委托

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public OptionsFactory(IEnumerable<IConfigureOptions<TOptions>> setups, IEnumerable<IPostConfigureOptions<TOptions>> postConfigures)
{
_setups = setups;
_postConfigures = postConfigures;
}

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

cacheGetorAdd 封装了一个支持并发的字典 ConcurrentDictionary<string, Lazy<TOptions>> ,最后是将要执行的委托 ()=>_factory.Create(name) , GetOrAdd 到这个字典中去。 Lazy 保证了多线程,并发 GetOrAdd 同一个 Key ,虽然可能会已经有多个相同委托产生,但延迟执行最后只取第一个执行的委托的返回值 TOptions

OptionsManager 相比 ,OptionsMonitor 更加灵活,首先 OptionsMonitor 下的 _cache 是由生命周期 Singleton 注入的 OptionsCache ,相较于 OptionsManager 自己实例化的私有变量 _cache,它可以随时随地清除缓存,移除新增等。

可以通过 OptionsMonitor.OnChange(Action<TOptions, string> listener) 注册事件,OptionsMonitor 有一个事件类型的字段 _onChange , 在 IConfigurationRoot reload 的时候触发事件。

总之, 虽然两个都是全局的缓存。但是 OptionsMonitor 更加灵活,可注册事件,并且随着绑定的 IConfigurationRoot 实例的变化而改变,并且触发事件,还可以管理缓存。

OptionsMonitor 是全局 Singleton ,通过 event 注册,并发会线程安全吗??