重新整理 .net core 实践篇—————服务的配置更新[十三]
阅读原文时间:2021年06月08日阅读:1

前言

前文讲述了,服务和配置直接的配合,这一节写一下,当配置文件修改了,每个服务如何感知自己的配置。

正文

服务感知到自己的配置发生变化,这就牵扯出两个东西:

IoptionsMonitor<out TOptions>

IoptionSnapshot<out TOptions>

在作用域范围使用IoptionSnapshot,在单例中使用IoptionsMonitor 。

IoptionsMonitor

先来演示作用域范围的使用。

配置:

{
  "SelfService": {
    "name" : "zhangsan"
  }
}

SelfServiceOption:

public class SelfServiceOption
{
    public string Name { get; set; }
}

服务:

public class SelfService : ISelfService
{
    IOptionsSnapshot<SelfServiceOption> _options;
    public SelfService(IOptionsSnapshot<SelfServiceOption> options)
    {
        this._options = options;
    }
    public string ShowOptionName()
    {
        return _options.Value.Name;
    }
}

注册:

services.Configure<SelfServiceOption>(Configuration.GetSection("SelfService"), BinderOptions =>
{
    BinderOptions.BindNonPublicProperties = true;
});
services.AddScoped<ISelfService, SelfService>();

测试:

[HttpGet]
public int GetService([FromServices]ISelfService selfService)
{
    Console.WriteLine(selfService.ShowOptionName());
    return 1;
}

结果:

第一次访问后修改为zhangsan_change,再次访问接口,会呈现上述效果。

那么为什么使用IoptionsMonitor,而为什么Ioptions 没有用呢。

前一篇写过Ioptions 的实现类OptionsManager,这个是有缓存的_cache,如下:

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>(); // Note: this is a private cache

    /// <summary>
    /// Initializes a new instance with the specified options configurations.
    /// </summary>
    /// <param name="factory">The factory to use to create options.</param>
    public OptionsManager(IOptionsFactory<TOptions> factory)
    {
        _factory = factory;
    }

    /// <summary>
    /// The default configured <typeparamref name="TOptions"/> instance, equivalent to Get(Options.DefaultName).
    /// </summary>
    public TOptions Value
    {
        get
        {
            return Get(Options.DefaultName);
        }
    }

    /// <summary>
    /// Returns a configured <typeparamref name="TOptions"/> instance with the given <paramref name="name"/>.
    /// </summary>
    public virtual TOptions Get(string name)
    {
        name = name ?? Options.DefaultName;

        // Store the options in our instance cache
        return _cache.GetOrAdd(name, () => _factory.Create(name));
    }
}

IoptionsMonitor的实现类也是OptionsManager,但是人家是作用域模式。

在Addoptions中:

services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptions<>), typeof(OptionsManager<>)));
services.TryAdd(ServiceDescriptor.Scoped(typeof(IOptionsSnapshot<>), typeof(OptionsManager<>)));

也就是说每创建一个SelfService,就会创建一个OptionsManager。缓存自然只在作用域内有效。

好的,那么来看下单例。

IoptionsMonitor

服务:

public class SelfService : ISelfService
{
    IOptionsMonitor<SelfServiceOption> _options;
    public SelfService(IOptionsMonitor<SelfServiceOption> options)
    {
        this._options = options;

        _options.OnChange((selftServiceOptions) =>
        {
            Console.WriteLine("alter change:"+selftServiceOptions.Name);
        });
    }
    public string ShowOptionName()
    {
        return _options.CurrentValue.Name;
    }
}

注册:

services.Configure<SelfServiceOption>(Configuration.GetSection("SelfService"), BinderOptions =>
{
    BinderOptions.BindNonPublicProperties = true;
});
services.AddSingleton<ISelfService, SelfService>();

测试接口:

[HttpGet]
public int GetService([FromServices]ISelfService selfService)
{
    Console.WriteLine(selfService.ShowOptionName());
    return 1;
}

同意是修改钱访问一次,修改后访问一次。

结果如下:

那么看下IOptionMonitor的实现类OptionsMonitor:

public class OptionsMonitor<TOptions> : IOptionsMonitor<TOptions>, IDisposable where TOptions : class, new()
{
    private readonly IOptionsMonitorCache<TOptions> _cache;
    private readonly IOptionsFactory<TOptions> _factory;
    private readonly IEnumerable<IOptionsChangeTokenSource<TOptions>> _sources;
    private readonly List<IDisposable> _registrations = new List<IDisposable>();
    internal event Action<TOptions, string> _onChange;

    /// <summary>
    /// Constructor.
    /// </summary>
    /// <param name="factory">The factory to use to create options.</param>
    /// <param name="sources">The sources used to listen for changes to the options instance.</param>
    /// <param name="cache">The cache used to store options.</param>
    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);
        }
    }

    /// <summary>
    /// The present value of the options.
    /// </summary>
    public TOptions CurrentValue
    {
        get => Get(Options.DefaultName);
    }

    /// <summary>
    /// Returns a configured <typeparamref name="TOptions"/> instance with the given <paramref name="name"/>.
    /// </summary>
    public virtual TOptions Get(string name)
    {
        name = name ?? Options.DefaultName;
        return _cache.GetOrAdd(name, () => _factory.Create(name));
    }

    /// <summary>
    /// Registers a listener to be called whenever <typeparamref name="TOptions"/> changes.
    /// </summary>
    /// <param name="listener">The action to be invoked when <typeparamref name="TOptions"/> has changed.</param>
    /// <returns>An <see cref="IDisposable"/> which should be disposed to stop listening for changes.</returns>
    public IDisposable OnChange(Action<TOptions, string> listener)
    {
        var disposable = new ChangeTrackerDisposable(this, listener);
        _onChange += disposable.OnChange;
        return disposable;
    }

    /// <summary>
    /// Removes all change registration subscriptions.
    /// </summary>
    public void Dispose()
    {
        // Remove all subscriptions to the change tokens
        foreach (var registration in _registrations)
        {
            registration.Dispose();
        }

        _registrations.Clear();
    }

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

给每个给做了监听哈:

foreach (var source in _sources)
{
    var registration = ChangeToken.OnChange(
          () => source.GetChangeToken(),
          (name) => InvokeChanged(name),
          source.Name);

    _registrations.Add(registration);
}

这个IOptionsChangeTokenSource怎么来的呢?是在我们的configure配置方法中:

public static IServiceCollection Configure<TOptions>(this IServiceCollection services, string name, IConfiguration config, Action<BinderOptions> configureBinder)
    where TOptions : class
{
    if (services == null)
    {
        throw new ArgumentNullException(nameof(services));
    }

    if (config == null)
    {
        throw new ArgumentNullException(nameof(config));
    }

    services.AddOptions();
    services.AddSingleton<IOptionsChangeTokenSource<TOptions>>(new ConfigurationChangeTokenSource<TOptions>(name, config));
    return services.AddSingleton<IConfigureOptions<TOptions>>(new NamedConfigureFromConfigurationOptions<TOptions>(name, config, configureBinder));
}

看到这一段:services.AddSingleton(new ConfigurationChangeTokenSource(name, config));。

当有修改后,那么会调用:

private void InvokeChanged(string name)
{
    name = name ?? Options.DefaultName;
    _cache.TryRemove(name);
    var options = Get(name);
    if (_onChange != null)
    {
        _onChange.Invoke(options, name);
    }
}

这里面会移除缓存_cache.TryRemove(name);,然后重新新调用: Get(name);也就会再绑定一次。

这里面有一个值得注意的是,如果有回调,不一定是本身这个服务的配置修改,可能是其他服务的配置修改了,也会被通知,因为这个是文件发生变化就会被通知。

原理如下:

GetSession会返回一个 ConfigurationSection。那么它里面的GetReloadToken是这样的:

 public IChangeToken GetReloadToken() => _root.GetReloadToken();

这返回了ConfigurationRoot的GetReloadToken。

实验一下:

{
    "SelfService": {
    "name": "zhangsan"
  },
  "SelfService2": {
    "name" : "good one"
  }
}

改成:

{
    "SelfService": {
    "name": "zhangsan"
  },
  "SelfService2": {
    "name" : "good one1"
  }
}

结果:

索引我们可以在服务里面配置增加一个version版本号,如果版本修改了,然后才做相应的操作。

以上只是个人整理,如有错误,望请指点。

下一节:配置验证。