ASP.NET Core 配置源:实时生效

在之前的文章 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 的对象中:

代码如下:

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 源码[1]

Options 模式

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

使用方式及表现

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

{
  ...
  "UserOption": {
    "Name": "beck"
  }
}

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

public void ConfigureServices(IServiceCollection services)
{
  services.Configure<UserOption>(Configuration.GetSection("UserOption"));
  services.AddControllers();
}

使用方式如下:

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 源码[2] 来分析其本质原因。

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

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 模式 下核心部分。

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

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

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 对象,区别在于创建出的具体对象生命周期不一样,核心代码如下:

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 监听变化并实时更新配置。

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 对象。

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

参考资料

[1]

JsonConfigurationProvider 源码: https://github.com/aspnet/Configuration/blob/master/src/Config.Json/JsonConfigurationProvider.cs

[2]

Options 源码: https://github.com/aspnet/Options/tree/master/src/Microsoft.Extensions.Options

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值