重新整理 .net core 實踐篇—————服務的配置更新[十三]

不問前世發表於2021-06-07

前言

前文講述了,服務和配置直接的配合,這一節寫一下,當配置檔案修改了,每個服務如何感知自己的配置。

正文

服務感知到自己的配置發生變化,這就牽扯出兩個東西:

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<IOptionsChangeTokenSource>(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版本號,如果版本修改了,然後才做相應的操作。

以上只是個人整理,如有錯誤,望請指點。

下一節:配置驗證。

相關文章