Asp.NetCore源码学习[1-2]:配置[Option]

在上一篇文章中,我们知道了可以通过IConfiguration访问到注入的ConfigurationRoot,但是这样只能通过索引器IConfiguration["配置名"]访问配置。这篇文章将一下如何将IConfiguration映射到强类型。

本系列源码地址

一、使用强类型访问Configuration的用法

指定需要配置的强类型MyOptions和对应的IConfiguration

  1. public void ConfigureServices(IServiceCollection services)
  2. {
  3. //使用Configuration配置Option
  4. services.Configure<MyOptions>(Configuration.GetSection("MyOptions"));
  5. //载入Configuration后再次进行配置
  6. services.PostConfigure<MyOptions>(options=> { options.FilePath = "/"; });
  7. }

在控制器中通过DI访问强类型配置,一共有三种方法可以访问到强类型配置MyOptions,分别是IOptionsIOptionsSnapshotIOptionsMonitor。先大概了解一下这三种方法的区别:

  1. public class ValuesController : ControllerBase
  2. {
  3. private readonly MyOptions _options1;
  4. private readonly MyOptions _options2;
  5. private readonly MyOptions _options3;
  6. private readonly IConfiguration _configurationRoot;
  7. public ValuesController(IConfiguration configurationRoot, IOptionsMonitor<MyOptions> options1, IOptionsSnapshot<MyOptions> options2,
  8. IOptions<MyOptions> options3 )
  9. {
  10. //IConfiguration(ConfigurationRoot)随着配置文件进行更新(需要IConfigurationProvider监听配置源的更改)
  11. _configurationRoot = configurationRoot;
  12. //单例,监听IConfiguration的IChangeToken,在配置源发生改变时,自动删除缓存
  13. //生成新的Option实例并绑定,加入缓存
  14. _options1 = options1.CurrentValue;
  15. //scoped,每次请求重新生成Option实例并从IConfiguration获取数据进行绑定
  16. _options2 = options2.Value;
  17. //单例,从IConfiguration获取数据进行绑定,只绑定一次
  18. _options3 = options3.Value;
  19. }
  20. }

二、源码解读

首先看看Configure扩展方法,方法很简单,通过DI注入了Options需要的依赖。这里注入了了三种访问强类型配置的方法所需的所有依赖,接下来我们按照这三种方法去分析源码。

  1. public static IServiceCollection Configure<TOptions>(this IServiceCollection services, IConfiguration config) where TOptions : class
  2. => services.Configure<TOptions>(Options.Options.DefaultName, config, _ => { });
  3. public static IServiceCollection Configure<TOptions>(this IServiceCollection services, string name, IConfiguration config, Action<BinderOptions> configureBinder)
  4. where TOptions : class
  5. {
  6. services.AddOptions();
  7. services.AddSingleton<IOptionsChangeTokenSource<TOptions>>(new ConfigurationChangeTokenSource<TOptions>(name, config));
  8. return services.AddSingleton<IConfigureOptions<TOptions>>(new NamedConfigureFromConfigurationOptions<TOptions>(name, config, configureBinder));
  9. }
  1. /// 为IConfigurationSection实例注册需要绑定的TOptions
  2. public static IServiceCollection AddOptions(this IServiceCollection services)
  3. {
  4. services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptions<>), typeof(OptionsManager<>)));
  5. //创建以客户端请求为范围的作用域
  6. services.TryAdd(ServiceDescriptor.Scoped(typeof(IOptionsSnapshot<>), typeof(OptionsManager<>)));
  7. services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptionsMonitor<>), typeof(OptionsMonitor<>)));
  8. services.TryAdd(ServiceDescriptor.Transient(typeof(IOptionsFactory<>), typeof(OptionsFactory<>)));
  9. services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptionsMonitorCache<>), typeof(OptionsCache<>)));
  10. return services;
  11. }

1. 通过IOptions访问强类型配置

与其有关的注入只有三个:

  1. services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptions<>), typeof(OptionsManager<>)));
  2. services.TryAdd(ServiceDescriptor.Transient(typeof(IOptionsFactory<>), typeof(OptionsFactory<>)));
  3. services.AddSingleton<IConfigureOptions<TOptions>>(new NamedConfigureFromConfigurationOptions<TOptions>(name, config, configureBinder));

从以上代码我们知道,通过IOptions访问到的其实是OptionsManager实例。

1.1 OptionsManager 的实现

通过IOptionsFactory<>创建TOptions实例,并使用OptionsCache<>充当缓存。OptionsCache<>实际上是通过ConcurrentDictionary实现了IOptionsMonitorCache接口的缓存实现,相关代码没有展示。

  1. public class OptionsManager<TOptions> : IOptions<TOptions>, IOptionsSnapshot<TOptions> where TOptions : class
  2. {
  3. private readonly IOptionsFactory<TOptions> _factory;
  4. // 单例OptionsManager的私有缓存,通过ConcurrentDictionary实现了 IOptionsMonitorCache接口
  5. // Di中注入的单例OptionsCache<> 是给 OptionsMonitor<>使用的
  6. private readonly OptionsCache<TOptions> _cache = new OptionsCache<TOptions>(); // Note: this is a private cache
  7. public OptionsManager(IOptionsFactory<TOptions> factory)
  8. {
  9. _factory = factory;
  10. }
  11. public TOptions Value
  12. {
  13. get
  14. {
  15. return Get(Options.DefaultName);
  16. }
  17. }
  18. public virtual TOptions Get(string name)
  19. {
  20. name = name ?? Options.DefaultName;
  21. return _cache.GetOrAdd(name, () => _factory.Create(name));
  22. }
  23. }

1.2 IOptionsFactory 的实现

首先通过Activator创建TOptions的实例,然后通过IConfigureNamedOptions.Configure()方法配置实例。该工厂类依赖于注入的一系列IConfigureOptions,在Di中注入的实现为NamedConfigureFromConfigurationOptions,其通过委托保存了配置源和绑定的方法

  1. /// Options工厂类 生命周期:Transient
  2. /// 单例OptionsManager和单例OptionsMonitor持有不同的工厂实例
  3. public class OptionsFactory<TOptions> : IOptionsFactory<TOptions> where TOptions : class
  4. {
  5. private readonly IEnumerable<IConfigureOptions<TOptions>> _setups;
  6. private readonly IEnumerable<IPostConfigureOptions<TOptions>> _postConfigures;
  7. public OptionsFactory(IEnumerable<IConfigureOptions<TOptions>> setups, IEnumerable<IPostConfigureOptions<TOptions>> postConfigures)
  8. {
  9. _setups = setups;
  10. _postConfigures = postConfigures;
  11. }
  12. public TOptions Create(string name)
  13. {
  14. var options = CreateInstance(name);
  15. foreach (var setup in _setups)
  16. {
  17. if (setup is IConfigureNamedOptions<TOptions> namedSetup)
  18. {
  19. namedSetup.Configure(name, options);
  20. }
  21. else if (name == Options.DefaultName)
  22. {
  23. setup.Configure(options);
  24. }
  25. }
  26. foreach (var post in _postConfigures)
  27. {
  28. post.PostConfigure(name, options);
  29. }
  30. return options;
  31. }
  32. protected virtual TOptions CreateInstance(string name)
  33. {
  34. return Activator.CreateInstance<TOptions>();
  35. }
  36. }

1.3 NamedConfigureFromConfigurationOptions 的实现

在内部通过Action委托,保存了IConfiguration.Bind()方法。该方法实现了从IConfigurationTOptions实例的赋值。

此处合并了NamedConfigureFromConfigurationOptionsConfigureNamedOptions的代码。

  1. public class NamedConfigureFromConfigurationOptions<TOptions> : ConfigureNamedOptions<TOptions>
  2. where TOptions : class
  3. {
  4. public NamedConfigureFromConfigurationOptions(string name, IConfiguration config)
  5. : this(name, config, _ => { })
  6. { }
  7. public NamedConfigureFromConfigurationOptions(string name, IConfiguration config, Action<BinderOptions> configureBinder)
  8. : this(name, options => config.Bind(options, configureBinder))
  9. { }
  10. public ConfigureNamedOptions(string name, Action<TOptions> action)
  11. {
  12. Name = name;
  13. Action = action;
  14. }
  15. public string Name { get; }
  16. public Action<TOptions> Action { get; }
  17. public virtual void Configure(string name, TOptions options)
  18. {
  19. if (Name == null || name == Name)
  20. {
  21. Action?.Invoke(options);
  22. }
  23. }
  24. public void Configure(TOptions options) => Configure(string.Empty, options);
  25. }

由于OptionsManager<>是单例模式,只会从IConfiguration中获取一次数据,在配置发生更改后,OptionsManager<>返回的TOptions实例不会更新。

2. 通过IOptionsSnapshot访问强类型配置

该方法和第一种相同,唯一不同的是,在注入DI系统的时候,其生命周期为scoped,每次请求重新创建OptionsManager<>。这样每次获取TOptions实例时,会新建实例并从IConfiguration重新获取数据对其赋值,那么TOptions实例的值自然就是最新的。

3. 通过IOptionsMonitor访问强类型配置

与其有关的注入有五个:

  1. services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptionsMonitor<>), typeof(OptionsMonitor<>)));
  2. services.TryAdd(ServiceDescriptor.Transient(typeof(IOptionsFactory<>), typeof(OptionsFactory<>)));
  3. services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptionsMonitorCache<>), typeof(OptionsCache<>)));
  4. services.AddSingleton<IOptionsChangeTokenSource<TOptions>>(new ConfigurationChangeTokenSource<TOptions>(name, config));
  5. services.AddSingleton<IConfigureOptions<TOptions>>(new NamedConfigureFromConfigurationOptions<TOptions>(name, config, configureBinder));

第二种方法在每次请求时,都新建实例进行绑定,对性能会有影响。如何监测IConfiguration的变化,在变化的时候进行重新获取TOptions实例呢?答案是通过IChangeToken去监听配置源的改变。从上一篇知道,当使用FileProviders监听文件更改时,会返回一个IChangeToken,在FileProviders中监听返回的IChangeToken可以得知文件发生了更改并进行重新加载文件数据。所以使用IConfiguration 访问到的ConfigurationRoot 永远都是最新的。在IConfigurationProviderIConfigurationRoot中也维护了IChangeToken字段,这是用于向外部一层层的传递更改通知。下图为更改通知的传递方向:

  1. graph LR
  2. A["FileProviders"]--IChangeToken-->B
  3. B["IConfigurationProvider"]--IChangeToken-->C["IConfigurationRoot"]

由于NamedConfigureFromConfigurationOptions没有直接保存IConfiguration字段,所以没办法通过它获取IConfiguration.GetReloadToken()。在源码中通过注入ConfigurationChangeTokenSource实现获取IChangeToken的目的

3.1 ConfigurationChangeTokenSource的实现

该类保存IConfiguration,并实现IOptionsChangeTokenSource接口

  1. public class ConfigurationChangeTokenSource<TOptions> : IOptionsChangeTokenSource<TOptions>
  2. {
  3. private IConfiguration _config;
  4. public ConfigurationChangeTokenSource(IConfiguration config) : this(string.Empty, config)
  5. { }
  6. public ConfigurationChangeTokenSource(string name, IConfiguration config)
  7. {
  8. _config = config;
  9. Name = name ?? string.Empty;
  10. }
  11. public string Name { get; }
  12. public IChangeToken GetChangeToken()
  13. {
  14. return _config.GetReloadToken();
  15. }
  16. }

3.2 OptionsMonitor的实现

该类通过IOptionsChangeTokenSource获取IConfigurationIChangeToken。通过监听更改通知,在配置源发生改变时,删除缓存,重新绑定强类型配置,并加入到缓存中。IOptionsMonitor接口还有一个OnChange()方法,可以注册更改通知发生时候的回调方法,在TOptions实例发生更改的时候,进行回调。值得一提的是,该类有一个内部类ChangeTrackerDisposable,在注册回调方法时,返回该类型,在需要取消回调时,通过ChangeTrackerDisposable.Dispose()取消刚刚注册的方法。

  1. public class OptionsMonitor<TOptions> : IOptionsMonitor<TOptions>, IDisposable where TOptions : class
  2. {
  3. private readonly IOptionsMonitorCache<TOptions> _cache;
  4. private readonly IOptionsFactory<TOptions> _factory;
  5. private readonly IEnumerable<IOptionsChangeTokenSource<TOptions>> _sources;
  6. private readonly List<IDisposable> _registrations = new List<IDisposable>();
  7. internal event Action<TOptions, string> _onChange;
  8. public OptionsMonitor(IOptionsFactory<TOptions> factory, IEnumerable<IOptionsChangeTokenSource<TOptions>> sources, IOptionsMonitorCache<TOptions> cache)
  9. {
  10. _factory = factory;
  11. _sources = sources;
  12. _cache = cache;
  13. foreach (var source in _sources)
  14. {
  15. var registration = ChangeToken.OnChange(
  16. () => source.GetChangeToken(),
  17. (name) => InvokeChanged(name),
  18. source.Name);
  19. _registrations.Add(registration);
  20. }
  21. }
  22. private void InvokeChanged(string name)
  23. {
  24. name = name ?? Options.DefaultName;
  25. _cache.TryRemove(name);
  26. var options = Get(name);
  27. if (_onChange != null)
  28. {
  29. _onChange.Invoke(options, name);
  30. }
  31. }
  32. public TOptions CurrentValue
  33. {
  34. get => Get(Options.DefaultName);
  35. }
  36. public virtual TOptions Get(string name)
  37. {
  38. name = name ?? Options.DefaultName;
  39. return _cache.GetOrAdd(name, () => _factory.Create(name));
  40. }
  41. public IDisposable OnChange(Action<TOptions, string> listener)
  42. {
  43. var disposable = new ChangeTrackerDisposable(this, listener);
  44. _onChange += disposable.OnChange;
  45. return disposable;
  46. }
  47. public void Dispose()
  48. {
  49. foreach (var registration in _registrations)
  50. {
  51. registration.Dispose();
  52. }
  53. _registrations.Clear();
  54. }
  55. internal class ChangeTrackerDisposable : IDisposable
  56. {
  57. private readonly Action<TOptions, string> _listener;
  58. private readonly OptionsMonitor<TOptions> _monitor;
  59. public ChangeTrackerDisposable(OptionsMonitor<TOptions> monitor, Action<TOptions, string> listener)
  60. {
  61. _listener = listener;
  62. _monitor = monitor;
  63. }
  64. public void OnChange(TOptions options, string name) => _listener.Invoke(options, name);
  65. public void Dispose() => _monitor._onChange -= OnChange;
  66. }
  67. }

4. 测试代码

本篇文章中,由于Option依赖于自带的注入系统,而本项目中Di部分还没有完成,所以,这篇文章的测试代码直接new依赖的对象。

  1. public class ConfigurationTest
  2. {
  3. public static void Run()
  4. {
  5. var builder = new ConfigurationBuilder();
  6. builder.AddJsonFile(null, $@"C:\WorkStation\Code\GitHubCode\CoreApp\CoreWebApp\appsettings.json", true,true);
  7. var configuration = builder.Build();
  8. Task.Run(() => {
  9. ChangeToken.OnChange(() => configuration.GetReloadToken(), () => {
  10. Console.WriteLine("Configuration has changed");
  11. });
  12. });
  13. var optionsChangeTokenSource = new ConfigurationChangeTokenSource<MyOption>(configuration);
  14. var configureOptions = new NamedConfigureFromConfigurationOptions<MyOption>(string.Empty, configuration);
  15. var optionsFactory = new OptionsFactory<MyOption>(new List<IConfigureOptions<MyOption>>() { configureOptions },new List<IPostConfigureOptions<MyOption>>());
  16. var optionsMonitor = new OptionsMonitor<MyOption>(optionsFactory,new List<IOptionsChangeTokenSource<MyOption>>() { optionsChangeTokenSource },new OptionsCache<MyOption>());
  17. optionsMonitor.OnChange((option,name) => {
  18. Console.WriteLine($@"optionsMonitor Detected Configuration has changed,current Value is {option.TestOption}");
  19. });
  20. Thread.Sleep(600000);
  21. }
  22. }

测试结果

回调会触发两次,这是由于FileSystemWatcher造成的,可以通过设置一个后台线程,在检测到文件变化时,主线程将标志位置true,后台线程轮询标志位

结语

至此,从IConfigurationTOptions强类型的映射已经完成。

Asp.NetCore源码学习[1-2]:配置[Option]的更多相关文章

  1. Asp.NetCore源码学习[2-1]:配置[Configuration]

    Asp.NetCore源码学习[2-1]:配置[Configuration] 在Asp. NetCore中,配置系统支持不同的配置源(文件.环境变量等),虽然有多种的配置源,但是最终提供给系统使用的只 ...

  2. Asp.NetCore源码学习[2-1]:日志

    Asp.NetCore源码学习[2-1]:日志 在一个系统中,日志是不可或缺的部分.对于.net而言有许多成熟的日志框架,包括Log4Net.NLog.Serilog 等等.你可以在系统中直接使用这些 ...

  3. 05.ElementUI源码学习:项目发布配置(github pages&npm package)

    0x00.前言 书接上文.项目第一个组件已经封装好,说明文档也已编写好.下面需要将说明文档发布到外网上,以此来展示和推广项目,使用 Github Pages功能实现.同时将组件发布之 npm 上,方便 ...

  4. Vue2.0源码学习(4) - 合并配置

    合并配置 通过之前的源码学习,我们已经了解到了new Vue主要有两种场景,第一种就是在外部主动调用new Vue创建一个实例,第二个就是代码内部创建子组件的时候自行创建一个new Vue实例.但是无 ...

  5. 【spring源码学习】spring配置的事务方式是REQUIRED,但业务层抛出TransactionRequiredException异常问题

    (1)spring抛出异常的点:org.springframework.orm.jpa.EntityManagerFactoryUtils public static DataAccessExcept ...

  6. 源码学习之ASP.NET MVC Application Using Entity Framework

    源码学习的重要性,再一次让人信服. ASP.NET MVC Application Using Entity Framework Code First 做MVC已经有段时间了,但看了一些CodePle ...

  7. geoserver源码学习与扩展——跨域访问配置

    在 geoserver源码学习与扩展——restAPI访问 博客中提到了geoserver的跨域参数设置,本文详细讲一下geoserver的跨域访问配置. geoserver的跨域访问依赖java-p ...

  8. 源码学习系列之SpringBoot自动配置(篇一)

    源码学习系列之SpringBoot自动配置源码学习(篇一) ok,本博客尝试跟一下Springboot的自动配置源码,做一下笔记记录,自动配置是Springboot的一个很关键的特性,也容易被忽略的属 ...

  9. 源码学习系列之SpringBoot自动配置(篇二)

    源码学习系列之SpringBoot自动配置(篇二)之HttpEncodingAutoConfiguration 源码分析 继上一篇博客源码学习系列之SpringBoot自动配置(篇一)之后,本博客继续 ...

随机推荐

  1. 机器学习-利用pickle加载cifar文件

    首先这里有百度云的数据集供大家下载:(官网太慢了) 链接:https://pan.baidu.com/s/1G0MxZIGSK_DyZTcuNbxraQ 提取码:ui51 复制这段内容后打开百度网盘手 ...

  2. 【Spring】The matching wildcard is strict……

    applicationContext.xml 文件抛出了这个异常信息. 解决方法: 需要在 namespace 后加上对应的 schemaLocation,如下所示: <?xml version ...

  3. 【Mac】Mac 使用 zsh 后, mvn 命令无效

    如题-- 解决方法: 将 maven 的环境变量配置放到 .zshrc 文件中. 参考链接: http://ruby-china.org/topics/23158 https://yq.aliyun. ...

  4. 微信小程序中悬浮窗功能的实现(主要探讨和解决在原生组件上的拖动)

    问题场景 所谓悬浮窗就是图中微信图标的按钮,采用fixed定位,可拖动和点击. 这算是一个比较常见的实现场景了. 为什么要用cover-view做悬浮窗?原生组件出来背锅了~ 最初我做悬浮窗用的不是c ...

  5. 如何选择合适的SSL证书类型

    网站安装SSL证书就可以将http升级为https加密模式,网站安装SSL证书因此成为一种趋势.如何为网站选择适合的SSL证书类型呢? SSL证书类型可分为2大类:1)按照验证方式分类2)按照支持域名 ...

  6. 洛谷P2763题解

    吐槽一下:蜜汁UKE是什么玩意?! 题目分析: 观察题面,对于给定的组卷要求,计算满足要求的组卷方案,可以发现这是一道明显的有条件的二分图匹配问题,于是考虑建模. 建一个超级源点,一个超级汇点:源点与 ...

  7. Keil5调试过程中遇到的一些警告和错误

    最近用keil5调试代码出了一些警告与错误,整理如下: 1.warning: #1295-D: Deprecated declaration run_c - give arg types void r ...

  8. 微信公众平台注册及AppID和AppSecret的获取

    一.注册公众平台 1.入口 浏览器搜索“微信公众平台”,进入官网,点右上角立即注册. 2.选择账号类型 注册前需要选择一个账号类型,共有4个账号类型可以选择,每种类型能提供不同的功能,功能区别见下图. ...

  9. [转载]windows下mongodb安装与使用整理

    windows下mongodb安装与使用整理 一.首先安装mongodb 1.下载地址:http://www.mongodb.org/downloads 2.解压缩到自己想要安装的目录,比如d:\mo ...

  10. idea+Spring+Mybatis+jersey+jetty构建一个简单的web项目

    一.先使用idea创建一个maven项目. 二.引入jar包,修改pom.xml <dependencies> <dependency> <groupId>org. ...