19

ASP.NET Core 配置源:实时生效 | Beck's Blog

 4 years ago
source link: http://beckjin.com/2020/03/07/aspnet-configuration-reload/?
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

在之前的文章 ASP.NET Core 自定义配置源ASP.NET Core etcd 配置源 中主要是介绍如何实现自定义的配置源,但不论内置的和自定义的配置源,都会面临如何使配置修改后实时生效的问题(修改配置后在不重启服务的情况下能马上生效)。在 ASP.NET Core etcd 配置源 的最后部分其实有用到 IOptionsSnapshot Options 快照的方式获取到最新配置,但其实这里依然不是实时数据,所以本文将继续深入介绍配置使用方式及内部处理机制。(以下测试代码基于 ASP.NET Core 3.1)。

Configuration 模式

在 ASP.NET Core Web API 应用程序中 IConfiguration 服务默认已以单例模式注入到 services 中,含以下 ConfigurationProvider 中的配置信息,同时对 appsettings.jsonappsettings.Development.json 已设置 reloadOnChange 为 true,即文件内容有变化将自动更新到 IConfiguration 的对象中:

providers

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class TestController : ControllerBase
{
private readonly IConfiguration _configuration;

public TestController(IConfiguration configuration)
{
_configuration = configuration;
}

[HttpGet]
public string Get()
{
return _configuration["Name"];
}
}

在不重启服务的情况下修改 Name 的值,重新调用接口将会马上获取到最新值,所以通过从 IConfiguration 中获取配置可以做到实时生效,这主要是因为在 JsonConfigurationProvider 的实现中当设置 reloadOnChange 为 true,则会监控文件的变化,一旦有变则重新加载当前 Provider 的配置信息,整个原理和 ASP.NET Core etcd 配置源 的实现类似,JsonConfigurationProvider 源码

Options 模式

Options 模式 也是比较常用的一种配置使用方式,通过将某个 Section 的配置信息以对象的方式注入到服务中,在程序中使用将更加方便与形象,在 Options 模式 主要有 3 种使用方式,分别是:IOptionsIOptionsSnapshotIOptionsMonitor

使用方式及表现

首先在 appsettings.json 中添加如下配置:

1
2
3
4
5
6
{
...
"UserOption": {
"Name": "beck"
}
}

然后在 startup.csConfigureServices 注册配置服务:

1
2
3
4
5
public void ConfigureServices(IServiceCollection services)
{
services.Configure<UserOption>(Configuration.GetSection("UserOption"));
services.AddControllers();
}

使用方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class TestController : ControllerBase
{
private readonly IOptions<UserOption> _options;

public TestController(IOptions<UserOption> options)
{
_options = options;
}

[HttpGet]
public void Get()
{
Console.WriteLine(_options.Value.Name);

Thread.Sleep(5000); // 等待过程中,手动修改配置文件

Console.WriteLine(_options.Value.Name);
}
}

分布使用 IOptionsIOptionsSnapshotIOptionsMonitor(需改为 _options.CurrentValue) 进行测试,每种测试对配置文件中 Name 的值进行调整,具体表现结果如下:

  • IOptions:本次请求内修改不会生效,重新请求也不会生效,需重启服务;
  • IOptionsSnapshot:本次请求内修改不会生效,重新请求生效;
  • IOptionsMonitor:实时生效;

处理机制分析

基于以上表现结果,我们接下来通过 Options 源码 来分析其本质原因。

首先在服务启动阶段 HostBuilder 的 Build 方法中调用 services.AddOptions() 进行 Options 相关服务的注册,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
}

从代码中可以明显的看出 IOptionsIOptionsSnapshot 对应的实现都是OptionsManagerIOptions 的生命周期是 Singleton 模式,IOptionsSnapshot 的生命周期是 Scoped 模式,所以这个就比较好解释为什么 IOptions 方式下配置调整后需要重启才能生效,而 IOptionsSnapshot 是每次请求内不变,重新请求会变化的原因了。

另外 IOptionsMonitor 的具体实现是 OptionsMonitor,生命周期是 Singleton 模式。同时还包含了 IOptionsFactoryIOptionsMonitorCache 两个服务的注册,它们也是 Options 模式 下核心部分。

这几个服务之间的关系如下:

relation

IOptionsFactory 主要负责创建 TOptions 类型的具体对象,核心代码如下:

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 class OptionsFactory<TOptions> : IOptionsFactory<TOptions> where TOptions : class, new()
{
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.Add(result.FailureMessage);
}
}
if (failures.Count > 0)
{
throw new OptionsValidationException(name, typeof(TOptions), failures);
}
}
return options;
}
}

其中 _setups 的来源即最开始 services.Configure 注册的配置服务,_postConfigures 的来源是以 services.PostConfigure 方式注册的配置服务(本例中未使用到),它们的区别是 PostConfigure 注册的服务将在 Configure 之后执行。_validations 是参数合法性验证集合,如果有需要,可以在服务注册时指定。

OptionsManager 同时是 IOptionsIOptionsSnapshot 的实现,内部通过 OptionsCache 缓存 IOptionsFactory 创建的具体 TOptions 对象,区别在于创建出的具体对象生命周期不一样,核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class OptionsManager<TOptions> : IOptions<TOptions>, IOptionsSnapshot<TOptions> where TOptions : class, new()
{
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));
}
}

OptionsMonitorIOptionsMonitor 的实现,内部通过 IOptionsMonitorCache 缓存 IOptionsFactory 创建的具体 TOptions 对象,同时对于采用 IConfiguration 作为数据源类型的,通过 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
public class OptionsMonitor<TOptions> : IOptionsMonitor<TOptions> where TOptions : class, new()
{
public OptionsMonitor(IOptionsFactory<TOptions> factory, IEnumerable<IOptionsChangeTokenSource<TOptions>> sources, IOptionsMonitorCache<TOptions> cache)
{
foreach (var source in _sources)
{
ChangeToken.OnChange<string>(
() => source.GetChangeToken(),
(name) => InvokeChanged(name),
source.Name);
}
}

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

自实现 OptionsMonitor

Configuration 对象默认提供了 GetReloadToken 方法,所以我们也可以通过监听 Token 的变化自己实现类似 OptionsMonitor 的效果,毕竟有时候并不会选择 Configuration 模式Options 模式,以下是在控制台程序中的使用,假设使用 Autofac 作为 DI 容器,SetUserOption 内将可重新注册 UserOption 对象。

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
private static UserOption userOption;

static void Main(string[] args)
{
var configurationRoot = GetRoot();

ChangeToken.OnChange(() => configurationRoot.GetReloadToken(), () =>
{
SetUserOption(configurationRoot);
Console.WriteLine(userOption.Name);
});

Console.WriteLine("started");
Console.ReadKey();
}

private static IConfigurationRoot GetRoot()
{
var builder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", true, true);

return builder.Build();
}

private static void SetUserOption(IConfigurationRoot configuration)
{
userOption = configuration.GetSection("UserOption").Get<UserOption>();
}

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK